From 89e82322d2c46961221e9a25940ad151e2223b1d Mon Sep 17 00:00:00 2001 From: Evan Rosky Date: Thu, 16 Feb 2023 20:09:19 -0800 Subject: [PATCH] Stabilize overview/quick-switch transitions When done in rapid succession or at some bad timings, it was possible to get stuck either with touch being ignored or with some windows incorrectly invisible. This is fixed primarily via tracking of task state in the recents transition animator to account for many changes being merged into one animation. This includes a task-switch being interrupted by a re-entering gesture as well as re-opening of the live-tile due to unsolicited intents (eg. notification permission activity). Without detailed tracking, the WMCore state and the recents animator state would fall out of sync. Second, make sure that visibility-changes resulting from `setFocusedTask` calls are done in a transition. This API was just missed before so Shell wouldn't be notified of some visibility changes. Bug: 269691848 Bug: 269691226 Test: existing tests pass. Also manually do rapid quick-switches. Change-Id: I0b465b11ea316fc670a1ba121e61535e22108be5 --- .../shared/system/RemoteTransitionCompat.java | 185 ++++++++++++------ .../server/wm/ActivityTaskManagerService.java | 20 +- 2 files changed, 144 insertions(+), 61 deletions(-) diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java index 2a37bd370b7ca..80b97588279f8 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java @@ -36,7 +36,6 @@ import android.os.IBinder; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; -import android.util.SparseArray; import android.view.IRecentsAnimationController; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; @@ -106,12 +105,24 @@ public class RemoteTransitionCompat { private RecentsAnimationListener mListener = null; private RecentsAnimationControllerCompat mWrapped = null; private IRemoteTransitionFinishedCallback mFinishCB = null; + + /** + * List of tasks that we are switching away from via this transition. Upon finish, these + * pausing tasks will become invisible. + * These need to be ordered since the order must be restored if there is no task-switch. + */ private ArrayList mPausingTasks = null; + + /** + * List of tasks that we are switching to. Upon finish, these will remain visible and + * on top. + */ + private ArrayList mOpeningTasks = null; + private WindowContainerToken mPipTask = null; private WindowContainerToken mRecentsTask = null; private int mRecentsTaskId = 0; private TransitionInfo mInfo = null; - private ArrayList mOpeningLeashes = null; private boolean mOpeningSeparateHome = false; private ArrayMap mLeashMap = null; private PictureInPictureSurfaceTransaction mPipTransaction = null; @@ -120,6 +131,15 @@ public class RemoteTransitionCompat { private RemoteAnimationTarget[] mAppearedTargets; private boolean mWillFinishToHome = false; + /** The animation is idle, waiting for the user to choose a task to switch to. */ + private static final int STATE_NORMAL = 0; + + /** The user chose a new task to switch to and the animation is animating to it. */ + private static final int STATE_NEW_TASK = 1; + + /** The latest state that the recents animation is operating in. */ + private int mState = STATE_NORMAL; + void start(RecentsAnimationControllerCompat wrapped, RecentsAnimationListener listener, IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, IRemoteTransitionFinishedCallback finishedCallback) { @@ -132,12 +152,14 @@ public class RemoteTransitionCompat { mInfo = info; mFinishCB = finishedCallback; mPausingTasks = new ArrayList<>(); + mOpeningTasks = new ArrayList<>(); mPipTask = null; mRecentsTask = null; mRecentsTaskId = -1; mLeashMap = new ArrayMap<>(); mTransition = transition; mKeyguardLocked = (info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0; + mState = STATE_NORMAL; final ArrayList apps = new ArrayList<>(); final ArrayList wallpapers = new ArrayList<>(); @@ -178,6 +200,9 @@ public class RemoteTransitionCompat { } else if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) { mRecentsTask = taskInfo.token; mRecentsTaskId = taskInfo.taskId; + } else if (change.getMode() == TRANSIT_OPEN + || change.getMode() == TRANSIT_TO_FRONT) { + mOpeningTasks.add(new TaskState(change, target.leash)); } } } @@ -189,34 +214,41 @@ public class RemoteTransitionCompat { @SuppressLint("NewApi") boolean merge(TransitionInfo info, SurfaceControl.Transaction t) { - SparseArray openingTasks = null; + ArrayList openingTasks = null; + ArrayList closingTasks = null; mAppearedTargets = null; - boolean foundHomeOpening = false; + mOpeningSeparateHome = false; + TransitionInfo.Change recentsOpening = null; boolean foundRecentsClosing = false; boolean hasChangingApp = false; - for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final RemoteAnimationTargetCompat.LeafTaskFilter leafTaskFilter = + new RemoteAnimationTargetCompat.LeafTaskFilter(); + for (int i = 0; i < info.getChanges().size(); ++i) { final TransitionInfo.Change change = info.getChanges().get(i); + final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); + final boolean isLeafTask = leafTaskFilter.test(change); if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) { - final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); - if (taskInfo != null) { + if (mRecentsTask.equals(change.getContainer())) { + recentsOpening = change; + } else if (isLeafTask) { if (taskInfo.topActivityType == ACTIVITY_TYPE_HOME) { // This is usually a 3p launcher - foundHomeOpening = true; + mOpeningSeparateHome = true; } if (openingTasks == null) { - openingTasks = new SparseArray<>(); + openingTasks = new ArrayList<>(); } - if (taskInfo.hasParentTask()) { - // Collects opening leaf tasks only since Launcher monitors leaf task - // ids to perform recents animation. - openingTasks.remove(taskInfo.parentTaskId); - } - openingTasks.put(taskInfo.taskId, change); + openingTasks.add(change); } } else if (change.getMode() == TRANSIT_CLOSE || change.getMode() == TRANSIT_TO_BACK) { if (mRecentsTask.equals(change.getContainer())) { foundRecentsClosing = true; + } else if (isLeafTask) { + if (closingTasks == null) { + closingTasks = new ArrayList<>(); + } + closingTasks.add(change); } } else if (change.getMode() == TRANSIT_CHANGE) { hasChangingApp = true; @@ -234,45 +266,72 @@ public class RemoteTransitionCompat { } return false; } - if (openingTasks == null) return false; - int pauseMatches = 0; - if (!foundHomeOpening) { + if (recentsOpening != null) { + // the recents task re-appeared. This happens if the user gestures before the + // task-switch (NEW_TASK) animation finishes. + if (mState == STATE_NORMAL) { + Log.e(TAG, "Returning to recents while recents is already idle."); + } + if (closingTasks == null || closingTasks.size() == 0) { + Log.e(TAG, "Returning to recents without closing any opening tasks."); + } + // Setup may hide it initially since it doesn't know that overview was still active. + t.show(recentsOpening.getLeash()); + t.setAlpha(recentsOpening.getLeash(), 1.f); + mState = STATE_NORMAL; + } + boolean didMergeThings = false; + if (closingTasks != null) { + // Cancelling a task-switch. Move the tasks back to mPausing from mOpening + for (int i = 0; i < closingTasks.size(); ++i) { + final TransitionInfo.Change change = closingTasks.get(i); + int openingIdx = TaskState.indexOf(mOpeningTasks, change); + if (openingIdx < 0) { + Log.e(TAG, "Back to existing recents animation from an unrecognized " + + "task: " + change.getTaskInfo().taskId); + continue; + } + mPausingTasks.add(mOpeningTasks.remove(openingIdx)); + didMergeThings = true; + } + } + if (openingTasks != null && openingTasks.size() > 0) { + // Switching to some new tasks, add to mOpening and remove from mPausing. Also, + // enter NEW_TASK state since this will start the switch-to animation. + final int layer = mInfo.getChanges().size() * 3; + final RemoteAnimationTarget[] targets = + new RemoteAnimationTarget[openingTasks.size()]; for (int i = 0; i < openingTasks.size(); ++i) { - if (TaskState.indexOf(mPausingTasks, openingTasks.valueAt(i)) >= 0) { - ++pauseMatches; + final TransitionInfo.Change change = openingTasks.get(i); + int pausingIdx = TaskState.indexOf(mPausingTasks, change); + if (pausingIdx >= 0) { + // Something is showing/opening a previously-pausing app. + targets[i] = newTarget(change, layer, mPausingTasks.get(pausingIdx).mLeash); + mOpeningTasks.add(mPausingTasks.remove(pausingIdx)); + // Setup hides opening tasks initially, so make it visible again (since we + // are already showing it). + t.show(change.getLeash()); + t.setAlpha(change.getLeash(), 1.f); + } else { + // We are receiving new opening tasks, so convert to onTasksAppeared. + targets[i] = newTarget(change, layer, info, t, mLeashMap); + t.reparent(targets[i].leash, mInfo.getRootLeash()); + t.setLayer(targets[i].leash, layer); + mOpeningTasks.add(new TaskState(change, targets[i].leash)); } } + didMergeThings = true; + mState = STATE_NEW_TASK; + mAppearedTargets = targets; } - if (pauseMatches > 0) { - if (pauseMatches != mPausingTasks.size()) { - // We are not really "returning" properly... something went wrong. - throw new IllegalStateException("\"Concelling\" a recents transitions by " - + "unpausing " + pauseMatches + " apps after pausing " - + mPausingTasks.size() + " apps."); - } - // In this case, we are "returning" to an already running app, so just consume - // the merge and do nothing. - info.releaseAllSurfaces(); - t.close(); - return true; - } - final int layer = mInfo.getChanges().size() * 3; - mOpeningLeashes = new ArrayList<>(); - mOpeningSeparateHome = foundHomeOpening; - final RemoteAnimationTarget[] targets = - new RemoteAnimationTarget[openingTasks.size()]; - for (int i = 0; i < openingTasks.size(); ++i) { - final TransitionInfo.Change change = openingTasks.valueAt(i); - mOpeningLeashes.add(change.getLeash()); - // We are receiving new opening tasks, so convert to onTasksAppeared. - targets[i] = newTarget(change, layer, info, t, mLeashMap); - t.reparent(targets[i].leash, mInfo.getRootLeash()); - t.setLayer(targets[i].leash, layer); + if (!didMergeThings) { + // Didn't recognize anything in incoming transition so don't merge it. + Log.w(TAG, "Don't know how to merge this transition."); + return false; } t.apply(); // not using the incoming anim-only surfaces info.releaseAnimSurfaces(); - mAppearedTargets = targets; return true; } @@ -300,6 +359,8 @@ public class RemoteTransitionCompat { if (enabled) { // transient launches don't receive focus automatically. Since we are taking over // the gesture now, take focus explicitly. + // This also moves recents back to top if the user gestured before a switch + // animation finished. try { ActivityTaskManager.getService().setFocusedTask(mRecentsTaskId); } catch (RemoteException e) { @@ -336,8 +397,8 @@ public class RemoteTransitionCompat { if (toHome) wct.reorder(mRecentsTask, true /* toTop */); else wct.restoreTransientOrder(mRecentsTask); } - if (!toHome && !mWillFinishToHome && mPausingTasks != null && mOpeningLeashes == null) { - // The gesture went back to opening the app rather than continuing with + if (!toHome && !mWillFinishToHome && mPausingTasks != null && mState == STATE_NORMAL) { + // The gesture is returning to the pausing-task(s) rather than continuing with // recents, so end the transition by moving the app back to the top (and also // re-showing it's task). for (int i = mPausingTasks.size() - 1; i >= 0; --i) { @@ -349,25 +410,28 @@ public class RemoteTransitionCompat { wct.restoreTransientOrder(mRecentsTask); } } else if (toHome && mOpeningSeparateHome && mPausingTasks != null) { - // Special situaition where 3p launcher was changed during recents (this happens + // Special situation where 3p launcher was changed during recents (this happens // during tapltests...). Here we get both "return to home" AND "home opening". - // This is basically going home, but we have to restore recents order and also - // treat the home "pausing" task properly. - for (int i = mPausingTasks.size() - 1; i >= 0; --i) { - final TaskState state = mPausingTasks.get(i); + // This is basically going home, but we have to restore the recents and home order. + for (int i = 0; i < mOpeningTasks.size(); ++i) { + final TaskState state = mOpeningTasks.get(i); if (state.mTaskInfo.topActivityType == ACTIVITY_TYPE_HOME) { - // Treat as opening (see above) + // Make sure it is on top. wct.reorder(state.mToken, true /* onTop */); - t.show(state.mTaskSurface); - } else { - // Treat as hiding (see below) - t.hide(state.mTaskSurface); } + t.show(state.mTaskSurface); + } + for (int i = mPausingTasks.size() - 1; i >= 0; --i) { + t.hide(mPausingTasks.get(i).mTaskSurface); } if (!mKeyguardLocked && mRecentsTask != null) { wct.restoreTransientOrder(mRecentsTask); } } else { + // The general case: committing to recents, going home, or switching tasks. + for (int i = 0; i < mOpeningTasks.size(); ++i) { + t.show(mOpeningTasks.get(i).mTaskSurface); + } for (int i = 0; i < mPausingTasks.size(); ++i) { if (!sendUserLeaveHint) { // This means recents is not *actually* finishing, so of course we gotta @@ -391,7 +455,7 @@ public class RemoteTransitionCompat { try { mFinishCB.onTransitionFinished(wct.isEmpty() ? null : wct, t); } catch (RemoteException e) { - Log.e("RemoteTransitionCompat", "Failed to call animation finish callback", e); + Log.e(TAG, "Failed to call animation finish callback", e); t.apply(); } // Only release the non-local created surface references. The animator is responsible @@ -402,12 +466,13 @@ public class RemoteTransitionCompat { mListener = null; mFinishCB = null; mPausingTasks = null; + mOpeningTasks = null; mAppearedTargets = null; mInfo = null; - mOpeningLeashes = null; mOpeningSeparateHome = false; mLeashMap = null; mTransition = null; + mState = STATE_NORMAL; } @Override public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) { diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 8d671f768e87a..78da015402102 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -65,6 +65,7 @@ import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_PIP; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_CONFIGURATION; @@ -2034,7 +2035,20 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { return; } - if (r.moveFocusableActivityToTop("setFocusedTask")) { + final Transition transition = (getTransitionController().isCollecting() + || !getTransitionController().isShellTransitionsEnabled()) ? null + : getTransitionController().createTransition(TRANSIT_TO_FRONT); + if (transition != null) { + // Set ready before doing anything. If order does change, then that will set it unready + // so that we wait for the new lifecycles to complete. + transition.setReady(task, true /* ready */); + } + final boolean movedToTop = r.moveFocusableActivityToTop("setFocusedTask"); + if (movedToTop) { + if (transition != null) { + getTransitionController().requestStartTransition( + transition, null /* startTask */, null /* remote */, null /* display */); + } mRootWindowContainer.resumeFocusedTasksTopActivities(); } else if (touchedActivity != null && touchedActivity.isFocusable()) { final TaskFragment parent = touchedActivity.getTaskFragment(); @@ -2046,6 +2060,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { true /* updateInputWindows */); } } + if (transition != null && !movedToTop) { + // No order changes and focus-changes, alone, aren't captured in transitions. + transition.abort(); + } } @Override