Merge "Add OnControllableInsetsChangedListener" into rvc-dev

This commit is contained in:
Jorim Jaggi
2020-03-11 13:33:33 +00:00
committed by Android (Google) Code Review
9 changed files with 267 additions and 11 deletions

View File

@@ -55582,10 +55582,12 @@ package android.view {
}
public interface WindowInsetsController {
method public void addOnControllableInsetsChangedListener(@NonNull android.view.WindowInsetsController.OnControllableInsetsChangedListener);
method @NonNull public android.os.CancellationSignal controlWindowInsetsAnimation(int, long, @Nullable android.view.animation.Interpolator, @NonNull android.view.WindowInsetsAnimationControlListener);
method public int getSystemBarsAppearance();
method public int getSystemBarsBehavior();
method public void hide(int);
method public void removeOnControllableInsetsChangedListener(@NonNull android.view.WindowInsetsController.OnControllableInsetsChangedListener);
method public void setSystemBarsAppearance(int, int);
method public void setSystemBarsBehavior(int);
method public void show(int);
@@ -55596,6 +55598,10 @@ package android.view {
field public static final int BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE = 2; // 0x2
}
public static interface WindowInsetsController.OnControllableInsetsChangedListener {
method public void onControllableInsetsChanged(@NonNull android.view.WindowInsetsController, int);
}
public interface WindowManager extends android.view.ViewManager {
method @NonNull public default android.view.WindowMetrics getCurrentWindowMetrics();
method @Deprecated public android.view.Display getDefaultDisplay();

View File

@@ -28,6 +28,7 @@ import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import java.lang.annotation.Retention;
@@ -94,6 +95,13 @@ public class SoftInputWindow extends Dialog {
lp.token = token;
getWindow().setAttributes(lp);
updateWindowState(SoftInputWindowState.TOKEN_SET);
// As soon as we have a token, make sure the window is added (but not shown) by
// setting visibility to INVISIBLE and calling show() on Dialog. Note that
// WindowInsetsController.OnControllableInsetsChangedListener relies on the window
// being added to function.
getWindow().getDecorView().setVisibility(View.INVISIBLE);
show();
return;
case SoftInputWindowState.TOKEN_SET:
case SoftInputWindowState.SHOWN_AT_LEAST_ONCE:

View File

@@ -52,6 +52,7 @@ import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -59,6 +60,7 @@ import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
/**
@@ -306,6 +308,11 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
private SyncRtSurfaceTransactionApplier mApplier;
private Runnable mPendingControlTimeout = this::abortPendingImeControlRequest;
private final ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners
= new ArrayList<>();
/** Set of inset types for which an animation was started since last resetting this field */
private @InsetsType int mLastStartedAnimTypes;
public InsetsController(ViewRootImpl viewRoot) {
this(viewRoot, (controller, type) -> {
@@ -459,6 +466,13 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
}
mTmpControlArray.clear();
// Do not override any animations that the app started in the OnControllableInsetsChanged
// listeners.
int animatingTypes = invokeControllableInsetsChangedListeners();
showTypes[0] &= ~animatingTypes;
hideTypes[0] &= ~animatingTypes;
if (showTypes[0] != 0) {
applyAnimation(showTypes[0], true /* show */, false /* fromIme */);
}
@@ -542,9 +556,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
private CancellationSignal controlWindowInsetsAnimation(@InsetsType int types,
WindowInsetsAnimationControlListener listener, boolean fromIme, long durationMs,
@Nullable Interpolator interpolator, @AnimationType int animationType) {
// If the frame of our window doesn't span the entire display, the control API makes very
// little sense, as we don't deal with negative insets. So just cancel immediately.
if (!mState.getDisplayFrame().equals(mFrame)) {
if (!checkDisplayFramesForControlling()) {
listener.onCancelled();
CancellationSignal cancellationSignal = new CancellationSignal();
cancellationSignal.cancel();
@@ -554,6 +566,13 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
false /* fade */, animationType, getLayoutInsetsDuringAnimationMode(types));
}
private boolean checkDisplayFramesForControlling() {
// If the frame of our window doesn't span the entire display, the control API makes very
// little sense, as we don't deal with negative insets. So just cancel immediately.
return mState.getDisplayFrame().equals(mFrame);
}
private CancellationSignal controlAnimationUnchecked(@InsetsType int types,
WindowInsetsAnimationControlListener listener, Rect frame, boolean fromIme,
long durationMs, Interpolator interpolator, boolean fade,
@@ -567,6 +586,7 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
return cancellationSignal;
}
cancelExistingControllers(types);
mLastStartedAnimTypes |= types;
final ArraySet<Integer> internalTypes = InsetsState.toInternalType(types);
final SparseArray<InsetsSourceControl> controls = new SparseArray<>();
@@ -994,6 +1014,48 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
return mViewRoot.mWindowAttributes.insetsFlags.behavior;
}
private @InsetsType int calculateControllableTypes() {
if (!checkDisplayFramesForControlling()) {
return 0;
}
@InsetsType int result = 0;
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
if (consumer.getControl() != null) {
result |= toPublicType(consumer.mType);
}
}
return result;
}
/**
* @return The types that are now animating due to a listener invoking control/show/hide
*/
private @InsetsType int invokeControllableInsetsChangedListeners() {
mLastStartedAnimTypes = 0;
@InsetsType int types = calculateControllableTypes();
int size = mControllableInsetsChangedListeners.size();
for (int i = 0; i < size; i++) {
mControllableInsetsChangedListeners.get(i).onControllableInsetsChanged(this, types);
}
return mLastStartedAnimTypes;
}
@Override
public void addOnControllableInsetsChangedListener(
OnControllableInsetsChangedListener listener) {
Objects.requireNonNull(listener);
mControllableInsetsChangedListeners.add(listener);
listener.onControllableInsetsChanged(this, calculateControllableTypes());
}
@Override
public void removeOnControllableInsetsChangedListener(
OnControllableInsetsChangedListener listener) {
Objects.requireNonNull(listener);
mControllableInsetsChangedListeners.remove(listener);
}
/**
* At the time we receive new leashes (e.g. InsetsSourceConsumer is processing
* setControl) we need to release the old leash. But we may have already scheduled

View File

@@ -38,6 +38,8 @@ public class PendingInsetsController implements WindowInsetsController {
private @Behavior int mBehavior = KEEP_BEHAVIOR;
private final InsetsState mDummyState = new InsetsState();
private InsetsController mReplayedInsetsController;
private ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners
= new ArrayList<>();
@Override
public void show(int types) {
@@ -112,6 +114,27 @@ public class PendingInsetsController implements WindowInsetsController {
return mDummyState;
}
@Override
public void addOnControllableInsetsChangedListener(
OnControllableInsetsChangedListener listener) {
if (mReplayedInsetsController != null) {
mReplayedInsetsController.addOnControllableInsetsChangedListener(listener);
} else {
mControllableInsetsChangedListeners.add(listener);
listener.onControllableInsetsChanged(this, 0);
}
}
@Override
public void removeOnControllableInsetsChangedListener(
OnControllableInsetsChangedListener listener) {
if (mReplayedInsetsController != null) {
mReplayedInsetsController.removeOnControllableInsetsChangedListener(listener);
} else {
mControllableInsetsChangedListeners.remove(listener);
}
}
/**
* Replays the commands on {@code controller} and attaches it to this instance such that any
* calls will be forwarded to the real instance in the future.
@@ -128,9 +151,15 @@ public class PendingInsetsController implements WindowInsetsController {
for (int i = 0; i < size; i++) {
mRequests.get(i).replay(controller);
}
size = mControllableInsetsChangedListeners.size();
for (int i = 0; i < size; i++) {
controller.addOnControllableInsetsChangedListener(
mControllableInsetsChangedListeners.get(i));
}
// Reset all state so it doesn't get applied twice just in case
mRequests.clear();
mControllableInsetsChangedListeners.clear();
mBehavior = KEEP_BEHAVIOR;
mAppearance = 0;
mAppearanceMask = 0;

View File

@@ -20,7 +20,9 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.graphics.Insets;
import android.inputmethodservice.InputMethodService;
import android.os.CancellationSignal;
import android.view.WindowInsets.Type;
import android.view.WindowInsets.Type.InsetsType;
import android.view.animation.Interpolator;
@@ -212,4 +214,55 @@ public interface WindowInsetsController {
* @hide
*/
InsetsState getState();
/**
* Adds a {@link OnControllableInsetsChangedListener} to the window insets controller.
*
* @param listener The listener to add.
*
* @see OnControllableInsetsChangedListener
* @see #removeOnControllableInsetsChangedListener(OnControllableInsetsChangedListener)
*/
void addOnControllableInsetsChangedListener(
@NonNull OnControllableInsetsChangedListener listener);
/**
* Removes a {@link OnControllableInsetsChangedListener} from the window insets controller.
*
* @param listener The listener to remove.
*
* @see OnControllableInsetsChangedListener
* @see #addOnControllableInsetsChangedListener(OnControllableInsetsChangedListener)
*/
void removeOnControllableInsetsChangedListener(
@NonNull OnControllableInsetsChangedListener listener);
/**
* Listener to be notified when the set of controllable {@link InsetsType} controlled by a
* {@link WindowInsetsController} changes.
* <p>
* Once a {@link InsetsType} becomes controllable, the app will be able to control the window
* that is causing this type of insets by calling {@link #controlWindowInsetsAnimation}.
* <p>
* Note: When listening to controllability of the {@link Type#ime},
* {@link #controlWindowInsetsAnimation} may still fail in case the {@link InputMethodService}
* decides to cancel the show request. This could happen when there is a hardware keyboard
* attached.
*
* @see #addOnControllableInsetsChangedListener(OnControllableInsetsChangedListener)
* @see #removeOnControllableInsetsChangedListener(OnControllableInsetsChangedListener)
*/
interface OnControllableInsetsChangedListener {
/**
* Called when the set of controllable {@link InsetsType} changes.
*
* @param controller The controller for which the set of controllable {@link InsetsType}s
* are changing.
* @param typeMask Bitwise type-mask of the {@link InsetsType}s the controller is currently
* able to control.
*/
void onControllableInsetsChanged(@NonNull WindowInsetsController controller,
@InsetsType int typeMask);
}
}

View File

@@ -49,6 +49,7 @@ import android.os.CancellationSignal;
import android.platform.test.annotations.Presubmit;
import android.view.SurfaceControl.Transaction;
import android.view.WindowInsets.Type;
import android.view.WindowInsetsController.OnControllableInsetsChangedListener;
import android.view.WindowManager.BadTokenException;
import android.view.WindowManager.LayoutParams;
import android.view.animation.LinearInterpolator;
@@ -67,6 +68,8 @@ import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mockito;
import java.util.concurrent.CountDownLatch;
import java.util.function.Supplier;
@@ -171,15 +174,24 @@ public class InsetsControllerTest {
mController.onControlsChanged(new InsetsSourceControl[] { control });
assertEquals(mLeash,
mController.getSourceConsumer(ITYPE_STATUS_BAR).getControl().getLeash());
mController.addOnControllableInsetsChangedListener(
((controller, typeMask) -> assertEquals(statusBars(), typeMask)));
}
@Test
public void testControlsRevoked() {
OnControllableInsetsChangedListener listener
= mock(OnControllableInsetsChangedListener.class);
mController.addOnControllableInsetsChangedListener(listener);
InsetsSourceControl control =
new InsetsSourceControl(ITYPE_STATUS_BAR, mLeash, new Point());
mController.onControlsChanged(new InsetsSourceControl[] { control });
mController.onControlsChanged(new InsetsSourceControl[0]);
assertNull(mController.getSourceConsumer(ITYPE_STATUS_BAR).getControl());
InOrder inOrder = Mockito.inOrder(listener);
inOrder.verify(listener).onControllableInsetsChanged(eq(mController), eq(0));
inOrder.verify(listener).onControllableInsetsChanged(eq(mController), eq(statusBars()));
inOrder.verify(listener).onControllableInsetsChanged(eq(mController), eq(0));
}
@Test
@@ -206,10 +218,15 @@ public class InsetsControllerTest {
public void testFrameDoesntMatchDisplay() {
mController.onFrameChanged(new Rect(0, 0, 100, 100));
mController.getState().setDisplayFrame(new Rect(0, 0, 200, 200));
InsetsSourceControl control =
new InsetsSourceControl(ITYPE_STATUS_BAR, mLeash, new Point());
mController.onControlsChanged(new InsetsSourceControl[] { control });
WindowInsetsAnimationControlListener controlListener =
mock(WindowInsetsAnimationControlListener.class);
mController.controlWindowInsetsAnimation(0, 0 /* durationMs */, new LinearInterpolator(),
controlListener);
mController.addOnControllableInsetsChangedListener(
(controller, typeMask) -> assertEquals(0, typeMask));
verify(controlListener).onCancelled();
verify(controlListener, never()).onReady(any(), anyInt());
}

View File

@@ -25,12 +25,14 @@ import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import android.os.CancellationSignal;
import android.platform.test.annotations.Presubmit;
import android.view.WindowInsetsController.OnControllableInsetsChangedListener;
import android.view.animation.LinearInterpolator;
import org.junit.Before;
@@ -162,12 +164,44 @@ public class PendingInsetsControllerTest {
APPEARANCE_LIGHT_STATUS_BARS);
}
@Test
public void testAddOnControllableInsetsChangedListener() {
OnControllableInsetsChangedListener listener =
mock(OnControllableInsetsChangedListener.class);
mPendingInsetsController.addOnControllableInsetsChangedListener(listener);
mPendingInsetsController.replayAndAttach(mReplayedController);
verify(mReplayedController).addOnControllableInsetsChangedListener(eq(listener));
verify(listener).onControllableInsetsChanged(eq(mPendingInsetsController), eq(0));
}
@Test
public void testAddRemoveControllableInsetsChangedListener() {
OnControllableInsetsChangedListener listener =
mock(OnControllableInsetsChangedListener.class);
mPendingInsetsController.addOnControllableInsetsChangedListener(listener);
mPendingInsetsController.removeOnControllableInsetsChangedListener(listener);
mPendingInsetsController.replayAndAttach(mReplayedController);
verify(mReplayedController, never()).addOnControllableInsetsChangedListener(any());
verify(listener).onControllableInsetsChanged(eq(mPendingInsetsController), eq(0));
}
@Test
public void testAddOnControllableInsetsChangedListener_direct() {
mPendingInsetsController.replayAndAttach(mReplayedController);
OnControllableInsetsChangedListener listener =
mock(OnControllableInsetsChangedListener.class);
mPendingInsetsController.addOnControllableInsetsChangedListener(listener);
verify(mReplayedController).addOnControllableInsetsChangedListener(eq(listener));
}
@Test
public void testReplayTwice() {
mPendingInsetsController.show(systemBars());
mPendingInsetsController.setSystemBarsBehavior(BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
mPendingInsetsController.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS,
APPEARANCE_LIGHT_STATUS_BARS);
mPendingInsetsController.addOnControllableInsetsChangedListener(
(controller, typeMask) -> {});
mPendingInsetsController.replayAndAttach(mReplayedController);
InsetsController secondController = mock(InsetsController.class);
mPendingInsetsController.replayAndAttach(secondController);

View File

@@ -2447,10 +2447,6 @@ public class WindowManagerService extends IWindowManager.Stub
// of a transaction to avoid artifacts.
win.mAnimatingExit = true;
} else {
final DisplayContent displayContent = win.getDisplayContent();
if (displayContent.mInputMethodWindow == win) {
displayContent.setInputMethodWindowLocked(null);
}
boolean stopped = win.mActivityRecord != null ? win.mActivityRecord.mAppStopped : true;
// We set mDestroying=true so ActivityRecord#notifyAppStopped in-to destroy surfaces
// will later actually destroy the surface if we do not do so here. Normally we leave

View File

@@ -16,11 +16,15 @@
package com.google.android.test.windowinsetstests;
import static android.view.WindowInsets.Type.ime;
import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
import static java.lang.Math.max;
import static java.lang.Math.min;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
@@ -37,6 +41,8 @@ import android.view.WindowInsetsAnimation;
import android.view.WindowInsetsAnimation.Callback;
import android.view.WindowInsetsAnimationControlListener;
import android.view.WindowInsetsAnimationController;
import android.view.WindowInsetsController;
import android.view.WindowInsetsController.OnControllableInsetsChangedListener;
import android.view.animation.LinearInterpolator;
import android.widget.LinearLayout;
@@ -82,8 +88,8 @@ public class WindowInsetsActivity extends AppCompatActivity {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDown = event.getY();
mDownInsets = v.getRootWindowInsets().getInsets(Type.ime());
mShownAtDown = v.getRootWindowInsets().isVisible(Type.ime());
mDownInsets = v.getRootWindowInsets().getInsets(ime());
mShownAtDown = v.getRootWindowInsets().isVisible(ime());
mRequestedController = false;
mCurrentRequest = null;
break;
@@ -94,7 +100,7 @@ public class WindowInsetsActivity extends AppCompatActivity {
> mViewConfiguration.getScaledTouchSlop()
&& !mRequestedController) {
mRequestedController = true;
v.getWindowInsetsController().controlWindowInsetsAnimation(Type.ime(),
v.getWindowInsetsController().controlWindowInsetsAnimation(ime(),
1000, new LinearInterpolator(),
mCurrentRequest = new WindowInsetsAnimationControlListener() {
@Override
@@ -189,6 +195,51 @@ public class WindowInsetsActivity extends AppCompatActivity {
getWindow().getDecorView().post(() -> getWindow().setDecorFitsSystemWindows(false));
}
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
getWindow().getInsetsController().addOnControllableInsetsChangedListener(
new OnControllableInsetsChangedListener() {
boolean hasControl = false;
@Override
public void onControllableInsetsChanged(WindowInsetsController controller,
int types) {
if ((types & ime()) != 0 && !hasControl) {
hasControl = true;
controller.controlWindowInsetsAnimation(ime(), -1,
new LinearInterpolator(),
new WindowInsetsAnimationControlListener() {
@Override
public void onReady(
WindowInsetsAnimationController controller,
int types) {
ValueAnimator anim = ValueAnimator.ofFloat(0f, 1f);
anim.setDuration(1500);
anim.addUpdateListener(animation
-> controller.setInsetsAndAlpha(
controller.getShownStateInsets(),
(float) animation.getAnimatedValue(),
anim.getAnimatedFraction()));
anim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
controller.finish(true);
}
});
anim.start();
}
@Override
public void onCancelled() {
}
});
}
}
});
}
static class Transition {
private int mEndBottom;
private int mStartBottom;
@@ -200,7 +251,7 @@ public class WindowInsetsActivity extends AppCompatActivity {
}
void onPrepare(WindowInsetsAnimation animation) {
if ((animation.getTypeMask() & Type.ime()) != 0) {
if ((animation.getTypeMask() & ime()) != 0) {
mInsetsAnimation = animation;
}
mStartBottom = mView.getBottom();