diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index d08c573736d14967eb7191d194cacbbc8f406cb8..cb5d1c42c7d295938ffb898fe8469593457a9b83 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -83,11 +83,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont public static final boolean IS_ENABLED = SystemProperties.getInt("persist.wm.debug.predictive_back", SETTING_VALUE_ON) == SETTING_VALUE_ON; - /** Flag for U animation features */ - public static boolean IS_U_ANIMATION_ENABLED = - SystemProperties.getInt("persist.wm.debug.predictive_back_anim", - SETTING_VALUE_ON) == SETTING_VALUE_ON; - public static final float FLING_MAX_LENGTH_SECONDS = 0.1f; // 100ms public static final float FLING_SPEED_UP_FACTOR = 0.6f; @@ -110,10 +105,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont /** Tracks if an uninterruptible animation is in progress */ private boolean mPostCommitAnimationInProgress = false; + /** Tracks if we should start the back gesture on the next motion move event */ private boolean mShouldStartOnNextMoveEvent = false; - /** @see #setTriggerBack(boolean) */ - private boolean mTriggerBack; private final FlingAnimationUtils mFlingAnimationUtils; @@ -128,6 +122,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont private final ShellController mShellController; private final ShellExecutor mShellExecutor; private final Handler mBgHandler; + + /** + * Tracks the current user back gesture. + */ + private TouchTracker mCurrentTracker = new TouchTracker(); + + /** + * Tracks the next back gesture in case a new user gesture has started while the back animation + * (and navigation) associated with {@link #mCurrentTracker} have not yet finished. + */ + private TouchTracker mQueuedTracker = new TouchTracker(); + private final Runnable mAnimationTimeoutRunnable = () -> { ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...", MAX_ANIMATION_DURATION); @@ -138,8 +144,6 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @VisibleForTesting BackAnimationAdapter mBackAnimationAdapter; - private final TouchTracker mTouchTracker = new TouchTracker(); - @Nullable private IOnBackInvokedCallback mActiveCallback; @@ -156,7 +160,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Navigation window gone."); setTriggerBack(false); - onGestureFinished(false); + resetTouchTracker(); }); } }); @@ -357,6 +361,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellBackAnimationRegistry.unregisterAnimation(type); } + private TouchTracker getActiveTracker() { + if (mCurrentTracker.isActive()) return mCurrentTracker; + if (mQueuedTracker.isActive()) return mQueuedTracker; + return null; + } + /** * Called when a new motion event needs to be transferred to this * {@link BackAnimationController} @@ -368,11 +378,18 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont float velocityY, int keyAction, @BackEvent.SwipeEdge int swipeEdge) { - if (mPostCommitAnimationInProgress) { + + TouchTracker activeTouchTracker = getActiveTracker(); + if (activeTouchTracker != null) { + activeTouchTracker.update(touchX, touchY, velocityX, velocityY); + } + + // two gestures are waiting to be processed at the moment, skip any further user touches + if (mCurrentTracker.isFinished() && mQueuedTracker.isFinished()) { + Log.d(TAG, "Ignoring MotionEvent because two gestures are already being queued."); return; } - mTouchTracker.update(touchX, touchY, velocityX, velocityY); if (keyAction == MotionEvent.ACTION_DOWN) { if (!mBackGestureStarted) { mShouldStartOnNextMoveEvent = true; @@ -390,33 +407,46 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Finishing gesture with event action: %d", keyAction); if (keyAction == MotionEvent.ACTION_CANCEL) { - mTriggerBack = false; + setTriggerBack(false); } - onGestureFinished(true); + onGestureFinished(); } } private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted); - if (mBackGestureStarted || mBackNavigationInfo != null) { - Log.e(TAG, "Animation is being initialized but is already started."); - finishBackNavigation(); + TouchTracker touchTracker; + if (mCurrentTracker.isInInitialState()) { + touchTracker = mCurrentTracker; + } else if (mQueuedTracker.isInInitialState()) { + touchTracker = mQueuedTracker; + } else { + Log.w(TAG, "Cannot start tracking new gesture with neither tracker in initial state."); + return; } - - mTouchTracker.setGestureStartLocation(touchX, touchY, swipeEdge); + touchTracker.setGestureStartLocation(touchX, touchY, swipeEdge); + touchTracker.setState(TouchTracker.TouchTrackerState.ACTIVE); mBackGestureStarted = true; + if (touchTracker == mCurrentTracker) { + // Only start the back navigation if no other gesture is being processed. Otherwise, + // the back navigation will be started once the current gesture has finished. + startBackNavigation(mCurrentTracker); + } + } + + private void startBackNavigation(@NonNull TouchTracker touchTracker) { try { mBackNavigationInfo = mActivityTaskManager.startBackNavigation( mNavigationObserver, mEnableAnimations.get() ? mBackAnimationAdapter : null); - onBackNavigationInfoReceived(mBackNavigationInfo); + onBackNavigationInfoReceived(mBackNavigationInfo, touchTracker); } catch (RemoteException remoteException) { Log.e(TAG, "Failed to initAnimation", remoteException); - finishBackNavigation(); + finishBackNavigation(touchTracker.getTriggerBack()); } } - private void onBackNavigationInfoReceived(@Nullable BackNavigationInfo backNavigationInfo) { + private void onBackNavigationInfoReceived(@Nullable BackNavigationInfo backNavigationInfo, + @NonNull TouchTracker touchTracker) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo); if (backNavigationInfo == null) { Log.e(TAG, "Received BackNavigationInfo is null."); @@ -430,7 +460,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } else { mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback(); - dispatchOnBackStarted(mActiveCallback, mTouchTracker.createStartEvent(null)); + dispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null)); } } @@ -438,12 +468,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (!mBackGestureStarted || mBackNavigationInfo == null || mActiveCallback == null) { return; } - - final BackMotionEvent backEvent = mTouchTracker.createProgressEvent(); + // Skip dispatching if the move corresponds to the queued instead of the current gesture + if (mQueuedTracker.isActive()) return; + final BackMotionEvent backEvent = mCurrentTracker.createProgressEvent(); dispatchOnBackProgressed(mActiveCallback, backEvent); } private void injectBackKey() { + Log.d(TAG, "injectBackKey"); sendBackEvent(KeyEvent.ACTION_DOWN); sendBackEvent(KeyEvent.ACTION_UP); } @@ -488,7 +520,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont * * @param callback the callback to be invoked when the animation ends. */ - private void dispatchOrAnimateOnBackInvoked(IOnBackInvokedCallback callback) { + private void dispatchOrAnimateOnBackInvoked(IOnBackInvokedCallback callback, + @NonNull TouchTracker touchTracker) { if (callback == null) { return; } @@ -497,12 +530,12 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (mBackNavigationInfo != null && mBackNavigationInfo.isAnimationCallback()) { - final BackMotionEvent backMotionEvent = mTouchTracker.createProgressEvent(); + final BackMotionEvent backMotionEvent = touchTracker.createProgressEvent(); if (backMotionEvent != null) { // Constraints - absolute values float minVelocity = mFlingAnimationUtils.getMinVelocityPxPerSecond(); float maxVelocity = mFlingAnimationUtils.getHighVelocityPxPerSecond(); - float maxX = mTouchTracker.getMaxDistance(); // px + float maxX = touchTracker.getMaxDistance(); // px float maxFlingDistance = maxX * MAX_FLING_PROGRESS; // px // Current state @@ -530,9 +563,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont animator.addUpdateListener(animation -> { Float animatedValue = (Float) animation.getAnimatedValue(); - float progress = mTouchTracker.getProgress(animatedValue); - final BackMotionEvent backEvent = mTouchTracker - .createProgressEvent(progress); + float progress = touchTracker.getProgress(animatedValue); + final BackMotionEvent backEvent = touchTracker.createProgressEvent( + progress); dispatchOnBackProgressed(mActiveCallback, backEvent); }); @@ -591,27 +624,27 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont * Sets to true when the back gesture has passed the triggering threshold, false otherwise. */ public void setTriggerBack(boolean triggerBack) { - if (mPostCommitAnimationInProgress) { - return; + TouchTracker activeBackGestureInfo = getActiveTracker(); + if (activeBackGestureInfo != null) { + activeBackGestureInfo.setTriggerBack(triggerBack); } - mTriggerBack = triggerBack; - mTouchTracker.setTriggerBack(triggerBack); } private void setSwipeThresholds( float linearDistance, float maxDistance, float nonLinearFactor) { - mTouchTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor); + mCurrentTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor); + mQueuedTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor); } - private void invokeOrCancelBack() { + private void invokeOrCancelBack(@NonNull TouchTracker touchTracker) { // Make a synchronized call to core before dispatch back event to client side. // If the close transition happens before the core receives onAnimationFinished, there will // play a second close animation for that transition. if (mBackAnimationFinishedCallback != null) { try { - mBackAnimationFinishedCallback.onAnimationFinished(mTriggerBack); + mBackAnimationFinishedCallback.onAnimationFinished(touchTracker.getTriggerBack()); } catch (RemoteException e) { Log.e(TAG, "Failed call IBackAnimationFinishedCallback", e); } @@ -620,30 +653,30 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (mBackNavigationInfo != null) { final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); - if (mTriggerBack) { - dispatchOrAnimateOnBackInvoked(callback); + if (touchTracker.getTriggerBack()) { + dispatchOrAnimateOnBackInvoked(callback, touchTracker); } else { dispatchOnBackCancelled(callback); } } - finishBackNavigation(); + finishBackNavigation(touchTracker.getTriggerBack()); } /** * Called when the gesture is released, then it could start the post commit animation. */ - private void onGestureFinished(boolean fromTouch) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack); - if (!mBackGestureStarted) { - finishBackNavigation(); + private void onGestureFinished() { + TouchTracker activeTouchTracker = getActiveTracker(); + if (!mBackGestureStarted || activeTouchTracker == null) { + // This can happen when an unfinished gesture has been reset in resetTouchTracker + Log.d(TAG, "onGestureFinished called while no gesture is started"); return; } + boolean triggerBack = activeTouchTracker.getTriggerBack(); + ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", triggerBack); - if (fromTouch) { - // Let touch reset the flag otherwise it will start a new back navigation and refresh - // the info when received a new move event. - mBackGestureStarted = false; - } + mBackGestureStarted = false; + activeTouchTracker.setState(TouchTracker.TouchTrackerState.FINISHED); if (mPostCommitAnimationInProgress) { ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation is still running"); @@ -652,11 +685,16 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (mBackNavigationInfo == null) { // No focus window found or core are running recents animation, inject back key as - // legacy behavior. - if (mTriggerBack) { + // legacy behavior, or new back gesture was started while previous has not finished yet + if (!mQueuedTracker.isInInitialState()) { + Log.e(TAG, "mBackNavigationInfo is null AND there is another back animation in " + + "progress"); + } + mCurrentTracker.reset(); + if (triggerBack) { injectBackKey(); } - finishBackNavigation(); + finishBackNavigation(triggerBack); return; } @@ -664,7 +702,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont // Simply trigger and finish back navigation when no animator defined. if (!shouldDispatchToAnimator() || mShellBackAnimationRegistry.isAnimationCancelledOrNull(backType)) { - invokeOrCancelBack(); + Log.d(TAG, "Trigger back without dispatching to animator."); + invokeOrCancelBack(mCurrentTracker); + mCurrentTracker.reset(); return; } else if (mShellBackAnimationRegistry.isWaitingAnimation(backType)) { ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Gesture released, but animation didn't ready."); @@ -691,8 +731,8 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION); // The next callback should be {@link #onBackAnimationFinished}. - if (mTriggerBack) { - dispatchOrAnimateOnBackInvoked(mActiveCallback); + if (mCurrentTracker.getTriggerBack()) { + dispatchOrAnimateOnBackInvoked(mActiveCallback, mCurrentTracker); } else { dispatchOnBackCancelled(mActiveCallback); } @@ -708,27 +748,64 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable); mPostCommitAnimationInProgress = false; - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()"); + Log.d(TAG, "BackAnimationController: onBackAnimationFinished()"); + + if (mCurrentTracker.isActive() || mCurrentTracker.isFinished()) { + // Trigger the real back. + invokeOrCancelBack(mCurrentTracker); + } else { + Log.d(TAG, "mCurrentBackGestureInfo was null when back animation finished"); + } + resetTouchTracker(); + } + + /** + * Resets the TouchTracker and potentially starts a new back navigation in case one is queued + */ + private void resetTouchTracker() { + TouchTracker temp = mCurrentTracker; + mCurrentTracker = mQueuedTracker; + temp.reset(); + mQueuedTracker = temp; + + if (mCurrentTracker.isInInitialState()) { + if (mBackGestureStarted) { + mBackGestureStarted = false; + dispatchOnBackCancelled(mActiveCallback); + finishBackNavigation(false); + Log.d(TAG, "resetTouchTracker -> reset an unfinished gesture"); + } else { + Log.d(TAG, "resetTouchTracker -> no queued gesture"); + } + return; + } - // Trigger the real back. - invokeOrCancelBack(); + if (mCurrentTracker.isFinished() && mCurrentTracker.getTriggerBack()) { + Log.d(TAG, "resetTouchTracker -> start queued back navigation AND post commit " + + "animation"); + injectBackKey(); + finishBackNavigation(true); + mCurrentTracker.reset(); + } else if (!mCurrentTracker.isFinished()) { + Log.d(TAG, "resetTouchTracker -> queued gesture not finished; do nothing"); + } else { + Log.d(TAG, "resetTouchTracker -> reset queued gesture"); + mCurrentTracker.reset(); + } } /** * This should be called after the whole back navigation is completed. */ @VisibleForTesting - void finishBackNavigation() { + void finishBackNavigation(boolean triggerBack) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()"); - mShouldStartOnNextMoveEvent = false; - mTouchTracker.reset(); mActiveCallback = null; mShellBackAnimationRegistry.resetDefaultCrossActivity(); if (mBackNavigationInfo != null) { - mBackNavigationInfo.onBackNavigationFinished(mTriggerBack); + mBackNavigationInfo.onBackNavigationFinished(triggerBack); mBackNavigationInfo = null; } - mTriggerBack = false; } @@ -781,14 +858,14 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (apps.length >= 1) { dispatchOnBackStarted( mActiveCallback, - mTouchTracker.createStartEvent(apps[0])); + mCurrentTracker.createStartEvent(apps[0])); } // Dispatch the first progress after animation start for // smoothing the initial animation, instead of waiting for next // onMove. - final BackMotionEvent backFinish = - mTouchTracker.createProgressEvent(); + final BackMotionEvent backFinish = mCurrentTracker + .createProgressEvent(); dispatchOnBackProgressed(mActiveCallback, backFinish); if (!mBackGestureStarted) { // if the down -> up gesture happened before animation @@ -808,7 +885,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return; } if (!mBackGestureStarted) { - invokeOrCancelBack(); + invokeOrCancelBack(mCurrentTracker); } }); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java index a0ada39b459e4be3cab29b1cb6be7027571bd8f6..8a59a9f62425004489187a04c284fee8e4d0e182 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/TouchTracker.java @@ -52,6 +52,7 @@ class TouchTracker { private float mStartThresholdX; private int mSwipeEdge; private boolean mCancelled; + private TouchTrackerState mState = TouchTrackerState.INITIAL; void update(float touchX, float touchY, float velocityX, float velocityY) { /** @@ -76,6 +77,26 @@ class TouchTracker { mTriggerBack = triggerBack; } + boolean getTriggerBack() { + return mTriggerBack; + } + + void setState(TouchTrackerState state) { + mState = state; + } + + boolean isInInitialState() { + return mState == TouchTrackerState.INITIAL; + } + + boolean isActive() { + return mState == TouchTrackerState.ACTIVE; + } + + boolean isFinished() { + return mState == TouchTrackerState.FINISHED; + } + void setGestureStartLocation(float touchX, float touchY, int swipeEdge) { mInitTouchX = touchX; mInitTouchY = touchY; @@ -89,6 +110,7 @@ class TouchTracker { mStartThresholdX = 0; mCancelled = false; mTriggerBack = false; + mState = TouchTrackerState.INITIAL; mSwipeEdge = BackEvent.EDGE_LEFT; } @@ -186,4 +208,9 @@ class TouchTracker { mMaxDistance = maxDistance; mNonLinearFactor = nonLinearFactor; } + + enum TouchTrackerState { + INITIAL, ACTIVE, FINISHED + } + } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index d6141cfd21baf1c08750129a550c826349e6b0f9..6cf5450db9ea064289f5a7536c33a9a3b7bf1405 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -37,6 +37,7 @@ import android.app.WindowConfiguration; import android.content.pm.ApplicationInfo; import android.graphics.Point; import android.graphics.Rect; +import android.hardware.input.InputManager; import android.os.Bundle; import android.os.Handler; import android.os.RemoteCallback; @@ -44,9 +45,9 @@ import android.os.RemoteException; import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableContentResolver; -import android.testing.TestableContext; import android.testing.TestableLooper; import android.view.IRemoteAnimationRunner; +import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; @@ -58,7 +59,6 @@ import android.window.IOnBackInvokedCallback; import androidx.annotation.Nullable; import androidx.test.filters.SmallTest; -import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.util.test.FakeSettingsProvider; import com.android.wm.shell.ShellTestCase; @@ -69,7 +69,6 @@ import com.android.wm.shell.sysui.ShellSharedConstants; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -86,9 +85,6 @@ public class BackAnimationControllerTest extends ShellTestCase { private static final String ANIMATION_ENABLED = "1"; private final TestShellExecutor mShellExecutor = new TestShellExecutor(); - @Rule - public TestableContext mContext = - new TestableContext(InstrumentationRegistry.getInstrumentation().getContext()); private ShellInit mShellInit; @Mock @@ -112,6 +108,9 @@ public class BackAnimationControllerTest extends ShellTestCase { @Mock private BackAnimationBackground mAnimationBackground; + @Mock + private InputManager mInputManager; + private BackAnimationController mController; private TestableContentResolver mContentResolver; private TestableLooper mTestableLooper; @@ -121,6 +120,7 @@ public class BackAnimationControllerTest extends ShellTestCase { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); + mContext.addMockSystemService(InputManager.class, mInputManager); mContext.getApplicationInfo().privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED; mContentResolver = new TestableContentResolver(mContext); mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); @@ -224,7 +224,7 @@ public class BackAnimationControllerTest extends ShellTestCase { .setPrepareRemoteAnimation(true) .setOnBackNavigationDone(new RemoteCallback(result))); triggerBackGesture(); - simulateRemoteAnimationStart(type); + simulateRemoteAnimationStart(); mShellExecutor.flushAll(); releaseBackGesture(); simulateRemoteAnimationFinished(); @@ -248,7 +248,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // Check that back start and progress is dispatched when first move. doMotionEvent(MotionEvent.ACTION_MOVE, 100); - simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); + simulateRemoteAnimationStart(); verify(mAnimatorCallback).onBackStarted(any(BackMotionEvent.class)); verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); @@ -274,7 +274,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // Check that back start and progress is dispatched when first move. doMotionEvent(MotionEvent.ACTION_MOVE, 100, 3000); - simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); + simulateRemoteAnimationStart(); verify(mAnimatorCallback).onBackStarted(any(BackMotionEvent.class)); verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); @@ -327,14 +327,14 @@ public class BackAnimationControllerTest extends ShellTestCase { } @Test - public void ignoresGesture_transitionInProgress() throws RemoteException { + public void gestureQueued_WhenPreviousTransitionHasNotYetEnded() throws RemoteException { registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, /* enableAnimation = */ true, /* isAnimationCallback = */ false); triggerBackGesture(); - simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); + simulateRemoteAnimationStart(); releaseBackGesture(); // Check that back invocation is dispatched. @@ -344,24 +344,64 @@ public class BackAnimationControllerTest extends ShellTestCase { reset(mAnimatorCallback); reset(mBackAnimationRunner); - // Verify that we prevent animation from restarting if another gestures happens before - // the previous transition is finished. - doMotionEvent(MotionEvent.ACTION_DOWN, 0); + // Verify that we prevent any interaction with the animator callback in case a new gesture + // starts while the current back animation has not ended, instead the gesture is queued + triggerBackGesture(); verifyNoMoreInteractions(mAnimatorCallback); - // Finish back navigation. + // Finish previous back navigation. simulateRemoteAnimationFinished(); - // Verify that more events from a rejected swipe cannot start animation. + // Verify that releasing the gesture causes back key to be injected + releaseBackGesture(); + verify(mInputManager, times(2)) + .injectInputEvent(any(KeyEvent.class), any(Integer.class)); + + // Verify that we start accepting gestures again once transition finishes. + doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); - doMotionEvent(MotionEvent.ACTION_UP, 0); + + simulateRemoteAnimationStart(); + verify(mAnimatorCallback).onBackStarted(any()); + verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); + } + + @Test + public void queuedFinishedGesture_RunsAfterPreviousTransitionEnded() throws RemoteException { + registerAnimation(BackNavigationInfo.TYPE_RETURN_TO_HOME); + createNavigationInfo(BackNavigationInfo.TYPE_RETURN_TO_HOME, + /* enableAnimation = */ true, + /* isAnimationCallback = */ false); + + triggerBackGesture(); + simulateRemoteAnimationStart(); + releaseBackGesture(); + + // Check that back invocation is dispatched. + verify(mAnimatorCallback).onBackInvoked(); + verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); + + reset(mAnimatorCallback); + reset(mBackAnimationRunner); + + // Verify that we prevent any interaction with the animator callback in case a new gesture + // starts while the current back animation has not ended, instead the gesture is queued + triggerBackGesture(); + releaseBackGesture(); verifyNoMoreInteractions(mAnimatorCallback); + // Finish previous back navigation. + simulateRemoteAnimationFinished(); + + // Verify that back key press is injected after previous back navigation has ended + verify(mInputManager, times(2)) + .injectInputEvent(any(KeyEvent.class), any(Integer.class)); + // Verify that we start accepting gestures again once transition finishes. doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); - simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); + simulateRemoteAnimationStart(); verify(mAnimatorCallback).onBackStarted(any()); verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); } @@ -377,7 +417,7 @@ public class BackAnimationControllerTest extends ShellTestCase { doNothing().when(mAnimatorCallback).onBackInvoked(); triggerBackGesture(); - simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); + simulateRemoteAnimationStart(); mShellExecutor.flushAll(); releaseBackGesture(); @@ -388,7 +428,7 @@ public class BackAnimationControllerTest extends ShellTestCase { doMotionEvent(MotionEvent.ACTION_DOWN, 0); doMotionEvent(MotionEvent.ACTION_MOVE, 100); - simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); + simulateRemoteAnimationStart(); verify(mAnimatorCallback).onBackStarted(any()); } @@ -404,7 +444,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // Check that back start and progress is dispatched when first move. doMotionEvent(MotionEvent.ACTION_MOVE, 100); - simulateRemoteAnimationStart(BackNavigationInfo.TYPE_RETURN_TO_HOME); + simulateRemoteAnimationStart(); verify(mAnimatorCallback).onBackStarted(any()); verify(mBackAnimationRunner).onAnimationStart(anyInt(), any(), any(), any(), any()); @@ -443,7 +483,7 @@ public class BackAnimationControllerTest extends ShellTestCase { .setPrepareRemoteAnimation(true) .setOnBackNavigationDone(new RemoteCallback(result))); triggerBackGesture(); - simulateRemoteAnimationStart(type); + simulateRemoteAnimationStart(); mShellExecutor.flushAll(); releaseBackGesture(); @@ -527,7 +567,7 @@ public class BackAnimationControllerTest extends ShellTestCase { // Check that back start and progress is dispatched when first move. doMotionEvent(MotionEvent.ACTION_MOVE, 100); - simulateRemoteAnimationStart(type); + simulateRemoteAnimationStart(); verify(callback).onBackStarted(any(BackMotionEvent.class)); verify(animationRunner).startAnimation(any(), any(), any(), any()); @@ -552,7 +592,7 @@ public class BackAnimationControllerTest extends ShellTestCase { /* swipeEdge */ BackEvent.EDGE_LEFT); } - private void simulateRemoteAnimationStart(int type) throws RemoteException { + private void simulateRemoteAnimationStart() throws RemoteException { RemoteAnimationTarget animationTarget = createAnimationTarget(); RemoteAnimationTarget[] targets = new RemoteAnimationTarget[]{animationTarget}; if (mController.mBackAnimationAdapter != null) { @@ -564,7 +604,7 @@ public class BackAnimationControllerTest extends ShellTestCase { private void simulateRemoteAnimationFinished() { mController.onBackAnimationFinished(); - mController.finishBackNavigation(); + mController.finishBackNavigation(/*triggerBack*/ true); } private void registerAnimation(int type) { @@ -590,5 +630,4 @@ public class BackAnimationControllerTest extends ShellTestCase { mTriggerBack = result.getBoolean(KEY_TRIGGER_BACK); } } - ; }