Implement proper lifecycle for WindowInsetsAnimationController
- Ensure that finish actually works.
- Ensure that cancelling also works.
- Ensure that if the app is losing control, the cancel callback
will be invoked.
Test: InsetsControllerTest
Test: InsetsAnimationControlImplTest
Bug: 111084606
Change-Id: I74d15fe2aa66c054b104c81795e7ab67336c9d04
diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java
index dd88e3c..50ef91f 100644
--- a/core/java/android/view/InsetsAnimationControlImpl.java
+++ b/core/java/android/view/InsetsAnimationControlImpl.java
@@ -20,6 +20,7 @@
import static android.view.InsetsState.INSET_SIDE_LEFT;
import static android.view.InsetsState.INSET_SIDE_RIGHT;
import static android.view.InsetsState.INSET_SIDE_TOP;
+import static android.view.InsetsState.toPublicType;
import android.annotation.Nullable;
import android.graphics.Insets;
@@ -33,6 +34,7 @@
import android.view.InsetsState.InsetSide;
import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams;
import android.view.WindowInsets.Type.InsetType;
+import android.view.WindowInsetsAnimationListener.InsetsAnimation;
import android.view.WindowManager.LayoutParams;
import com.android.internal.annotations.VisibleForTesting;
@@ -66,8 +68,12 @@
private final Supplier<SyncRtSurfaceTransactionApplier> mTransactionApplierSupplier;
private final InsetsController mController;
private final WindowInsetsAnimationListener.InsetsAnimation mAnimation;
+ private final Rect mFrame;
private Insets mCurrentInsets;
private Insets mPendingInsets;
+ private boolean mFinished;
+ private boolean mCancelled;
+ private int mFinishedShownTypes;
@VisibleForTesting
public InsetsAnimationControlImpl(SparseArray<InsetsSourceConsumer> consumers, Rect frame,
@@ -86,6 +92,7 @@
null /* typeSideMap */);
mShownInsets = calculateInsets(mInitialInsetsState, frame, consumers, true /* shown */,
mTypeSideMap);
+ mFrame = new Rect(frame);
buildTypeSourcesMap(mTypeSideMap, mSideSourceMap, mConsumers);
// TODO: Check for controllability first and wait for IME if needed.
@@ -119,12 +126,26 @@
@Override
public void changeInsets(Insets insets) {
+ if (mFinished) {
+ throw new IllegalStateException(
+ "Can't change insets on an animation that is finished.");
+ }
+ if (mCancelled) {
+ throw new IllegalStateException(
+ "Can't change insets on an animation that is cancelled.");
+ }
mPendingInsets = sanitize(insets);
mController.scheduleApplyChangeInsets();
}
@VisibleForTesting
- public void applyChangeInsets(InsetsState state) {
+ /**
+ * @return Whether the finish callback of this animation should be invoked.
+ */
+ public boolean applyChangeInsets(InsetsState state) {
+ if (mCancelled) {
+ return false;
+ }
final Insets offset = Insets.subtract(mShownInsets, mPendingInsets);
ArrayList<SurfaceParams> params = new ArrayList<>();
if (offset.left != 0) {
@@ -144,13 +165,40 @@
SyncRtSurfaceTransactionApplier applier = mTransactionApplierSupplier.get();
applier.scheduleApply(params.toArray(new SurfaceParams[params.size()]));
mCurrentInsets = mPendingInsets;
+ if (mFinished) {
+ mController.notifyFinished(this, mFinishedShownTypes);
+ }
+ return mFinished;
}
@Override
public void finish(int shownTypes) {
- // TODO
+ if (mCancelled) {
+ return;
+ }
+ InsetsState state = new InsetsState(mController.getState());
+ for (int i = mConsumers.size() - 1; i >= 0; i--) {
+ InsetsSourceConsumer consumer = mConsumers.valueAt(i);
+ boolean visible = (shownTypes & toPublicType(consumer.getType())) != 0;
+ state.getSource(consumer.getType()).setVisible(visible);
+ }
+ Insets insets = getInsetsFromState(state, mFrame, null /* typeSideMap */);
+ changeInsets(insets);
+ mFinished = true;
+ mFinishedShownTypes = shownTypes;
+ }
- mController.dispatchAnimationFinished(mAnimation);
+ @VisibleForTesting
+ public void onCancelled() {
+ if (mFinished) {
+ return;
+ }
+ mCancelled = true;
+ mListener.onCancelled();
+ }
+
+ InsetsAnimation getAnimation() {
+ return mAnimation;
}
private Insets calculateInsets(InsetsState state, Rect frame,
@@ -225,4 +273,3 @@
}
}
}
-
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 2586000..08f2e8d 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -17,6 +17,8 @@
package android.view;
import static android.view.InsetsState.TYPE_IME;
+import static android.view.InsetsState.toPublicType;
+import static android.view.WindowInsets.Type.all;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -99,6 +101,7 @@
private final SparseArray<InsetsSourceControl> mTmpControlArray = new SparseArray<>();
private final ArrayList<InsetsAnimationControlImpl> mAnimationControls = new ArrayList<>();
+ private final ArrayList<InsetsAnimationControlImpl> mTmpFinishedControls = new ArrayList<>();
private WindowInsets mLastInsets;
private boolean mAnimCallbackScheduled;
@@ -107,7 +110,6 @@
private final Rect mLastLegacyContentInsets = new Rect();
private final Rect mLastLegacyStableInsets = new Rect();
- private ObjectAnimator mAnimator;
private @AnimationDirection int mAnimationDirection;
private int mPendingTypesToShow;
@@ -122,19 +124,29 @@
return;
}
+ mTmpFinishedControls.clear();
InsetsState state = new InsetsState(mState, true /* copySources */);
for (int i = mAnimationControls.size() - 1; i >= 0; i--) {
- mAnimationControls.get(i).applyChangeInsets(state);
+ InsetsAnimationControlImpl control = mAnimationControls.get(i);
+ if (mAnimationControls.get(i).applyChangeInsets(state)) {
+ mTmpFinishedControls.add(control);
+ }
}
+
WindowInsets insets = state.calculateInsets(mFrame, mLastInsets.isRound(),
mLastInsets.shouldAlwaysConsumeNavBar(), mLastInsets.getDisplayCutout(),
mLastLegacyContentInsets, mLastLegacyStableInsets, mLastLegacySoftInputMode,
null /* typeSideMap */);
mViewRoot.mView.dispatchWindowInsetsAnimationProgress(insets);
+
+ for (int i = mTmpFinishedControls.size() - 1; i >= 0; i--) {
+ dispatchAnimationFinished(mTmpFinishedControls.get(i).getAnimation());
+ }
};
}
- void onFrameChanged(Rect frame) {
+ @VisibleForTesting
+ public void onFrameChanged(Rect frame) {
if (mFrame.equals(frame)) {
return;
}
@@ -279,7 +291,8 @@
// nothing to animate.
return;
}
- // TODO: Check whether we already have a controller.
+ cancelExistingControllers(types);
+
final ArraySet<Integer> internalTypes = mState.toInternalType(types);
final SparseArray<InsetsSourceConsumer> consumers = new SparseArray<>();
@@ -321,7 +334,7 @@
// Show request
switch(consumer.requestShow(fromIme)) {
case ShowResult.SHOW_IMMEDIATELY:
- typesReady |= InsetsState.toPublicType(TYPE_IME);
+ typesReady |= InsetsState.toPublicType(consumer.getType());
break;
case ShowResult.SHOW_DELAYED:
isReady = false;
@@ -365,6 +378,36 @@
return typesReady;
}
+ private void cancelExistingControllers(@InsetType int types) {
+ for (int i = mAnimationControls.size() - 1; i >= 0; i--) {
+ InsetsAnimationControlImpl control = mAnimationControls.get(i);
+ if ((control.getTypes() & types) != 0) {
+ cancelAnimation(control);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public void notifyFinished(InsetsAnimationControlImpl controller, int shownTypes) {
+ mAnimationControls.remove(controller);
+ hideDirectly(controller.getTypes() & ~shownTypes);
+ showDirectly(controller.getTypes() & shownTypes);
+ }
+
+ void notifyControlRevoked(InsetsSourceConsumer consumer) {
+ for (int i = mAnimationControls.size() - 1; i >= 0; i--) {
+ InsetsAnimationControlImpl control = mAnimationControls.get(i);
+ if ((control.getTypes() & toPublicType(consumer.getType())) != 0) {
+ cancelAnimation(control);
+ }
+ }
+ }
+
+ private void cancelAnimation(InsetsAnimationControlImpl control) {
+ control.onCancelled();
+ mAnimationControls.remove(control);
+ }
+
private void applyLocalVisibilityOverride() {
for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
final InsetsSourceConsumer controller = mSourceConsumers.valueAt(i);
@@ -455,8 +498,13 @@
}
WindowInsetsAnimationControlListener listener = new WindowInsetsAnimationControlListener() {
+
+ private WindowInsetsAnimationController mController;
+ private ObjectAnimator mAnimator;
+
@Override
public void onReady(WindowInsetsAnimationController controller, int types) {
+ mController = controller;
if (show) {
showDirectly(types);
} else {
@@ -474,10 +522,6 @@
: ANIMATION_DURATION_HIDE_MS);
mAnimator.setInterpolator(INTERPOLATOR);
mAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationCancel(Animator animation) {
- onAnimationFinish();
- }
@Override
public void onAnimationEnd(Animator animation) {
@@ -488,15 +532,15 @@
}
@Override
- public void onCancelled() {}
+ public void onCancelled() {
+ mAnimator.cancel();
+ }
private void onAnimationFinish() {
mAnimationDirection = DIRECTION_NONE;
+ mController.finish(show ? types : 0);
}
};
- // TODO: Instead of clearing this here, properly wire up
- // InsetsAnimationControlImpl.finish() to remove this from mAnimationControls.
- mAnimationControls.clear();
// Show/hide animations always need to be relative to the display frame, in order that shown
// and hidden state insets are correct.
@@ -522,10 +566,7 @@
*/
@VisibleForTesting
public void cancelExistingAnimation() {
- mAnimationDirection = DIRECTION_NONE;
- if (mAnimator != null) {
- mAnimator.cancel();
- }
+ cancelExistingControllers(all());
}
void dump(String prefix, PrintWriter pw) {
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index eab83ce..1383463 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -77,6 +77,9 @@
if (applyLocalVisibilityOverride()) {
mController.notifyVisibilityChanged();
}
+ if (mSourceControl == null) {
+ mController.notifyControlRevoked(this);
+ }
}
@VisibleForTesting
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index a78f2b0..4e3fc82 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -10956,7 +10956,7 @@
}
void dispatchWindowInsetsAnimationFinished(InsetsAnimation animation) {
- if (mListenerInfo != null && mListenerInfo.mOnApplyWindowInsetsListener != null) {
+ if (mListenerInfo != null && mListenerInfo.mWindowInsetsAnimationListener != null) {
mListenerInfo.mWindowInsetsAnimationListener.onFinished(animation);
}
}
diff --git a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
index 71ce02d..23ab05e 100644
--- a/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
+++ b/core/tests/coretests/src/android/view/InsetsAnimationControlImplTest.java
@@ -129,6 +129,29 @@
assertPosition(navParams.matrix, new Rect(400, 0, 500, 500), new Rect(460, 0, 560, 500));
}
+ @Test
+ public void testFinishing() {
+ when(mMockController.getState()).thenReturn(mInsetsState);
+ mController.finish(sideBars());
+ mController.applyChangeInsets(mInsetsState);
+ assertFalse(mInsetsState.getSource(TYPE_TOP_BAR).isVisible());
+ assertTrue(mInsetsState.getSource(TYPE_NAVIGATION_BAR).isVisible());
+ assertEquals(Insets.of(0, 0, 100, 0), mController.getCurrentInsets());
+ verify(mMockController).notifyFinished(eq(mController), eq(sideBars()));
+ }
+
+ @Test
+ public void testCancelled() {
+ mController.onCancelled();
+ try {
+ mController.changeInsets(Insets.NONE);
+ fail("Expected exception to be thrown");
+ } catch (IllegalStateException ignored) {
+ }
+ verify(mMockListener).onCancelled();
+ mController.finish(sideBars());
+ }
+
private void assertPosition(Matrix m, Rect original, Rect transformed) {
RectF rect = new RectF(original);
rect.offsetTo(0, 0);
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index 731d564..d71bde83 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -20,6 +20,7 @@
import static android.view.InsetsState.TYPE_NAVIGATION_BAR;
import static android.view.InsetsState.TYPE_TOP_BAR;
+import static android.view.WindowInsets.Type.topBar;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertFalse;
@@ -48,6 +49,9 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+
+import java.util.concurrent.CountDownLatch;
@Presubmit
@FlakyTest(detail = "Promote once confirmed non-flaky")
@@ -80,6 +84,8 @@
new DisplayCutout(
Insets.of(10, 10, 10, 10), rect, rect, rect, rect),
rect, rect, SOFT_INPUT_ADJUST_RESIZE);
+ mController.onFrameChanged(new Rect(0, 0, 100, 100));
+ mController.getState().setDisplayFrame(new Rect(0, 0, 100, 100));
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
@@ -101,6 +107,19 @@
}
@Test
+ public void testControlsRevoked_duringAnim() {
+ InsetsSourceControl control = new InsetsSourceControl(TYPE_TOP_BAR, mLeash, new Point());
+ mController.onControlsChanged(new InsetsSourceControl[] { control });
+
+ WindowInsetsAnimationControlListener mockListener =
+ mock(WindowInsetsAnimationControlListener.class);
+ mController.controlWindowInsetsAnimation(topBar(), mockListener);
+ verify(mockListener).onReady(any(), anyInt());
+ mController.onControlsChanged(new InsetsSourceControl[0]);
+ verify(mockListener).onCancelled();
+ }
+
+ @Test
public void testFrameDoesntMatchDisplay() {
mController.onFrameChanged(new Rect(0, 0, 100, 100));
mController.getState().setDisplayFrame(new Rect(0, 0, 200, 200));
@@ -119,24 +138,21 @@
InsetsSourceControl ime = controls[2];
InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ // since there is no focused view, forcefully make IME visible.
+ mController.applyImeVisibility(true /* setVisible */);
mController.show(Type.all());
// quickly jump to final state by cancelling it.
mController.cancelExistingAnimation();
assertTrue(mController.getSourceConsumer(navBar.getType()).isVisible());
assertTrue(mController.getSourceConsumer(topBar.getType()).isVisible());
- // no focused view, no IME.
- assertFalse(mController.getSourceConsumer(ime.getType()).isVisible());
+ assertTrue(mController.getSourceConsumer(ime.getType()).isVisible());
+ mController.applyImeVisibility(false /* setVisible */);
mController.hide(Type.all());
mController.cancelExistingAnimation();
assertFalse(mController.getSourceConsumer(navBar.getType()).isVisible());
assertFalse(mController.getSourceConsumer(topBar.getType()).isVisible());
assertFalse(mController.getSourceConsumer(ime.getType()).isVisible());
-
- mController.show(Type.ime());
- mController.cancelExistingAnimation();
- // no focused view, no IME.
- assertFalse(mController.getSourceConsumer(ime.getType()).isVisible());
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
@@ -292,6 +308,35 @@
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
+ @Test
+ public void testAnimationEndState_controller() throws Exception {
+ InsetsSourceControl control = new InsetsSourceControl(TYPE_TOP_BAR, mLeash, new Point());
+ mController.onControlsChanged(new InsetsSourceControl[] { control });
+
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ WindowInsetsAnimationControlListener mockListener =
+ mock(WindowInsetsAnimationControlListener.class);
+ mController.controlWindowInsetsAnimation(topBar(), mockListener);
+
+ ArgumentCaptor<WindowInsetsAnimationController> controllerCaptor =
+ ArgumentCaptor.forClass(WindowInsetsAnimationController.class);
+ verify(mockListener).onReady(controllerCaptor.capture(), anyInt());
+ controllerCaptor.getValue().finish(0 /* shownTypes */);
+ });
+ waitUntilNextFrame();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ assertFalse(mController.getSourceConsumer(TYPE_TOP_BAR).isVisible());
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ private void waitUntilNextFrame() throws Exception {
+ final CountDownLatch latch = new CountDownLatch(1);
+ Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT,
+ latch::countDown, null /* token */);
+ latch.await();
+ }
+
private InsetsSourceControl[] prepareControls() {
final InsetsSourceControl navBar = new InsetsSourceControl(TYPE_NAVIGATION_BAR, mLeash,
new Point());