/* * 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 android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.Display.INVALID_DISPLAY; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION; import static android.window.BackEvent.EDGE_NONE; import static android.window.DesktopExperienceFlags.ENABLE_INDEPENDENT_BACK_IN_PROJECTED; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; import static com.android.systemui.Flags.predictiveBackDelayWmTransition; import static com.android.window.flags2.Flags.unifyBackNavigationTransition; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.IActivityTaskManager; import android.app.TaskInfo; import android.content.ComponentName; import android.content.Context; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; import android.hardware.input.InputManager; import android.hardware.input.KeyGestureEvent; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; import android.os.SystemClock; 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.SurfaceControl; 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.IBackAnimationHandoffHandler; import android.window.IBackAnimationRunner; import android.window.IOnBackInvokedCallback; import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowAnimationState; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.internal.util.LatencyTracker; import com.android.internal.view.AppearanceRegion; import com.android.systemui.animation.TransitionAnimator; import com.android.window.flags2.Flags; 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.TransitionUtil; 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 com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; /** * Controls the window animation run when a user initiates a back gesture. */ public class BackAnimationController implements RemoteCallable, ConfigurationChangeListener { private static final String TAG = "ShellBackPreview"; /** * 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; @ShellMainThread private final Handler mHandler; /** True when a back gesture is ongoing */ @VisibleForTesting public boolean mBackGestureStarted = false; /** Tracks if an uninterruptible animation is in progress */ private boolean mPostCommitAnimationInProgress = false; private boolean mRealCallbackInvoked = 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 boolean mReceivedNullNavigationInfo = false; private final IActivityTaskManager mActivityTaskManager; private final Context mContext; private final ShellController mShellController; private final ShellCommandHandler mShellCommandHandler; private final ShellExecutor mShellExecutor; private final WindowManager mWindowManager; private final Transitions mTransitions; private final InputManager mInputManager; @VisibleForTesting final BackTransitionHandler mBackTransitionHandler; @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 BackTransitionObserver mBackTransitionObserver = new BackTransitionObserver(); 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 @VisibleForTesting RemoteAnimationTarget[] mApps; @VisibleForTesting final RemoteCallback mNavigationObserver = new RemoteCallback( new RemoteCallback.OnResultListener() { @Override public void onResult(@Nullable Bundle result) { mShellExecutor.execute(() -> { if (mBackGestureStarted && result != null && result.getBoolean( BackNavigationInfo.KEY_TOUCH_GESTURE_TRANSFERRED)) { // Host app won't able to process motion event anymore, so pilfer // pointers anyway. if (mBackNavigationInfo != null) { mBackNavigationInfo.disableAppProgressGenerationAllowed(); } tryPilferPointers(); return; } 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); // Trigger close transition if necessary. mBackTransitionHandler.onAnimationFinished(); 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; private BackAnimation.TopUiRequest mRequestTopUiCallback; private final IBackAnimationHandoffHandler mHandoffHandler = new IBackAnimationHandoffHandler.Stub() { @Override public void handOffAnimation( RemoteAnimationTarget[] targets, WindowAnimationState[] states) { mBackTransitionHandler.handOffAnimation(targets, states); } }; public BackAnimationController( @NonNull ShellInit shellInit, @NonNull ShellController shellController, @NonNull @ShellMainThread ShellExecutor shellExecutor, Context context, @NonNull BackAnimationBackground backAnimationBackground, ShellBackAnimationRegistry shellBackAnimationRegistry, ShellCommandHandler shellCommandHandler, Transitions transitions, @ShellMainThread Handler handler) { this( shellInit, shellController, shellExecutor, ActivityTaskManager.getService(), context, backAnimationBackground, shellBackAnimationRegistry, shellCommandHandler, transitions, handler); } @VisibleForTesting BackAnimationController( @NonNull ShellInit shellInit, @NonNull ShellController shellController, @NonNull @ShellMainThread ShellExecutor shellExecutor, @NonNull IActivityTaskManager activityTaskManager, Context context, @NonNull BackAnimationBackground backAnimationBackground, ShellBackAnimationRegistry shellBackAnimationRegistry, ShellCommandHandler shellCommandHandler, Transitions transitions, @NonNull @ShellMainThread Handler handler) { mShellController = shellController; mShellExecutor = shellExecutor; mActivityTaskManager = activityTaskManager; mContext = context; mRequirePointerPilfer = context.getResources().getBoolean(R.bool.config_backAnimationRequiresPointerPilfer); shellInit.addInitCallback(this::onInit, this); mAnimationBackground = backAnimationBackground; mShellBackAnimationRegistry = shellBackAnimationRegistry; mLatencyTracker = LatencyTracker.getInstance(mContext); mShellCommandHandler = shellCommandHandler; mWindowManager = context.getSystemService(WindowManager.class); mInputManager = context.getSystemService(InputManager.class); mTransitions = transitions; mBackTransitionHandler = new BackTransitionHandler(mTransitions); mTransitions.addHandler(mBackTransitionHandler); mHandler = handler; mTransitions.registerObserver(mBackTransitionObserver); mBackTransitionObserver.setBackTransitionHandler(mBackTransitionHandler); updateTouchableArea(); } private void onInit() { createAdapter(); mShellController.addExternalInterface(IBackAnimation.DESCRIPTOR, this::createExternalInterface, this); mShellCommandHandler.addDumpCallback(this::dump, this); mShellController.addConfigurationChangeListener(this); registerBackGestureDelegate(); } 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, int keyAction, @BackEvent.SwipeEdge int swipeEdge, int displayId ) { mShellExecutor.execute( () -> onMotionEvent(touchX, touchY, keyAction, swipeEdge, displayId)); } @Override public void onThresholdCrossed() { if (predictiveBackDelayWmTransition()) { mShellExecutor.execute(BackAnimationController.this::onThresholdCrossed); } else { 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; }); } @Override public void setTopUiRequestCallback(TopUiRequest topUiRequest) { mShellExecutor.execute(() -> mRequestTopUiCallback = topUiRequest); } } private 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, mHandler))); } @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; BackTouchTracker activeTracker = getActiveTracker(); if (predictiveBackDelayWmTransition() && activeTracker != null && mActiveCallback == null && mBackGestureStarted) { startBackNavigation(activeTracker); } // There was no focus window when calling startBackNavigation, still pilfer pointers so // the next focus window won't receive motion events. if (mBackNavigationInfo == null && mReceivedNullNavigationInfo) { tryPilferPointers(); return; } // 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, int keyAction, @BackEvent.SwipeEdge int swipeEdge, int displayId) { if (ENABLE_INDEPENDENT_BACK_IN_PROJECTED.isTrue()) { mBackAnimationAdapter.mOriginDisplayId = displayId; } BackTouchTracker activeTouchTracker = getActiveTracker(); if (activeTouchTracker != null) { activeTouchTracker.update(touchX, touchY); } // 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; } else if (mBackGestureStarted && mCurrentTracker.isInInitialState() && mQueuedTracker.isInInitialState()) { ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Both touch trackers in initial state and mBackGestureStarted=true"); mBackGestureStarted = false; } if (keyAction == MotionEvent.ACTION_DOWN) { if (!mBackGestureStarted) { if (swipeEdge == EDGE_NONE) { // start animation immediately for non-gestural sources (without ACTION_MOVE // events) if (!predictiveBackDelayWmTransition()) { mThresholdCrossed = true; } mPointersPilfered = true; onGestureStarted(touchX, touchY, swipeEdge); if (predictiveBackDelayWmTransition()) { onThresholdCrossed(); } mShouldStartOnNextMoveEvent = false; } else { if (predictiveBackDelayWmTransition()) { onGestureStarted(touchX, touchY, swipeEdge); } else { 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(swipeEdge); } 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 (!predictiveBackDelayWmTransition()) { startBackNavigation(touchTracker); } } private void startBackNavigation(@NonNull BackTouchTracker touchTracker) { 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. return; } try { startLatencyTracking(); if (mBackAnimationAdapter != null && mShellBackAnimationRegistry.hasSupportedAnimatorsChanged()) { mBackAnimationAdapter.updateSupportedAnimators( mShellBackAnimationRegistry.getSupportedAnimators()); } mBackNavigationInfo = mActivityTaskManager.startBackNavigation( mNavigationObserver, mBackAnimationAdapter); 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."); mReceivedNullNavigationInfo = true; cancelLatencyTracking(); tryPilferPointers(); return; } final int backType = backNavigationInfo.getType(); final boolean shouldDispatchToAnimator = shouldDispatchToAnimator(); if (shouldDispatchToAnimator) { if (!mShellBackAnimationRegistry.startGesture(backType)) { mActiveCallback = null; } requestTopUi(true, backType); 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(@BackEvent.SwipeEdge int swipeEdge) { if (predictiveBackDelayWmTransition() && mCurrentTracker.isActive()) { mCurrentTracker.updateSwipeEdge(swipeEdge); } 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(int displayId) { ProtoLog.d(WM_SHELL_BACK_PREVIEW, "injectBackKey"); sendBackEvent(KeyEvent.ACTION_DOWN, displayId); sendBackEvent(KeyEvent.ACTION_UP, displayId); } @SuppressLint("MissingPermission") private void sendBackEvent(int action, int displayId) { 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); if (ENABLE_INDEPENDENT_BACK_IN_PROJECTED.isTrue()) { ev.setDisplayId(displayId); } 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 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); if (mBackTransitionHandler.canHandOffAnimation()) { callback.setHandoffHandler(mHandoffHandler); } else { callback.setHandoffHandler(null); } 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); } } /** * @return Latest task id which back gesture has occurred on it. */ public int getLatestTriggerBackTask() { return mBackTransitionObserver.mFocusedTaskId; } /** * 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 && !mRealCallbackInvoked) { final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); if (touchTracker.getTriggerBack()) { dispatchOnBackInvoked(callback); } else { tryDispatchOnBackCancelled(callback); } } mRealCallbackInvoked = false; 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); if (triggerBack) { mBackTransitionObserver.update(mBackNavigationInfo != null ? mBackNavigationInfo.getFocusedTaskId() : INVALID_TASK_ID); } // 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(mBackAnimationAdapter.mOriginDisplayId); } 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 core gesture is commit if (shouldTriggerCloseTransition()) { mBackTransitionHandler.mCloseTransitionRequested = true; final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback(); // invoked client side onBackInvoked dispatchOnBackInvoked(callback); mRealCallbackInvoked = true; } // start post animation dispatchOnBackInvoked(mActiveCallback); } else { tryDispatchOnBackCancelled(mActiveCallback); } } // Close window won't create any transition private boolean shouldTriggerCloseTransition() { if (mBackNavigationInfo == null) { return false; } int type = mBackNavigationInfo.getType(); return type == BackNavigationInfo.TYPE_RETURN_TO_HOME || type == BackNavigationInfo.TYPE_CROSS_TASK || type == BackNavigationInfo.TYPE_CROSS_ACTIVITY; } /** * 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(); mBackTransitionHandler.onAnimationFinished(); } /** * 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(mBackAnimationAdapter.mOriginDisplayId); 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; mOnBackStartDispatched = false; mThresholdCrossed = false; mPointersPilfered = false; mShellBackAnimationRegistry.resetDefaultCrossActivity(); cancelLatencyTracking(); mReceivedNullNavigationInfo = false; if (mBackNavigationInfo != null) { mPreviousNavigationType = mBackNavigationInfo.getType(); mBackNavigationInfo.onBackNavigationFinished(triggerBack); mBackNavigationInfo = null; requestTopUi(false, mPreviousNavigationType); } } 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) { BackMotionEvent startEvent = mCurrentTracker.createStartEvent( Flags.removeDepartTargetFromMotion() ? null : mApps[0]); dispatchOnBackStarted(mActiveCallback, startEvent); if (startEvent.getSwipeEdge() == EDGE_NONE) { // TODO(b/373544911): onBackStarted is dispatched here so that // WindowOnBackInvokedDispatcher knows about the back navigation and intercepts // touch events while it's active. It would be cleaner and safer to disable // multitouch altogether (same as in gesture-nav). dispatchOnBackStarted(mBackNavigationInfo.getOnBackInvokedCallback(), startEvent); } } } private void requestTopUi(boolean hasTopUi, int backType) { if (mRequestTopUiCallback != null && (backType == BackNavigationInfo.TYPE_CROSS_TASK || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY)) { mRequestTopUiCallback.requestTopUi(hasTopUi, TAG); } } /** * 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 kickStartAnimation() { 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 (mCurrentTracker.isFinished()) { // if the down -> up gesture happened before animation // start, we have to trigger the uninterruptible transition // to finish the back animation. startPostCommitAnimation(); } } private void createAdapter() { IBackAnimationRunner runner = new IBackAnimationRunner.Stub() { @Override public void onAnimationStart( RemoteAnimationTarget[] apps, IBinder token, IBackAnimationFinishedCallback finishedCallback) { mShellExecutor.execute( () -> { endLatencyTracking(); if (!validateAnimationTargets(apps)) { Log.e(TAG, "Invalid animation targets!"); return; } mBackAnimationFinishedCallback = finishedCallback; mApps = apps; // app only visible after transition ready, break for now. if (token != null) { return; } kickStartAnimation(); }); } @Override public void onAnimationCancelled() { mShellExecutor.execute( () -> { if (!mShellBackAnimationRegistry.cancel( mBackNavigationInfo != null ? mBackNavigationInfo.getType() : mPreviousNavigationType)) { return; } if (!mBackGestureStarted) { invokeOrCancelBack(mCurrentTracker); } }); } }; mBackAnimationAdapter = new BackAnimationAdapter(runner); } private void registerBackGestureDelegate() { mInputManager.registerKeyGestureEventHandler(List.of(KeyGestureEvent.KEY_GESTURE_TYPE_BACK), (event, focussedToken) -> { if (event.getKeyGestureType() == KeyGestureEvent.KEY_GESTURE_TYPE_BACK) { mShellExecutor.execute(() -> { if (mBackGestureStarted) { Log.w(TAG, "Back gesture is running, ignore request"); return; } onMotionEvent(0, 0, KeyEvent.ACTION_DOWN, EDGE_NONE, INVALID_DISPLAY); setTriggerBack(true); onMotionEvent(0, 0, KeyEvent.ACTION_UP, EDGE_NONE, INVALID_DISPLAY); }); } else { Log.w(TAG, "Unsupported gesture " + event + " received!"); } }); } /** * Description of current BackAnimationController state. */ private void dump(PrintWriter pw, String prefix) { pw.println(prefix + "BackAnimationController state:"); 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 + " "); } class BackTransitionHandler implements Transitions.TransitionHandler { private final Transitions mTransitions; Runnable mOnAnimationFinishCallback; boolean mCloseTransitionRequested; SurfaceControl.Transaction mFinishOpenTransaction; Transitions.TransitionFinishCallback mFinishOpenTransitionCallback; // The Transition to make behindActivity become visible IBinder mPrepareOpenTransition; // The Transition to make behindActivity become invisible, if prepare open exist and // animation is canceled, start a close prepare transition to finish the whole transition. IBinder mClosePrepareTransition; TransitionInfo mOpenTransitionInfo; Transitions.TransitionHandler mTakeoverHandler; BackTransitionHandler(Transitions transitions) { mTransitions = transitions; } void onAnimationFinished() { if (!mCloseTransitionRequested && mPrepareOpenTransition != null) { createClosePrepareTransition(); } if (mOnAnimationFinishCallback != null) { mOnAnimationFinishCallback.run(); mOnAnimationFinishCallback = null; } } private void applyFinishOpenTransition() { if (mFinishOpenTransaction != null) { final SurfaceControl.Transaction t = mFinishOpenTransaction; t.apply(); } if (mFinishOpenTransitionCallback != null) { final Transitions.TransitionFinishCallback callback = mFinishOpenTransitionCallback; callback.onTransitionFinished(null); } cleanUpInternalState(); } private void cleanUpInternalState() { mOpenTransitionInfo = null; mPrepareOpenTransition = null; mFinishOpenTransaction = null; mFinishOpenTransitionCallback = null; mTakeoverHandler = null; } private void applyAndFinish(@NonNull SurfaceControl.Transaction st, @NonNull SurfaceControl.Transaction ft, @NonNull Transitions.TransitionFinishCallback finishCallback) { applyFinishOpenTransition(); st.apply(); ft.apply(); finishCallback.onTransitionFinished(null); mCloseTransitionRequested = false; } @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction st, @NonNull SurfaceControl.Transaction ft, @NonNull Transitions.TransitionFinishCallback finishCallback) { // Both mShellExecutor and Transitions#mMainExecutor are ShellMainThread, so we don't // need to post to ShellExecutor when called. if (info.getType() == WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) { // only consume it if this transition hasn't being processed. if (mClosePrepareTransition != null) { mClosePrepareTransition = null; applyAndFinish(st, ft, finishCallback); return true; } return false; } if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION && isNotGestureBackTransition(info)) { return false; } if (shouldCancelAnimation(info)) { mPrepareOpenTransition = null; return false; } if (mApps == null || mApps.length == 0) { if (mCloseTransitionRequested) { // animation never start, consume directly applyAndFinish(st, ft, finishCallback); return true; } else if (mClosePrepareTransition == null && info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { // Gesture animation was cancelled before prepare transition ready, create // the close prepare transition createClosePrepareTransition(); } } if (handlePrepareTransition(transition, info, st, ft, finishCallback)) { if (checkTakeoverFlags()) { mTakeoverHandler = mTransitions.getHandlerForTakeover(transition, info); } kickStartAnimation(); return true; } return handleCloseTransition(info, st, ft, finishCallback); } private boolean canHandOffAnimation() { if (!checkTakeoverFlags()) { return false; } return mTakeoverHandler != null; } private void handOffAnimation( RemoteAnimationTarget[] targets, WindowAnimationState[] states) { if (!checkTakeoverFlags()) { ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Trying to hand off the animation, but the required flags are disabled."); return; } else if (mTakeoverHandler == null) { ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Missing takeover handler when trying to hand off animation."); return; } else if (targets.length != states.length) { ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Targets passed for takeover don't match the window states."); return; } // The states passed to this method are paired with the targets, but they need to be // paired with the changes inside the TransitionInfo. So for each change we find its // matching target, and leave the state for any change missing a matching target blank. WindowAnimationState[] updatedStates = new WindowAnimationState[mOpenTransitionInfo.getChanges().size()]; for (int i = 0; i < mOpenTransitionInfo.getChanges().size(); i++) { ActivityManager.RunningTaskInfo taskInfo = mOpenTransitionInfo.getChanges().get(i).getTaskInfo(); if (taskInfo == null) { continue; } for (int j = 0; j < targets.length; j++) { if (taskInfo.taskId == targets[j].taskId) { updatedStates[i] = states[j]; break; } } } mTakeoverHandler.takeOverAnimation( mPrepareOpenTransition, mOpenTransitionInfo, new SurfaceControl.Transaction(), mFinishOpenTransitionCallback, updatedStates); cleanUpInternalState(); } @Override public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, @Nullable SurfaceControl.Transaction finishTransaction) { if (transition == mClosePrepareTransition && aborted) { mClosePrepareTransition = null; applyFinishOpenTransition(); } else if (!aborted && unifyBackNavigationTransition()) { // Since the closing target participates in the predictive back transition, the // merged transition must be applied with the first transition to ensure a seamless // animation. if (mFinishOpenTransaction != null && finishTransaction != null) { mFinishOpenTransaction.merge(finishTransaction); } } } void createClosePrepareTransition() { if (mClosePrepareTransition != null) { Log.e(TAG, "Re-create close prepare transition"); return; } final WindowContainerTransaction wct = new WindowContainerTransaction(); wct.restoreBackNavi(); mClosePrepareTransition = mTransitions.startTransition( TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION, wct, mBackTransitionHandler); } private void mergePendingTransitions(TransitionInfo info) { if (mOpenTransitionInfo == null) { return; } // Copy initial changes to final transition final TransitionInfo init = mOpenTransitionInfo; // Find prepare open target boolean openShowWallpaper = false; final ArrayList openSurfaces = new ArrayList<>(); int tmpSize; for (int j = init.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change change = init.getChanges().get(j); if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED) && TransitionUtil.isOpeningMode(change.getMode())) { final ComponentName openComponent = findComponentName(change); final int openTaskId = findTaskId(change); final WindowContainerToken openToken = findToken(change); if (openComponent == null && openTaskId == INVALID_TASK_ID && openToken == null) { continue; } openSurfaces.add(change.getLeash()); if (change.hasFlags(FLAG_SHOW_WALLPAPER)) { openShowWallpaper = true; } } } if (openSurfaces.isEmpty()) { // This shouldn't happen, but if that happen, consume the initial transition anyway. Log.e(TAG, "Unable to merge following transition, cannot find the gesture " + "animated target from the open transition=" + mOpenTransitionInfo); mOpenTransitionInfo = null; return; } // Find first non-prepare open target boolean isOpen = false; tmpSize = info.getChanges().size(); for (int j = 0; j < tmpSize; ++j) { final TransitionInfo.Change change = info.getChanges().get(j); if (isOpenSurfaceMatched(openSurfaces, change)) { // This is original close target, potential be close, but cannot determine // from it. if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { isOpen = !TransitionUtil.isClosingMode(change.getMode()); } else { isOpen = TransitionUtil.isOpeningMode(change.getMode()); break; } } } if (!isOpen) { // Close transition, the transition info should be: // init info(open A & wallpaper) => init info(open A & change B & wallpaper) // current info(close B target) => current info(change A & close B) // remove init info(open/change A target & wallpaper) boolean moveToTop = false; boolean excludeOpenTarget = false; boolean mergePredictive = false; for (int j = info.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change change = info.getChanges().get(j); if (isOpenSurfaceMatched(openSurfaces, change)) { if (TransitionUtil.isClosingMode(change.getMode())) { excludeOpenTarget = true; } moveToTop = change.hasFlags(FLAG_MOVED_TO_TOP); info.getChanges().remove(j); } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER))) { info.getChanges().remove(j); } else if (!mergePredictive && TransitionUtil.isClosingMode(change.getMode())) { mergePredictive = true; } } // Ignore merge if there is no close target if (!info.getChanges().isEmpty() && mergePredictive) { tmpSize = init.getChanges().size(); for (int i = 0; i < tmpSize; ++i) { final TransitionInfo.Change change = init.getChanges().get(i); if (change.hasFlags(FLAG_IS_WALLPAPER)) { continue; } if (isOpenSurfaceMatched(openSurfaces, change)) { if (excludeOpenTarget) { // App has triggered another change during predictive back // transition, filter out predictive back target. continue; } if (moveToTop) { change.setFlags(change.getFlags() | FLAG_MOVED_TO_TOP); } } else if (Flags.unifyBackNavigationTransition() && change.hasFlags(FLAG_BACK_GESTURE_ANIMATED) && change.getMode() == TRANSIT_CHANGE && isCloseChangeExist(info, change)) { // This is the original top target, don't add it into current transition // if it is closing. continue; } info.getChanges().add(i, change); } } } else { // Open transition, the transition info should be: // init info(open A & wallpaper) // current info(open C target + close B target + close A & wallpaper) // If close target isn't back navigated, filter out close A & wallpaper because the // (open C + close B) pair didn't participant prepare close boolean nonBackOpen = false; boolean nonBackClose = false; tmpSize = info.getChanges().size(); for (int j = 0; j < tmpSize; ++j) { final TransitionInfo.Change change = info.getChanges().get(j); if (!change.hasFlags(FLAG_BACK_GESTURE_ANIMATED) && canBeTransitionTarget(change)) { final int mode = change.getMode(); nonBackOpen |= TransitionUtil.isOpeningMode(mode); nonBackClose |= TransitionUtil.isClosingMode(mode); } } if (nonBackClose && nonBackOpen) { for (int j = info.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change change = info.getChanges().get(j); if (isOpenSurfaceMatched(openSurfaces, change)) { info.getChanges().remove(j); } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER))) { info.getChanges().remove(j); } } } } ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation transition, merge pending " + "transitions result=%s", info); // Only handle one merge transition request. mOpenTransitionInfo = null; } @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (mClosePrepareTransition == transition) { mClosePrepareTransition = null; } // try to handle unexpected transition if (mOpenTransitionInfo != null) { mergePendingTransitions(info); } if (info.getType() == TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION && !mCloseTransitionRequested && info.getChanges().isEmpty() && mApps == null) { finishCallback.onTransitionFinished(null); startT.apply(); applyFinishOpenTransition(); return; } if (isNotGestureBackTransition(info) || shouldCancelAnimation(info) || !mCloseTransitionRequested) { if (mPrepareOpenTransition != null) { applyFinishOpenTransition(); } return; } // Handle the commit transition if this handler is running the open transition. finishCallback.onTransitionFinished(null); startT.apply(); if (mCloseTransitionRequested) { if (mApps == null || mApps.length == 0) { // animation was done applyFinishOpenTransition(); mCloseTransitionRequested = false; } else { // we are animating, wait until animation finish mOnAnimationFinishCallback = () -> { applyFinishOpenTransition(); mCloseTransitionRequested = false; }; } } } // Cancel close animation if something happen unexpected, let another handler to handle private boolean shouldCancelAnimation(@NonNull TransitionInfo info) { final boolean noCloseAllowed = !mCloseTransitionRequested && info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; boolean unableToHandle = false; boolean filterTargets = false; for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change c = info.getChanges().get(i); final boolean backGestureAnimated = c.hasFlags(FLAG_BACK_GESTURE_ANIMATED); if (!backGestureAnimated && !c.hasFlags(FLAG_IS_WALLPAPER)) { // something we cannot handle? unableToHandle = true; filterTargets = true; } else if (noCloseAllowed && backGestureAnimated && TransitionUtil.isClosingMode(c.getMode())) { // Prepare back navigation shouldn't contain close change, unless top app // request close. unableToHandle = true; } } if (!unableToHandle) { return false; } if (!filterTargets) { return true; } if (TransitionUtil.isOpeningType(info.getType()) || TransitionUtil.isClosingType(info.getType())) { boolean removeWallpaper = false; for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change c = info.getChanges().get(i); // filter out opening target, keep original closing target in this transition if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) && TransitionUtil.isOpeningMode(c.getMode())) { info.getChanges().remove(i); removeWallpaper |= c.hasFlags(FLAG_SHOW_WALLPAPER); } } if (removeWallpaper) { for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change c = info.getChanges().get(i); if (c.hasFlags(FLAG_IS_WALLPAPER)) { info.getChanges().remove(i); } } } } return true; } /** * Check whether this transition is prepare for predictive back animation, which could * happen when core make an activity become visible. */ @VisibleForTesting boolean handlePrepareTransition(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction st, @NonNull SurfaceControl.Transaction ft, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { return false; } // Must have open target, must not have close target. if (hasAnimationInMode(info, TransitionUtil::isClosingMode) || !hasAnimationInMode(info, TransitionUtil::isOpeningMode)) { return false; } SurfaceControl openingLeash = null; SurfaceControl closingLeash = null; if (mApps != null) { for (int i = mApps.length - 1; i >= 0; --i) { if (mApps[i].mode == MODE_OPENING) { openingLeash = mApps[i].leash; } else if (mApps[i].mode == MODE_CLOSING) { closingLeash = mApps[i].leash; } } } if (openingLeash != null && closingLeash != null) { int rootIdx = -1; for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change c = info.getChanges().get(i); if (TransitionUtil.isOpeningMode(c.getMode())) { final Point offset = c.getEndRelOffset(); st.setPosition(c.getLeash(), offset.x, offset.y); st.reparent(c.getLeash(), openingLeash); st.setAlpha(c.getLeash(), 1.0f); rootIdx = TransitionUtil.rootIndexFor(c, info); } else if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) && c.getMode() == TRANSIT_CHANGE) { st.reparent(c.getLeash(), closingLeash); } } // The root leash and the leash of opening target should actually in the same level, // but since the root leash is created after opening target, it will have higher // layer in surface flinger. Move the root leash to lower level, so it won't affect // the playing animation. if (rootIdx >= 0 && info.getRootCount() > 0) { st.setLayer(info.getRoot(rootIdx).getLeash(), -1); } } st.apply(); // In case other transition handler took the handleRequest before this class. mPrepareOpenTransition = transition; mFinishOpenTransaction = ft; mFinishOpenTransitionCallback = finishCallback; mOpenTransitionInfo = info; return true; } /** * Check whether this transition is triggered from back gesture commitment. * Reparent the transition targets to animation leashes, so the animation won't be broken. */ @VisibleForTesting boolean handleCloseTransition(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction st, @NonNull SurfaceControl.Transaction ft, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (!mCloseTransitionRequested) { return false; } // must have close target if (!hasAnimationInMode(info, TransitionUtil::isClosingMode)) { return false; } if (mApps == null) { // animation is done applyAndFinish(st, ft, finishCallback); return true; } SurfaceControl openingLeash = null; SurfaceControl closingLeash = null; for (int i = mApps.length - 1; i >= 0; --i) { if (mApps[i].mode == MODE_OPENING) { openingLeash = mApps[i].leash; } if (mApps[i].mode == MODE_CLOSING) { closingLeash = mApps[i].leash; } } if (openingLeash != null && closingLeash != null) { for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change c = info.getChanges().get(i); if (c.hasFlags(FLAG_IS_WALLPAPER)) { st.setAlpha(c.getLeash(), 1.0f); continue; } if (TransitionUtil.isOpeningMode(c.getMode())) { final Point offset = c.getEndRelOffset(); st.setPosition(c.getLeash(), offset.x, offset.y); st.reparent(c.getLeash(), openingLeash); st.setAlpha(c.getLeash(), 1.0f); } else if (TransitionUtil.isClosingMode(c.getMode())) { st.reparent(c.getLeash(), closingLeash); } } } st.apply(); // mApps must exists mOnAnimationFinishCallback = () -> { ft.apply(); finishCallback.onTransitionFinished(null); mCloseTransitionRequested = false; }; return true; } @Nullable @Override public WindowContainerTransaction handleRequest( @NonNull IBinder transition, @NonNull TransitionRequestInfo request) { final int type = request.getType(); if (type == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) { mPrepareOpenTransition = transition; return new WindowContainerTransaction(); } if (type == WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) { return new WindowContainerTransaction(); } if (TransitionUtil.isClosingType(request.getType()) && mCloseTransitionRequested) { return new WindowContainerTransaction(); } return null; } private static boolean checkTakeoverFlags() { return TransitionAnimator.Companion.longLivedReturnAnimationsEnabled() && Flags.unifyBackNavigationTransition(); } } private static boolean isNotGestureBackTransition(@NonNull TransitionInfo info) { return !hasAnimationInMode(info, TransitionUtil::isOpenOrCloseMode); } private static boolean hasAnimationInMode(@NonNull TransitionInfo info, Predicate mode) { for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change c = info.getChanges().get(i); if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) && mode.test(c.getMode())) { return true; } } return false; } private static WindowContainerToken findToken(TransitionInfo.Change change) { return change.getContainer(); } private static ComponentName findComponentName(TransitionInfo.Change change) { final ComponentName componentName = change.getActivityComponent(); if (componentName != null) { return componentName; } final TaskInfo taskInfo = change.getTaskInfo(); if (taskInfo != null) { return taskInfo.topActivity; } return null; } private static int findTaskId(TransitionInfo.Change change) { final TaskInfo taskInfo = change.getTaskInfo(); if (taskInfo != null) { return taskInfo.taskId; } return INVALID_TASK_ID; } static boolean isOpenSurfaceMatched(@NonNull ArrayList openSurfaces, TransitionInfo.Change change) { for (int i = openSurfaces.size() - 1; i >= 0; --i) { if (openSurfaces.get(i).isSameSurface(change.getLeash())) { return true; } } return false; } private static boolean canBeTransitionTarget(TransitionInfo.Change change) { return findComponentName(change) != null || findTaskId(change) != INVALID_TASK_ID; } private static boolean isCloseChangeExist(TransitionInfo info, TransitionInfo.Change change) { for (int j = info.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change current = info.getChanges().get(j); if (TransitionUtil.isClosingMode(current.getMode()) && change.getLeash().isSameSurface(current.getLeash())) { return true; } } return false; } // Record the latest back gesture happen on which task. static class BackTransitionObserver implements Transitions.TransitionObserver { int mFocusedTaskId = INVALID_TASK_ID; IBinder mFocusTaskMonitorToken; private BackTransitionHandler mBackTransitionHandler; void setBackTransitionHandler(BackTransitionHandler handler) { mBackTransitionHandler = handler; } void update(int focusedTaskId) { mFocusedTaskId = focusedTaskId; } @Override public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction) { if (mFocusedTaskId == INVALID_TASK_ID) { return; } for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change c = info.getChanges().get(i); if (c.getTaskInfo() != null && c.getTaskInfo().taskId == mFocusedTaskId) { mFocusTaskMonitorToken = transition; break; } } // Transition happen but the task isn't involved, reset. if (mFocusTaskMonitorToken == null) { mFocusedTaskId = INVALID_TASK_ID; } } @Override public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { if (mFocusTaskMonitorToken == merged) { mFocusTaskMonitorToken = playing; } if (mBackTransitionHandler.mClosePrepareTransition == merged) { mBackTransitionHandler.mClosePrepareTransition = null; } } @Override public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) { if (mFocusTaskMonitorToken == transition) { mFocusedTaskId = INVALID_TASK_ID; } if (mBackTransitionHandler.mClosePrepareTransition == transition) { mBackTransitionHandler.mClosePrepareTransition = null; } } } }