summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/view/ImeBackAnimationController.java4
-rw-r--r--core/java/android/view/InsetsController.java9
-rw-r--r--core/java/android/view/ViewRootImpl.java10
-rw-r--r--core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java257
-rw-r--r--core/tests/coretests/src/android/view/InsetsControllerTest.java90
-rw-r--r--tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java1
6 files changed, 364 insertions, 7 deletions
diff --git a/core/java/android/view/ImeBackAnimationController.java b/core/java/android/view/ImeBackAnimationController.java
index e14ddd6ed75b..1afedc185c85 100644
--- a/core/java/android/view/ImeBackAnimationController.java
+++ b/core/java/android/view/ImeBackAnimationController.java
@@ -63,8 +63,8 @@ public class ImeBackAnimationController implements OnBackAnimationCallback {
private boolean mIsPreCommitAnimationInProgress = false;
private int mStartRootScrollY = 0;
- public ImeBackAnimationController(ViewRootImpl viewRoot) {
- mInsetsController = viewRoot.getInsetsController();
+ public ImeBackAnimationController(ViewRootImpl viewRoot, InsetsController insetsController) {
+ mInsetsController = insetsController;
mViewRoot = viewRoot;
}
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 6c9001163459..edf850cdff78 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -1028,7 +1028,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
reportRequestedVisibleTypes();
}
- void setPredictiveBackImeHideAnimInProgress(boolean isInProgress) {
+ @VisibleForTesting(visibility = PACKAGE)
+ public void setPredictiveBackImeHideAnimInProgress(boolean isInProgress) {
mIsPredictiveBackImeHideAnimInProgress = isInProgress;
}
@@ -1231,7 +1232,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
false /* fromPredictiveBack */);
}
- void controlWindowInsetsAnimation(@InsetsType int types,
+ @VisibleForTesting(visibility = PACKAGE)
+ public void controlWindowInsetsAnimation(@InsetsType int types,
@Nullable CancellationSignal cancellationSignal,
WindowInsetsAnimationControlListener listener,
boolean fromIme, long durationMs, @Nullable Interpolator interpolator,
@@ -1983,7 +1985,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
}
}
- Host getHost() {
+ @VisibleForTesting(visibility = PACKAGE)
+ public Host getHost() {
return mHost;
}
}
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 00840cc9d047..a41b938829c5 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -1205,7 +1205,7 @@ public final class ViewRootImpl implements ViewParent,
// TODO(b/222696368): remove getSfInstance usage and use vsyncId for transactions
mChoreographer = Choreographer.getInstance();
mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this));
- mImeBackAnimationController = new ImeBackAnimationController(this);
+ mImeBackAnimationController = new ImeBackAnimationController(this, mInsetsController);
mHandwritingInitiator = new HandwritingInitiator(
mViewConfiguration,
mContext.getSystemService(InputMethodManager.class));
@@ -5881,13 +5881,19 @@ public final class ViewRootImpl implements ViewParent,
return handled;
}
- void setScrollY(int scrollY) {
+ @VisibleForTesting(visibility = PACKAGE)
+ public void setScrollY(int scrollY) {
if (mScroller != null) {
mScroller.abortAnimation();
}
mScrollY = scrollY;
}
+ @VisibleForTesting
+ public int getScrollY() {
+ return mScrollY;
+ }
+
/**
* @hide
*/
diff --git a/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java b/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java
new file mode 100644
index 000000000000..c00ebe487620
--- /dev/null
+++ b/core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view;
+
+import static android.view.WindowInsets.Type.ime;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN;
+import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
+import static android.window.BackEvent.EDGE_LEFT;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyFloat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+import android.content.Context;
+import android.graphics.Insets;
+import android.platform.test.annotations.Presubmit;
+import android.view.animation.BackGestureInterpolator;
+import android.view.animation.Interpolator;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.TextView;
+import android.window.BackEvent;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for {@link ImeBackAnimationController}.
+ *
+ * <p>Build/Install/Run:
+ * atest FrameworksCoreTests:ImeBackAnimationControllerTest
+ *
+ * <p>This test class is a part of Window Manager Service tests and specified in
+ * {@link com.android.server.wm.test.filters.FrameworksTestsFilter}.
+ */
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class ImeBackAnimationControllerTest {
+
+ private static final float PEEK_FRACTION = 0.1f;
+ private static final Interpolator BACK_GESTURE = new BackGestureInterpolator();
+ private static final int IME_HEIGHT = 200;
+ private static final Insets IME_INSETS = Insets.of(0, 0, 0, IME_HEIGHT);
+
+ @Mock
+ private InsetsController mInsetsController;
+ @Mock
+ private WindowInsetsAnimationController mWindowInsetsAnimationController;
+ @Mock
+ private ViewRootInsetsControllerHost mViewRootInsetsControllerHost;
+
+ private ViewRootImpl mViewRoot;
+ private ImeBackAnimationController mBackAnimationController;
+
+ @Before
+ public void setup() {
+ MockitoAnnotations.initMocks(this);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
+ InputMethodManager inputMethodManager = context.getSystemService(
+ InputMethodManager.class);
+ // cannot mock ViewRootImpl since it's final.
+ mViewRoot = new ViewRootImpl(context, context.getDisplayNoVerify());
+ try {
+ mViewRoot.setView(new TextView(context), new WindowManager.LayoutParams(), null);
+ } catch (WindowManager.BadTokenException e) {
+ // activity isn't running, we will ignore BadTokenException.
+ }
+ mBackAnimationController = new ImeBackAnimationController(mViewRoot, mInsetsController);
+
+ when(mWindowInsetsAnimationController.getHiddenStateInsets()).thenReturn(Insets.NONE);
+ when(mWindowInsetsAnimationController.getShownStateInsets()).thenReturn(IME_INSETS);
+ when(mWindowInsetsAnimationController.getCurrentInsets()).thenReturn(IME_INSETS);
+ when(mInsetsController.getHost()).thenReturn(mViewRootInsetsControllerHost);
+ when(mViewRootInsetsControllerHost.getInputMethodManager()).thenReturn(
+ inputMethodManager);
+ });
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ @Test
+ public void testAdjustResizeWithAppWindowInsetsListenerPlaysAnim() {
+ // setup ViewRoot with InsetsAnimationCallback and softInputMode=adjustResize
+ mViewRoot.getView()
+ .setWindowInsetsAnimationCallback(mock(WindowInsetsAnimation.Callback.class));
+ mViewRoot.mWindowAttributes.softInputMode = SOFT_INPUT_ADJUST_RESIZE;
+ // start back gesture
+ mBackAnimationController.onBackStarted(new BackEvent(0f, 0f, 0f, EDGE_LEFT));
+ // verify that ImeBackAnimationController takes control over IME insets
+ verify(mInsetsController, times(1)).controlWindowInsetsAnimation(anyInt(), any(), any(),
+ anyBoolean(), anyLong(), any(), anyInt(), anyBoolean());
+ }
+
+ @Test
+ public void testAdjustResizeWithoutAppWindowInsetsListenerNotPlayingAnim() {
+ // setup ViewRoot with softInputMode=adjustResize
+ mViewRoot.mWindowAttributes.softInputMode = SOFT_INPUT_ADJUST_RESIZE;
+ // start back gesture
+ mBackAnimationController.onBackStarted(new BackEvent(0f, 0f, 0f, EDGE_LEFT));
+ // progress back gesture
+ mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, 0.5f, EDGE_LEFT));
+ // commit back gesture
+ mBackAnimationController.onBackInvoked();
+ // verify that InsetsController#hide is called
+ verify(mInsetsController, times(1)).hide(ime());
+ // verify that ImeBackAnimationController does not take control over IME insets
+ verify(mInsetsController, never()).controlWindowInsetsAnimation(anyInt(), any(), any(),
+ anyBoolean(), anyLong(), any(), anyInt(), anyBoolean());
+ }
+
+ @Test
+ public void testAdjustPanScrollsViewRoot() {
+ // simulate view root being panned upwards by 50px
+ int appPan = -50;
+ mViewRoot.setScrollY(appPan);
+ // setup ViewRoot with softInputMode=adjustPan
+ mViewRoot.mWindowAttributes.softInputMode = SOFT_INPUT_ADJUST_PAN;
+
+ // start back gesture
+ WindowInsetsAnimationControlListener animationControlListener = startBackGesture();
+ // simulate ImeBackAnimationController receiving control
+ animationControlListener.onReady(mWindowInsetsAnimationController, ime());
+
+ // progress back gesture
+ float progress = 0.5f;
+ mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, progress, EDGE_LEFT));
+
+ // verify that view root is scrolled by expected amount
+ float interpolatedProgress = BACK_GESTURE.getInterpolation(progress);
+ int expectedViewRootScroll =
+ (int) (appPan * (1 - interpolatedProgress * PEEK_FRACTION));
+ assertEquals(mViewRoot.getScrollY(), expectedViewRootScroll);
+ }
+
+ @Test
+ public void testNewGestureAfterCancelSeamlessTakeover() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ // start back gesture
+ WindowInsetsAnimationControlListener animationControlListener = startBackGesture();
+ // simulate ImeBackAnimationController receiving control
+ animationControlListener.onReady(mWindowInsetsAnimationController, ime());
+ // verify initial animation insets are set
+ verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha(
+ eq(Insets.of(0, 0, 0, IME_HEIGHT)), eq(1f), anyFloat());
+
+ // progress back gesture
+ mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, 0.5f, EDGE_LEFT));
+
+ // cancel back gesture
+ mBackAnimationController.onBackCancelled();
+ // verify that InsetsController does not notified of a hide-anim (because the gesture
+ // was cancelled)
+ verify(mInsetsController, never()).setPredictiveBackImeHideAnimInProgress(eq(true));
+
+ Mockito.clearInvocations(mWindowInsetsAnimationController);
+ // restart back gesture
+ mBackAnimationController.onBackStarted(new BackEvent(0f, 0f, 0f, EDGE_LEFT));
+ // verify that animation controller is reused and initial insets are set immediately
+ verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha(
+ eq(Insets.of(0, 0, 0, IME_HEIGHT)), eq(1f), anyFloat());
+ });
+ }
+
+ @Test
+ public void testImeInsetsManipulationCurve() {
+ // start back gesture
+ WindowInsetsAnimationControlListener animationControlListener = startBackGesture();
+ // simulate ImeBackAnimationController receiving control
+ animationControlListener.onReady(mWindowInsetsAnimationController, ime());
+ // verify initial animation insets are set
+ verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha(
+ eq(Insets.of(0, 0, 0, IME_HEIGHT)), eq(1f), anyFloat());
+
+ Mockito.clearInvocations(mWindowInsetsAnimationController);
+ // progress back gesture
+ float progress = 0.5f;
+ mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, progress, EDGE_LEFT));
+ // verify correct ime insets manipulation
+ float interpolatedProgress = BACK_GESTURE.getInterpolation(progress);
+ int expectedInset =
+ (int) (IME_HEIGHT - interpolatedProgress * PEEK_FRACTION * IME_HEIGHT);
+ verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha(
+ eq(Insets.of(0, 0, 0, expectedInset)), eq(1f), anyFloat());
+ }
+
+ @Test
+ public void testOnReadyAfterGestureFinished() {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ // start back gesture
+ WindowInsetsAnimationControlListener animationControlListener = startBackGesture();
+
+ // progress back gesture
+ mBackAnimationController.onBackProgressed(new BackEvent(100f, 0f, 0.5f, EDGE_LEFT));
+
+ // commit back gesture
+ mBackAnimationController.onBackInvoked();
+
+ // verify setInsetsAndAlpha never called due onReady delayed
+ verify(mWindowInsetsAnimationController, never()).setInsetsAndAlpha(any(), anyInt(),
+ anyFloat());
+ verify(mInsetsController, never()).setPredictiveBackImeHideAnimInProgress(eq(true));
+
+ // simulate ImeBackAnimationController receiving control
+ animationControlListener.onReady(mWindowInsetsAnimationController, ime());
+
+ // verify setInsetsAndAlpha immediately called
+ verify(mWindowInsetsAnimationController, times(1)).setInsetsAndAlpha(
+ eq(Insets.of(0, 0, 0, IME_HEIGHT)), eq(1f), anyFloat());
+ // verify post-commit hide anim has started
+ verify(mInsetsController, times(1)).setPredictiveBackImeHideAnimInProgress(eq(true));
+ });
+ }
+
+ private WindowInsetsAnimationControlListener startBackGesture() {
+ // start back gesture
+ mBackAnimationController.onBackStarted(new BackEvent(0f, 0f, 0f, EDGE_LEFT));
+
+ // verify controlWindowInsetsAnimation is called and capture animationControlListener
+ ArgumentCaptor<WindowInsetsAnimationControlListener> animationControlListener =
+ ArgumentCaptor.forClass(WindowInsetsAnimationControlListener.class);
+ verify(mInsetsController, times(1)).controlWindowInsetsAnimation(anyInt(), any(),
+ animationControlListener.capture(), anyBoolean(), anyLong(), any(), anyInt(),
+ anyBoolean());
+
+ return animationControlListener.getValue();
+ }
+}
diff --git a/core/tests/coretests/src/android/view/InsetsControllerTest.java b/core/tests/coretests/src/android/view/InsetsControllerTest.java
index 4fb85c1fa7ff..f05390dff5cd 100644
--- a/core/tests/coretests/src/android/view/InsetsControllerTest.java
+++ b/core/tests/coretests/src/android/view/InsetsControllerTest.java
@@ -21,6 +21,7 @@ import static android.view.InsetsController.ANIMATION_TYPE_HIDE;
import static android.view.InsetsController.ANIMATION_TYPE_NONE;
import static android.view.InsetsController.ANIMATION_TYPE_RESIZE;
import static android.view.InsetsController.ANIMATION_TYPE_SHOW;
+import static android.view.InsetsController.ANIMATION_TYPE_USER;
import static android.view.InsetsSource.FLAG_ANIMATE_RESIZING;
import static android.view.InsetsSource.ID_IME;
import static android.view.InsetsSourceConsumer.ShowResult.IME_SHOW_DELAYED;
@@ -925,6 +926,95 @@ public class InsetsControllerTest {
});
}
+ @Test
+ public void testImeRequestedVisibleDuringPredictiveBackAnim() {
+ prepareControls();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ // show ime as initial state
+ mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty());
+ mController.cancelExistingAnimations(); // fast forward show animation
+ assertTrue(mController.getState().peekSource(ID_IME).isVisible());
+
+ // start control request (for predictive back animation)
+ WindowInsetsAnimationControlListener listener =
+ mock(WindowInsetsAnimationControlListener.class);
+ mController.controlWindowInsetsAnimation(ime(), /*cancellationSignal*/ null,
+ listener, /*fromIme*/ false, /*duration*/ -1, /*interpolator*/ null,
+ ANIMATION_TYPE_USER, /*fromPredictiveBack*/ true);
+
+ // Verify that onReady is called (after next predraw)
+ mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw();
+ verify(listener).onReady(notNull(), eq(ime()));
+
+ // verify that insets are requested visible during animation
+ assertTrue(isRequestedVisible(mController, ime()));
+ });
+ }
+
+ @Test
+ public void testImeShowRequestCancelsPredictiveBackPostCommitAnim() {
+ prepareControls();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ // show ime as initial state
+ mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty());
+ mController.cancelExistingAnimations(); // fast forward show animation
+ mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw();
+ assertTrue(mController.getState().peekSource(ID_IME).isVisible());
+
+ // start control request (for predictive back animation)
+ WindowInsetsAnimationControlListener listener =
+ mock(WindowInsetsAnimationControlListener.class);
+ mController.controlWindowInsetsAnimation(ime(), /*cancellationSignal*/ null,
+ listener, /*fromIme*/ false, /*duration*/ -1, /*interpolator*/ null,
+ ANIMATION_TYPE_USER, /*fromPredictiveBack*/ true);
+
+ // verify that controller
+ // has ANIMATION_TYPE_USER set for ime()
+ assertEquals(ANIMATION_TYPE_USER, mController.getAnimationType(ime()));
+
+ // verify show request is ignored during pre commit phase of predictive back anim
+ mController.show(ime(), true /* fromIme */, null /* statsToken */);
+ assertEquals(ANIMATION_TYPE_USER, mController.getAnimationType(ime()));
+
+ // verify show request is applied during post commit phase of predictive back anim
+ mController.setPredictiveBackImeHideAnimInProgress(true);
+ mController.show(ime(), true /* fromIme */, null /* statsToken */);
+ assertEquals(ANIMATION_TYPE_SHOW, mController.getAnimationType(ime()));
+
+ // additionally verify that IME ends up visible
+ mController.cancelExistingAnimations();
+ assertTrue(mController.getState().peekSource(ID_IME).isVisible());
+ });
+ }
+
+ @Test
+ public void testImeHideRequestIgnoredDuringPredictiveBackPostCommitAnim() {
+ prepareControls();
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+ // show ime as initial state
+ mController.show(ime(), true /* fromIme */, ImeTracker.Token.empty());
+ mController.cancelExistingAnimations(); // fast forward show animation
+ mViewRoot.getView().getViewTreeObserver().dispatchOnPreDraw();
+ assertTrue(mController.getState().peekSource(ID_IME).isVisible());
+
+ // start control request (for predictive back animation)
+ WindowInsetsAnimationControlListener listener =
+ mock(WindowInsetsAnimationControlListener.class);
+ mController.controlWindowInsetsAnimation(ime(), /*cancellationSignal*/ null,
+ listener, /*fromIme*/ false, /*duration*/ -1, /*interpolator*/ null,
+ ANIMATION_TYPE_USER, /*fromPredictiveBack*/ true);
+
+ // verify that controller has ANIMATION_TYPE_USER set for ime()
+ assertEquals(ANIMATION_TYPE_USER, mController.getAnimationType(ime()));
+
+ // verify hide request is ignored during post commit phase of predictive back anim
+ // since IME is already animating away
+ mController.setPredictiveBackImeHideAnimInProgress(true);
+ mController.hide(ime(), true /* fromIme */, null /* statsToken */);
+ assertEquals(ANIMATION_TYPE_USER, mController.getAnimationType(ime()));
+ });
+ }
+
private void waitUntilNextFrame() throws Exception {
final CountDownLatch latch = new CountDownLatch(1);
Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT,
diff --git a/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java b/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java
index 7c5dcf8b95f7..e8be33cba3a1 100644
--- a/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java
+++ b/tests/utils/testutils/java/com/android/server/wm/test/filters/FrameworksTestsFilter.java
@@ -51,6 +51,7 @@ public final class FrameworksTestsFilter extends SelectTest {
"android.view.CutoutSpecificationTest",
"android.view.DisplayCutoutTest",
"android.view.DisplayShapeTest",
+ "android.view.ImeBackAnimationControllerTest",
"android.view.InsetsAnimationControlImplTest",
"android.view.InsetsControllerTest",
"android.view.InsetsFlagsTest",