/* * Copyright (C) 2021 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 com.android.wm.shell.back; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; import static com.android.window.flags.Flags.predictiveBackSystemAnims; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; import android.content.ContentResolver; import android.content.Context; import android.content.res.Configuration; import android.database.ContentObserver; import android.graphics.Rect; import android.hardware.input.InputManager; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.provider.Settings.Global; import android.util.Log; import android.view.IRemoteAnimationRunner; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.RemoteAnimationTarget; import android.view.WindowManager; import android.window.BackAnimationAdapter; import android.window.BackEvent; import android.window.BackMotionEvent; import android.window.BackNavigationInfo; import android.window.BackTouchTracker; import android.window.IBackAnimationFinishedCallback; import android.window.IBackAnimationRunner; import android.window.IOnBackInvokedCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.internal.util.LatencyTracker; import com.android.internal.view.AppearanceRegion; import com.android.wm.shell.R; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import java.io.PrintWriter; import java.util.concurrent.atomic.AtomicBoolean; /** * Controls the window animation run when a user initiates a back gesture. */ public class BackAnimationController implements RemoteCallable, ConfigurationChangeListener { private static final String TAG = "ShellBackPreview"; private static final int SETTING_VALUE_OFF = 0; private static final int SETTING_VALUE_ON = 1; public static final boolean IS_ENABLED = SystemProperties.getInt("persist.wm.debug.predictive_back", SETTING_VALUE_ON) == SETTING_VALUE_ON; /** Predictive back animation developer option */ private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false); /** * Max duration to wait for an animation to finish before triggering the real back. */ private static final long MAX_ANIMATION_DURATION = 2000; private final LatencyTracker mLatencyTracker; /** True when a back gesture is ongoing */ private boolean mBackGestureStarted = false; /** 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; private boolean mOnBackStartDispatched = false; private boolean mThresholdCrossed = false; private boolean mPointersPilfered = false; private final boolean mRequirePointerPilfer; /** Registry for the back animations */ private final ShellBackAnimationRegistry mShellBackAnimationRegistry; @Nullable private BackNavigationInfo mBackNavigationInfo; private final IActivityTaskManager mActivityTaskManager; private final Context mContext; private final ContentResolver mContentResolver; private final ShellController mShellController; private final ShellCommandHandler mShellCommandHandler; private final ShellExecutor mShellExecutor; private final Handler mBgHandler; private final WindowManager mWindowManager; @VisibleForTesting final Rect mTouchableArea = new Rect(); /** * Tracks the current user back gesture. */ private BackTouchTracker mCurrentTracker = new BackTouchTracker(); /** * 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 BackTouchTracker mQueuedTracker = new BackTouchTracker(); private final Runnable mAnimationTimeoutRunnable = () -> { ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...", MAX_ANIMATION_DURATION); finishBackAnimation(); }; private IBackAnimationFinishedCallback mBackAnimationFinishedCallback; @VisibleForTesting BackAnimationAdapter mBackAnimationAdapter; @Nullable private IOnBackInvokedCallback mActiveCallback; @Nullable private RemoteAnimationTarget[] mApps; @VisibleForTesting final RemoteCallback mNavigationObserver = new RemoteCallback( new RemoteCallback.OnResultListener() { @Override public void onResult(@Nullable Bundle result) { mShellExecutor.execute(() -> { if (!mBackGestureStarted || mPostCommitAnimationInProgress) { // If an uninterruptible animation is already in progress, we should // ignore this due to it may cause focus lost. (alpha = 0) return; } ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Navigation window gone."); setTriggerBack(false); resetTouchTracker(); // Don't wait for animation start mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable); }); } }); private final BackAnimationBackground mAnimationBackground; private StatusBarCustomizer mCustomizer; private boolean mTrackingLatency; // Keep previous navigation type before remove mBackNavigationInfo. @BackNavigationInfo.BackTargetType private int mPreviousNavigationType; private Runnable mPilferPointerCallback; public BackAnimationController( @NonNull ShellInit shellInit, @NonNull ShellController shellController, @NonNull @ShellMainThread ShellExecutor shellExecutor, @NonNull @ShellBackgroundThread Handler backgroundHandler, Context context, @NonNull BackAnimationBackground backAnimationBackground, ShellBackAnimationRegistry shellBackAnimationRegistry, ShellCommandHandler shellCommandHandler) { this( shellInit, shellController, shellExecutor, backgroundHandler, ActivityTaskManager.getService(), context, context.getContentResolver(), backAnimationBackground, shellBackAnimationRegistry, shellCommandHandler); } @VisibleForTesting BackAnimationController( @NonNull ShellInit shellInit, @NonNull ShellController shellController, @NonNull @ShellMainThread ShellExecutor shellExecutor, @NonNull @ShellBackgroundThread Handler bgHandler, @NonNull IActivityTaskManager activityTaskManager, Context context, ContentResolver contentResolver, @NonNull BackAnimationBackground backAnimationBackground, ShellBackAnimationRegistry shellBackAnimationRegistry, ShellCommandHandler shellCommandHandler) { mShellController = shellController; mShellExecutor = shellExecutor; mActivityTaskManager = activityTaskManager; mContext = context; mContentResolver = contentResolver; mRequirePointerPilfer = context.getResources().getBoolean(R.bool.config_backAnimationRequiresPointerPilfer); mBgHandler = bgHandler; shellInit.addInitCallback(this::onInit, this); mAnimationBackground = backAnimationBackground; mShellBackAnimationRegistry = shellBackAnimationRegistry; mLatencyTracker = LatencyTracker.getInstance(mContext); mShellCommandHandler = shellCommandHandler; mWindowManager = context.getSystemService(WindowManager.class); updateTouchableArea(); } private void onInit() { setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler); updateEnableAnimationFromFlags(); createAdapter(); mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION, this::createExternalInterface, this); mShellCommandHandler.addDumpCallback(this::dump, this); mShellController.addConfigurationChangeListener(this); } private void setupAnimationDeveloperSettingsObserver( @NonNull ContentResolver contentResolver, @NonNull @ShellBackgroundThread final Handler backgroundHandler) { if (predictiveBackSystemAnims()) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation aconfig flag is enabled, therefore " + "developer settings flag is ignored and no content observer registered"); return; } ContentObserver settingsObserver = new ContentObserver(backgroundHandler) { @Override public void onChange(boolean selfChange, Uri uri) { updateEnableAnimationFromFlags(); } }; contentResolver.registerContentObserver( Global.getUriFor(Global.ENABLE_BACK_ANIMATION), false, settingsObserver, UserHandle.USER_SYSTEM ); } /** * Updates {@link BackAnimationController#mEnableAnimations} based on the current values of the * aconfig flag and the developer settings flag */ @ShellBackgroundThread private void updateEnableAnimationFromFlags() { boolean isEnabled = predictiveBackSystemAnims() || isDeveloperSettingEnabled(); mEnableAnimations.set(isEnabled); ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s", isEnabled); } private boolean isDeveloperSettingEnabled() { return Global.getInt(mContext.getContentResolver(), Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF) == SETTING_VALUE_ON; } public BackAnimation getBackAnimationImpl() { return mBackAnimation; } private ExternalInterfaceBinder createExternalInterface() { return new IBackAnimationImpl(this); } private final BackAnimationImpl mBackAnimation = new BackAnimationImpl(); @Override public void onConfigurationChanged(Configuration newConfig) { mShellBackAnimationRegistry.onConfigurationChanged(newConfig); updateTouchableArea(); } private void updateTouchableArea() { mTouchableArea.set(mWindowManager.getCurrentWindowMetrics().getBounds()); } @Override public Context getContext() { return mContext; } @Override public ShellExecutor getRemoteCallExecutor() { return mShellExecutor; } private class BackAnimationImpl implements BackAnimation { @Override public void onBackMotion( float touchX, float touchY, float velocityX, float velocityY, int keyAction, @BackEvent.SwipeEdge int swipeEdge ) { mShellExecutor.execute(() -> onMotionEvent( /* touchX = */ touchX, /* touchY = */ touchY, /* velocityX = */ velocityX, /* velocityY = */ velocityY, /* keyAction = */ keyAction, /* swipeEdge = */ swipeEdge)); } @Override public void onThresholdCrossed() { BackAnimationController.this.onThresholdCrossed(); } @Override public void setTriggerBack(boolean triggerBack) { mShellExecutor.execute(() -> BackAnimationController.this.setTriggerBack(triggerBack)); } @Override public void setSwipeThresholds( float linearDistance, float maxDistance, float nonLinearFactor) { mShellExecutor.execute(() -> BackAnimationController.this.setSwipeThresholds( linearDistance, maxDistance, nonLinearFactor)); } @Override public void setStatusBarCustomizer(StatusBarCustomizer customizer) { mCustomizer = customizer; mAnimationBackground.setStatusBarCustomizer(customizer); } @Override public void setPilferPointerCallback(Runnable callback) { mShellExecutor.execute(() -> { mPilferPointerCallback = callback; }); } } private static class IBackAnimationImpl extends IBackAnimation.Stub implements ExternalInterfaceBinder { private BackAnimationController mController; IBackAnimationImpl(BackAnimationController controller) { mController = controller; } @Override public void setBackToLauncherCallback(IOnBackInvokedCallback callback, IRemoteAnimationRunner runner) { executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback", (controller) -> controller.registerAnimation( BackNavigationInfo.TYPE_RETURN_TO_HOME, new BackAnimationRunner( callback, runner, controller.mContext, CUJ_PREDICTIVE_BACK_HOME))); } @Override public void clearBackToLauncherCallback() { executeRemoteCallWithTaskPermission(mController, "clearBackToLauncherCallback", (controller) -> controller.unregisterAnimation( BackNavigationInfo.TYPE_RETURN_TO_HOME)); } public void customizeStatusBarAppearance(AppearanceRegion appearance) { executeRemoteCallWithTaskPermission(mController, "useLauncherSysBarFlags", (controller) -> controller.customizeStatusBarAppearance(appearance)); } @Override public void invalidate() { mController = null; } } private void customizeStatusBarAppearance(AppearanceRegion appearance) { if (mCustomizer != null) { mCustomizer.customizeStatusBarAppearance(appearance); } } void registerAnimation(@BackNavigationInfo.BackTargetType int type, @NonNull BackAnimationRunner runner) { mShellBackAnimationRegistry.registerAnimation(type, runner); } void unregisterAnimation(@BackNavigationInfo.BackTargetType int type) { mShellBackAnimationRegistry.unregisterAnimation(type); } private BackTouchTracker getActiveTracker() { if (mCurrentTracker.isActive()) return mCurrentTracker; if (mQueuedTracker.isActive()) return mQueuedTracker; return null; } @VisibleForTesting public void onThresholdCrossed() { mThresholdCrossed = true; // Dispatch onBackStarted, only to app callbacks. // System callbacks will receive onBackStarted when the remote animation starts. final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(); if (!shouldDispatchToAnimator && mActiveCallback != null) { mCurrentTracker.updateStartLocation(); tryDispatchOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent(null)); if (mBackNavigationInfo != null && !isAppProgressGenerationAllowed()) { tryPilferPointers(); } } else if (shouldDispatchToAnimator) { tryPilferPointers(); } } private boolean isAppProgressGenerationAllowed() { return mBackNavigationInfo.isAppProgressGenerationAllowed() && mBackNavigationInfo.getTouchableRegion().equals(mTouchableArea); } /** * Called when a new motion event needs to be transferred to this * {@link BackAnimationController} */ public void onMotionEvent( float touchX, float touchY, float velocityX, float velocityY, int keyAction, @BackEvent.SwipeEdge int swipeEdge) { BackTouchTracker 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()) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Ignoring MotionEvent because two gestures are already being queued."); return; } if (keyAction == MotionEvent.ACTION_DOWN) { if (!mBackGestureStarted) { mShouldStartOnNextMoveEvent = true; } } else if (keyAction == MotionEvent.ACTION_MOVE) { if (!mBackGestureStarted && mShouldStartOnNextMoveEvent) { // Let the animation initialized here to make sure the onPointerDownOutsideFocus // could be happened when ACTION_DOWN, it may change the current focus that we // would access it when startBackNavigation. onGestureStarted(touchX, touchY, swipeEdge); mShouldStartOnNextMoveEvent = false; } onMove(); } else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Finishing gesture with event action: %d", keyAction); if (keyAction == MotionEvent.ACTION_CANCEL) { setTriggerBack(false); } onGestureFinished(); } } private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) { boolean interruptCancelPostCommitAnimation = mPostCommitAnimationInProgress && mCurrentTracker.isFinished() && !mCurrentTracker.getTriggerBack() && mQueuedTracker.isInInitialState(); if (interruptCancelPostCommitAnimation) { // If a system animation is currently in the post-commit phase animating an // onBackCancelled event, let's interrupt it and start animating a new back gesture resetTouchTracker(); } BackTouchTracker touchTracker; if (mCurrentTracker.isInInitialState()) { touchTracker = mCurrentTracker; } else if (mQueuedTracker.isInInitialState()) { touchTracker = mQueuedTracker; } else { ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Cannot start tracking new gesture with neither tracker in initial state."); return; } touchTracker.setGestureStartLocation(touchX, touchY, swipeEdge); touchTracker.setState(BackTouchTracker.TouchTrackerState.ACTIVE); mBackGestureStarted = true; if (interruptCancelPostCommitAnimation) { // post-commit cancel is currently running. let's interrupt it and dispatch a new // onBackStarted event. mPostCommitAnimationInProgress = false; mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable); startSystemAnimation(); } else if (touchTracker == mCurrentTracker) { // Only start the back navigation if no other gesture is being processed. Otherwise, // the back navigation will fall back to legacy back event injection. startBackNavigation(mCurrentTracker); } } private void startBackNavigation(@NonNull BackTouchTracker touchTracker) { try { startLatencyTracking(); mBackNavigationInfo = mActivityTaskManager.startBackNavigation( mNavigationObserver, mEnableAnimations.get() ? mBackAnimationAdapter : null); onBackNavigationInfoReceived(mBackNavigationInfo, touchTracker); } catch (RemoteException remoteException) { Log.e(TAG, "Failed to initAnimation", remoteException); finishBackNavigation(touchTracker.getTriggerBack()); } } private void onBackNavigationInfoReceived(@Nullable BackNavigationInfo backNavigationInfo, @NonNull BackTouchTracker touchTracker) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo); if (backNavigationInfo == null) { ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Received BackNavigationInfo is null."); cancelLatencyTracking(); return; } final int backType = backNavigationInfo.getType(); final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(); if (shouldDispatchToAnimator) { if (!mShellBackAnimationRegistry.startGesture(backType)) { mActiveCallback = null; } tryPilferPointers(); } else { mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback(); // App is handling back animation. Cancel system animation latency tracking. cancelLatencyTracking(); tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent(null)); if (!isAppProgressGenerationAllowed()) { tryPilferPointers(); } } } private void onMove() { if (!mBackGestureStarted || mBackNavigationInfo == null || mActiveCallback == null || !mOnBackStartDispatched) { return; } // 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() { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "injectBackKey"); sendBackEvent(KeyEvent.ACTION_DOWN); sendBackEvent(KeyEvent.ACTION_UP); } @SuppressLint("MissingPermission") private void sendBackEvent(int action) { final long when = SystemClock.uptimeMillis(); final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK, 0 /* repeat */, 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, InputDevice.SOURCE_KEYBOARD); ev.setDisplayId(mContext.getDisplay().getDisplayId()); if (!mContext.getSystemService(InputManager.class) .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) { ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Inject input event fail"); } } private boolean shouldDispatchToAnimator() { return mEnableAnimations.get() && mBackNavigationInfo != null && mBackNavigationInfo.isPrepareRemoteAnimation(); } private void tryPilferPointers() { if (mPointersPilfered || !mThresholdCrossed) { return; } if (mPilferPointerCallback != null) { mPilferPointerCallback.run(); } mPointersPilfered = true; } private void tryDispatchOnBackStarted( IOnBackInvokedCallback callback, BackMotionEvent backEvent) { if (mOnBackStartDispatched || callback == null || (!mThresholdCrossed && mRequirePointerPilfer)) { return; } dispatchOnBackStarted(callback, backEvent); } private void dispatchOnBackStarted( IOnBackInvokedCallback callback, BackMotionEvent backEvent) { if (callback == null) { return; } try { callback.onBackStarted(backEvent); mOnBackStartDispatched = true; } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackStarted error: ", e); } } private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) { if (callback == null) { return; } try { callback.onBackInvoked(); } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackInvoked error: ", e); } } private void tryDispatchOnBackCancelled(IOnBackInvokedCallback callback) { if (!mOnBackStartDispatched) { Log.d(TAG, "Skipping dispatching onBackCancelled. Start was never dispatched."); return; } if (callback == null) { return; } try { callback.onBackCancelled(); } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackCancelled error: ", e); } } private void dispatchOnBackProgressed(IOnBackInvokedCallback callback, BackMotionEvent backEvent) { if (callback == null || (!shouldDispatchToAnimator() && mBackNavigationInfo != null && isAppProgressGenerationAllowed())) { return; } try { callback.onBackProgressed(backEvent); } catch (RemoteException e) { Log.e(TAG, "dispatchOnBackProgressed error: ", e); } } /** * Sets to true when the back gesture has passed the triggering threshold, false otherwise. */ public void setTriggerBack(boolean triggerBack) { if (mActiveCallback != null) { try { mActiveCallback.setTriggerBack(triggerBack); } catch (RemoteException e) { Log.e(TAG, "remote setTriggerBack error: ", e); } } BackTouchTracker activeBackGestureInfo = getActiveTracker(); if (activeBackGestureInfo != null) { activeBackGestureInfo.setTriggerBack(triggerBack); } } private void setSwipeThresholds( float linearDistance, float maxDistance, float nonLinearFactor) { mCurrentTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor); mQueuedTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor); } private void invokeOrCancelBack(@NonNull BackTouchTracker 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(touchTracker.getTriggerBack()); } catch (RemoteException e) { Log.e(TAG, "Failed call IBackAnimationFinishedCallback", e); } mBackAnimationFinishedCallback = null; } if (mBackNavigationInfo != null) { final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); if (touchTracker.getTriggerBack()) { dispatchOnBackInvoked(callback); } else { tryDispatchOnBackCancelled(callback); } } finishBackNavigation(touchTracker.getTriggerBack()); } /** * Called when the gesture is released, then it could start the post commit animation. */ private void onGestureFinished() { BackTouchTracker activeTouchTracker = getActiveTracker(); if (!mBackGestureStarted || activeTouchTracker == null) { // This can happen when an unfinished gesture has been reset in resetTouchTracker ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished called while no gesture is started"); return; } boolean triggerBack = activeTouchTracker.getTriggerBack(); ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", triggerBack); // Reset gesture states. mThresholdCrossed = false; mPointersPilfered = false; mBackGestureStarted = false; activeTouchTracker.setState(BackTouchTracker.TouchTrackerState.FINISHED); if (mPostCommitAnimationInProgress) { ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation is still running"); return; } if (mBackNavigationInfo == null) { // No focus window found or core are running recents animation, inject back key as // legacy behavior, or new back gesture was started while previous has not finished yet if (!mQueuedTracker.isInInitialState()) { ProtoLog.e(WM_SHELL_BACK_PREVIEW, "mBackNavigationInfo is null AND there is " + "another back animation in progress"); } mCurrentTracker.reset(); if (triggerBack) { injectBackKey(); } finishBackNavigation(triggerBack); return; } final int backType = mBackNavigationInfo.getType(); // Simply trigger and finish back navigation when no animator defined. if (!shouldDispatchToAnimator() || mShellBackAnimationRegistry.isAnimationCancelledOrNull(backType)) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "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."); // Supposed it is in post commit animation state, and start the timeout to watch // if the animation is ready. mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION); return; } startPostCommitAnimation(); } /** * Start the phase 2 animation when gesture is released. * Callback to {@link #onBackAnimationFinished} when it is finished or timeout. */ private void startPostCommitAnimation() { if (mPostCommitAnimationInProgress) { return; } mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable); ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startPostCommitAnimation()"); mPostCommitAnimationInProgress = true; mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, MAX_ANIMATION_DURATION); // The next callback should be {@link #onBackAnimationFinished}. if (mCurrentTracker.getTriggerBack()) { // notify gesture finished mBackNavigationInfo.onBackGestureFinished(true); dispatchOnBackInvoked(mActiveCallback); } else { tryDispatchOnBackCancelled(mActiveCallback); } } /** * Called when the post commit animation is completed or timeout. * This will trigger the real {@link IOnBackInvokedCallback} behavior. */ @VisibleForTesting void onBackAnimationFinished() { if (!mPostCommitAnimationInProgress) { // This can happen when a post-commit cancel animation was interrupted by a new back // gesture but the timing of interruption was bad such that the back-callback // implementation finished in between the time of the new gesture having started and // the time of the back-callback receiving the new onBackStarted event. Due to the // asynchronous APIs this isn't an unlikely case. To handle this, let's return early. // The back-callback implementation will call onBackAnimationFinished again when it is // done with animating the second gesture. return; } finishBackAnimation(); } private void finishBackAnimation() { // Stop timeout runner. mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable); mPostCommitAnimationInProgress = false; ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()"); if (mCurrentTracker.isActive() || mCurrentTracker.isFinished()) { // Trigger the real back. invokeOrCancelBack(mCurrentTracker); } else { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "mCurrentBackGestureInfo was null when back animation finished"); } resetTouchTracker(); } /** * Resets the BackTouchTracker and potentially starts a new back navigation in case one * is queued. */ private void resetTouchTracker() { BackTouchTracker temp = mCurrentTracker; mCurrentTracker = mQueuedTracker; temp.reset(); mQueuedTracker = temp; if (mCurrentTracker.isInInitialState()) { if (mBackGestureStarted) { mBackGestureStarted = false; tryDispatchOnBackCancelled(mActiveCallback); finishBackNavigation(false); ProtoLog.d(WM_SHELL_BACK_PREVIEW, "resetTouchTracker -> reset an unfinished gesture"); } else { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "resetTouchTracker -> no queued gesture"); } return; } if (mCurrentTracker.isFinished() && mCurrentTracker.getTriggerBack()) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "resetTouchTracker -> start queued back navigation " + "AND post commit animation"); injectBackKey(); finishBackNavigation(true); mCurrentTracker.reset(); } else if (!mCurrentTracker.isFinished()) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "resetTouchTracker -> queued gesture not finished; do nothing"); } else { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "resetTouchTracker -> reset queued gesture"); mCurrentTracker.reset(); } } /** * This should be called after the whole back navigation is completed. */ @VisibleForTesting void finishBackNavigation(boolean triggerBack) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()"); mActiveCallback = null; mApps = null; mShouldStartOnNextMoveEvent = false; mOnBackStartDispatched = false; mThresholdCrossed = false; mPointersPilfered = false; mShellBackAnimationRegistry.resetDefaultCrossActivity(); cancelLatencyTracking(); if (mBackNavigationInfo != null) { mPreviousNavigationType = mBackNavigationInfo.getType(); mBackNavigationInfo.onBackNavigationFinished(triggerBack); mBackNavigationInfo = null; } } private void startLatencyTracking() { if (mTrackingLatency) { cancelLatencyTracking(); } mLatencyTracker.onActionStart(LatencyTracker.ACTION_BACK_SYSTEM_ANIMATION); mTrackingLatency = true; } private void cancelLatencyTracking() { if (!mTrackingLatency) { return; } mLatencyTracker.onActionCancel(LatencyTracker.ACTION_BACK_SYSTEM_ANIMATION); mTrackingLatency = false; } private void endLatencyTracking() { if (!mTrackingLatency) { return; } mLatencyTracker.onActionEnd(LatencyTracker.ACTION_BACK_SYSTEM_ANIMATION); mTrackingLatency = false; } private void startSystemAnimation() { if (mBackNavigationInfo == null) { ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Lack of navigation info to start animation."); return; } if (!validateAnimationTargets(mApps)) { ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Not starting animation due to mApps being null."); return; } final BackAnimationRunner runner = mShellBackAnimationRegistry.getAnimationRunnerAndInit(mBackNavigationInfo); if (runner == null) { if (mBackAnimationFinishedCallback != null) { try { mBackAnimationFinishedCallback.onAnimationFinished(false); } catch (RemoteException e) { Log.w(TAG, "Failed call IBackNaviAnimationController", e); } } return; } mActiveCallback = runner.getCallback(); ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()"); runner.startAnimation(mApps, /*wallpapers*/ null, /*nonApps*/ null, () -> mShellExecutor.execute(this::onBackAnimationFinished)); if (mApps.length >= 1) { mCurrentTracker.updateStartLocation(); BackMotionEvent startEvent = mCurrentTracker.createStartEvent(mApps[0]); dispatchOnBackStarted(mActiveCallback, startEvent); } } /** * Validate animation targets. */ static boolean validateAnimationTargets(RemoteAnimationTarget[] apps) { if (apps == null || apps.length == 0) { return false; } for (int i = apps.length - 1; i >= 0; --i) { if (!apps[i].leash.isValid()) { return false; } } return true; } private void createAdapter() { IBackAnimationRunner runner = new IBackAnimationRunner.Stub() { @Override public void onAnimationStart( RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers, RemoteAnimationTarget[] nonApps, IBackAnimationFinishedCallback finishedCallback) { mShellExecutor.execute( () -> { endLatencyTracking(); if (!validateAnimationTargets(apps)) { Log.e(TAG, "Invalid animation targets!"); return; } mBackAnimationFinishedCallback = finishedCallback; mApps = apps; startSystemAnimation(); // Dispatch the first progress after animation start for // smoothing the initial animation, instead of waiting for next // onMove. final BackMotionEvent backFinish = mCurrentTracker .createProgressEvent(); dispatchOnBackProgressed(mActiveCallback, backFinish); if (!mBackGestureStarted) { // if the down -> up gesture happened before animation // start, we have to trigger the uninterruptible transition // to finish the back animation. startPostCommitAnimation(); } }); } @Override public void onAnimationCancelled() { mShellExecutor.execute( () -> { if (!mShellBackAnimationRegistry.cancel( mBackNavigationInfo != null ? mBackNavigationInfo.getType() : mPreviousNavigationType)) { return; } if (!mBackGestureStarted) { invokeOrCancelBack(mCurrentTracker); } }); } }; mBackAnimationAdapter = new BackAnimationAdapter(runner); } /** * Description of current BackAnimationController state. */ private void dump(PrintWriter pw, String prefix) { pw.println(prefix + "BackAnimationController state:"); pw.println(prefix + " mEnableAnimations=" + mEnableAnimations.get()); pw.println(prefix + " mBackGestureStarted=" + mBackGestureStarted); pw.println(prefix + " mPostCommitAnimationInProgress=" + mPostCommitAnimationInProgress); pw.println(prefix + " mShouldStartOnNextMoveEvent=" + mShouldStartOnNextMoveEvent); pw.println(prefix + " mPointerPilfered=" + mThresholdCrossed); pw.println(prefix + " mRequirePointerPilfer=" + mRequirePointerPilfer); pw.println(prefix + " mCurrentTracker state:"); mCurrentTracker.dump(pw, prefix + " "); pw.println(prefix + " mQueuedTracker state:"); mQueuedTracker.dump(pw, prefix + " "); } }