Loading core/java/android/view/ImeBackAnimationController.java +2 −2 Original line number Original line Diff line number Diff line Loading @@ -63,8 +63,8 @@ public class ImeBackAnimationController implements OnBackAnimationCallback { private boolean mIsPreCommitAnimationInProgress = false; private boolean mIsPreCommitAnimationInProgress = false; private int mStartRootScrollY = 0; private int mStartRootScrollY = 0; public ImeBackAnimationController(ViewRootImpl viewRoot) { public ImeBackAnimationController(ViewRootImpl viewRoot, InsetsController insetsController) { mInsetsController = viewRoot.getInsetsController(); mInsetsController = insetsController; mViewRoot = viewRoot; mViewRoot = viewRoot; } } Loading core/java/android/view/InsetsController.java +6 −3 Original line number Original line Diff line number Diff line Loading @@ -1028,7 +1028,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation reportRequestedVisibleTypes(); reportRequestedVisibleTypes(); } } void setPredictiveBackImeHideAnimInProgress(boolean isInProgress) { @VisibleForTesting(visibility = PACKAGE) public void setPredictiveBackImeHideAnimInProgress(boolean isInProgress) { mIsPredictiveBackImeHideAnimInProgress = isInProgress; mIsPredictiveBackImeHideAnimInProgress = isInProgress; } } Loading Loading @@ -1231,7 +1232,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation false /* fromPredictiveBack */); false /* fromPredictiveBack */); } } void controlWindowInsetsAnimation(@InsetsType int types, @VisibleForTesting(visibility = PACKAGE) public void controlWindowInsetsAnimation(@InsetsType int types, @Nullable CancellationSignal cancellationSignal, @Nullable CancellationSignal cancellationSignal, WindowInsetsAnimationControlListener listener, WindowInsetsAnimationControlListener listener, boolean fromIme, long durationMs, @Nullable Interpolator interpolator, boolean fromIme, long durationMs, @Nullable Interpolator interpolator, Loading Loading @@ -1983,7 +1985,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } } } } Host getHost() { @VisibleForTesting(visibility = PACKAGE) public Host getHost() { return mHost; return mHost; } } } } core/java/android/view/ViewRootImpl.java +8 −2 Original line number Original line Diff line number Diff line Loading @@ -1243,7 +1243,7 @@ public final class ViewRootImpl implements ViewParent, // TODO(b/222696368): remove getSfInstance usage and use vsyncId for transactions // TODO(b/222696368): remove getSfInstance usage and use vsyncId for transactions mChoreographer = Choreographer.getInstance(); mChoreographer = Choreographer.getInstance(); mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this)); mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this)); mImeBackAnimationController = new ImeBackAnimationController(this); mImeBackAnimationController = new ImeBackAnimationController(this, mInsetsController); mHandwritingInitiator = new HandwritingInitiator( mHandwritingInitiator = new HandwritingInitiator( mViewConfiguration, mViewConfiguration, mContext.getSystemService(InputMethodManager.class)); mContext.getSystemService(InputMethodManager.class)); Loading Loading @@ -5956,13 +5956,19 @@ public final class ViewRootImpl implements ViewParent, return handled; return handled; } } void setScrollY(int scrollY) { @VisibleForTesting(visibility = PACKAGE) public void setScrollY(int scrollY) { if (mScroller != null) { if (mScroller != null) { mScroller.abortAnimation(); mScroller.abortAnimation(); } } mScrollY = scrollY; mScrollY = scrollY; } } @VisibleForTesting public int getScrollY() { return mScrollY; } /** /** * @hide * @hide */ */ Loading core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java 0 → 100644 +257 −0 Original line number Original line Diff line number Diff line /* * 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(); } } core/tests/coretests/src/android/view/InsetsControllerTest.java +90 −0 Original line number Original line Diff line number Diff line Loading @@ -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_NONE; import static android.view.InsetsController.ANIMATION_TYPE_RESIZE; import static android.view.InsetsController.ANIMATION_TYPE_RESIZE; import static android.view.InsetsController.ANIMATION_TYPE_SHOW; 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.FLAG_ANIMATE_RESIZING; import static android.view.InsetsSource.ID_IME; import static android.view.InsetsSource.ID_IME; import static android.view.InsetsSourceConsumer.ShowResult.IME_SHOW_DELAYED; import static android.view.InsetsSourceConsumer.ShowResult.IME_SHOW_DELAYED; Loading Loading @@ -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 { private void waitUntilNextFrame() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1); Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT, Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT, Loading Loading
core/java/android/view/ImeBackAnimationController.java +2 −2 Original line number Original line Diff line number Diff line Loading @@ -63,8 +63,8 @@ public class ImeBackAnimationController implements OnBackAnimationCallback { private boolean mIsPreCommitAnimationInProgress = false; private boolean mIsPreCommitAnimationInProgress = false; private int mStartRootScrollY = 0; private int mStartRootScrollY = 0; public ImeBackAnimationController(ViewRootImpl viewRoot) { public ImeBackAnimationController(ViewRootImpl viewRoot, InsetsController insetsController) { mInsetsController = viewRoot.getInsetsController(); mInsetsController = insetsController; mViewRoot = viewRoot; mViewRoot = viewRoot; } } Loading
core/java/android/view/InsetsController.java +6 −3 Original line number Original line Diff line number Diff line Loading @@ -1028,7 +1028,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation reportRequestedVisibleTypes(); reportRequestedVisibleTypes(); } } void setPredictiveBackImeHideAnimInProgress(boolean isInProgress) { @VisibleForTesting(visibility = PACKAGE) public void setPredictiveBackImeHideAnimInProgress(boolean isInProgress) { mIsPredictiveBackImeHideAnimInProgress = isInProgress; mIsPredictiveBackImeHideAnimInProgress = isInProgress; } } Loading Loading @@ -1231,7 +1232,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation false /* fromPredictiveBack */); false /* fromPredictiveBack */); } } void controlWindowInsetsAnimation(@InsetsType int types, @VisibleForTesting(visibility = PACKAGE) public void controlWindowInsetsAnimation(@InsetsType int types, @Nullable CancellationSignal cancellationSignal, @Nullable CancellationSignal cancellationSignal, WindowInsetsAnimationControlListener listener, WindowInsetsAnimationControlListener listener, boolean fromIme, long durationMs, @Nullable Interpolator interpolator, boolean fromIme, long durationMs, @Nullable Interpolator interpolator, Loading Loading @@ -1983,7 +1985,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation } } } } Host getHost() { @VisibleForTesting(visibility = PACKAGE) public Host getHost() { return mHost; return mHost; } } } }
core/java/android/view/ViewRootImpl.java +8 −2 Original line number Original line Diff line number Diff line Loading @@ -1243,7 +1243,7 @@ public final class ViewRootImpl implements ViewParent, // TODO(b/222696368): remove getSfInstance usage and use vsyncId for transactions // TODO(b/222696368): remove getSfInstance usage and use vsyncId for transactions mChoreographer = Choreographer.getInstance(); mChoreographer = Choreographer.getInstance(); mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this)); mInsetsController = new InsetsController(new ViewRootInsetsControllerHost(this)); mImeBackAnimationController = new ImeBackAnimationController(this); mImeBackAnimationController = new ImeBackAnimationController(this, mInsetsController); mHandwritingInitiator = new HandwritingInitiator( mHandwritingInitiator = new HandwritingInitiator( mViewConfiguration, mViewConfiguration, mContext.getSystemService(InputMethodManager.class)); mContext.getSystemService(InputMethodManager.class)); Loading Loading @@ -5956,13 +5956,19 @@ public final class ViewRootImpl implements ViewParent, return handled; return handled; } } void setScrollY(int scrollY) { @VisibleForTesting(visibility = PACKAGE) public void setScrollY(int scrollY) { if (mScroller != null) { if (mScroller != null) { mScroller.abortAnimation(); mScroller.abortAnimation(); } } mScrollY = scrollY; mScrollY = scrollY; } } @VisibleForTesting public int getScrollY() { return mScrollY; } /** /** * @hide * @hide */ */ Loading
core/tests/coretests/src/android/view/ImeBackAnimationControllerTest.java 0 → 100644 +257 −0 Original line number Original line Diff line number Diff line /* * 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(); } }
core/tests/coretests/src/android/view/InsetsControllerTest.java +90 −0 Original line number Original line Diff line number Diff line Loading @@ -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_NONE; import static android.view.InsetsController.ANIMATION_TYPE_RESIZE; import static android.view.InsetsController.ANIMATION_TYPE_RESIZE; import static android.view.InsetsController.ANIMATION_TYPE_SHOW; 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.FLAG_ANIMATE_RESIZING; import static android.view.InsetsSource.ID_IME; import static android.view.InsetsSource.ID_IME; import static android.view.InsetsSourceConsumer.ShowResult.IME_SHOW_DELAYED; import static android.view.InsetsSourceConsumer.ShowResult.IME_SHOW_DELAYED; Loading Loading @@ -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 { private void waitUntilNextFrame() throws Exception { final CountDownLatch latch = new CountDownLatch(1); final CountDownLatch latch = new CountDownLatch(1); Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT, Choreographer.getMainThreadInstance().postCallback(Choreographer.CALLBACK_COMMIT, Loading