Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit cf8688f4 authored by Johannes Gallmann's avatar Johannes Gallmann Committed by Android (Google) Code Review
Browse files

Merge "Add tests for IME predictive back animation" into main

parents c929dc6e 73415720
Loading
Loading
Loading
Loading
+2 −2
Original line number Original line Diff line number Diff line
@@ -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;
    }
    }


+6 −3
Original line number Original line Diff line number Diff line
@@ -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;
    }
    }


@@ -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,
@@ -1983,7 +1985,8 @@ public class InsetsController implements WindowInsetsController, InsetsAnimation
        }
        }
    }
    }


    Host getHost() {
    @VisibleForTesting(visibility = PACKAGE)
    public Host getHost() {
        return mHost;
        return mHost;
    }
    }
}
}
+8 −2
Original line number Original line Diff line number Diff line
@@ -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));
@@ -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
     */
     */
+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();
    }
}
+90 −0
Original line number Original line Diff line number Diff line
@@ -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;
@@ -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