Loading libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md +1 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ particular order): 4) [Threading model in the Shell](threading.md) 5) [Making changes in the Shell](changes.md) 6) [Extending the Shell for Products/OEMs](extending.md) 6) [Shell transitions](transitions.md) 7) [Debugging in the Shell](debugging.md) 8) [Testing in the Shell](testing.md) Loading libs/WindowManager/Shell/src/com/android/wm/shell/docs/transitions.md 0 → 100644 +118 −0 Original line number Diff line number Diff line # Shell transitions [Back to home](README.md) --- ## General General guides for using Shell Transitions can be found here: - [Shell transitions animation guide](http://go/shell-transit-anim) - [Hitchhiker's guide to transitions](http://go/transitions-book) ## Transient-launch transitions <span style="color:orange">Use with care!</span> Transient-launch transitions are a way to handle non-atomic (ie. gestural) transitions by allowing WM Core to put participating activities into a transiently visible or hidden state for the duration of the animation and adding the ability to cancel the transition. For example, if you are launching an activity normally, WM Core will be updated at the start of the animation which includes pausing the previous activity and resuming the next activity (and subsequently the transition will reconcile that state via an animation). If you are transiently launching an activity though, WM Core will ensure that both the leaving activity and the incoming activity will be RESUMED for the duration of the transition duration. In addition, WM Core will track the position of the transient-launch activity in the window hierarchy prior to the launch, and allow Shell to restore it to that position if the transitions needs to be canceled. Starting a transient-launch transition can be done via the activity options (since the activity may not have been started yet): ```kotlin val opts = ActivityOptions.makeBasic().setTransientLaunch() val wct = WindowContainerTransaction() wct.sendPendingIntent(pendingIntent, new Intent(), opts.toBundle()) transitions.startTransition(TRANSIT_OPEN, wct, ...) ``` And restoring the transient order via a WCT: ```kotlin val wct = WindowContainerTransaction() wct.restoreTransientOrder(transientLaunchContainerToken) transitions.startTransition(TRANSIT_RESTORE, wct, ...) ``` ### <span style="color:orange">Considerations</span> Usage of transient-launch transitions should be done with consideration, there are a few gotchas that might result in subtle and hard-to-reproduce issues. #### Understanding the flow When starting a transient-launch transition, there are several possible outcomes: 1) The transition finishes as normal: The user is committing the transition to the state requested at the start of the transition. In such cases, you can simply finish the transition and the states of the transiently shown/hidden activities will be updated to match the original state that a non-transient transition would have (ie. closing activities will be stopped). 2) The transition is interrupted: A change in the system results in the window hierarchy changing in a way which may or may not affect the transient-launch activity. eg. We transiently-launch home from app A, but then app B launches. In this case, WM attempts to create a new transition reflecting the window hierarchy changes (ie. if B occludes Home in the above example, then the transition will have Home TO_BACK, and B TO_FRONT). At this point, the transition handler can choose to merge the incoming transition or not (to queue it after this current transition). Take note of the next section for concerns re. bookend transitions. 3) The transition is canceled: The user is canceling the transition to the previous state. In such cases, you need to store the `WindowContainerToken` for the task associated with the transient-launch activity, and restore the transient order via the `WindowContainerTransaction` API above. In some cases, if anything has been reordered since (ie. due to other merged transitions), then you may also want to use `WindowContainerTransaction#reorder()` to place all the relevant containers to their original order (provided via the change-order in the initial launch transition). #### Finishing the transient-launch transition When restoring the transient order in the 3rd flow above, it is recommended to do it in a new transition and <span style="color:orange">**not**</span> via the WindowContainerTransaction in `TransitionFinishCallback#onTransitionFinished()` provided when starting the transition. Changes to the window hierarchy via the finish transaction are not applied in sync with other transitions that are collecting and aplying, and are also not observable in Shell in any way. Starting a new transition instead ensures both. (The finish transaction can still be used if there are non-transition affecting properties (ie. container properties) that need to be updated as a part of finishing the transient-launch transition). So the general idea is when restoring is: 1) Start transient-launch transition START_T 2) ... 3) All done, start bookend transition END_T 4) Handler receives END_T, merges it and then finishes START_T In practice it's not quite that simple, due to the ordering of transitions and extra care must be taken when using a new transition to prevent deadlocking when merging transitions. When a new transition arrives while a transient-launch transition is playing, the handler can choose to handle/merge the transition into the ongoing one, or skip merging to queue it up to be played after. In the above flow, we can see how this might result in a deadlock: Queueing END during merge: 1) Start transient-launch transition START_T 2) ... 3) Incoming transition OTHER_T, choose to cancel START_T -> start bookend transition END_T, but don't merge OTHER_T 3) Waiting for END_T... <span style="color:red">Deadlock!</span> Interrupt while pending END: 1) Start transient-launch transition START_T 2) ... 3) All done, start bookend transition END_T 3) Incoming transition OTHER_T occurs before END_T, but don't merge OTHER_T 3) Waiting for END_T... <span style="color:red">Deadlock!</span> This means that when using transient-launch transitions with a bookend transition <span style="color:orange">requires</span> you to handle any incoming transitions if the bookend is ever queued (or already posted) after it. You can do so by preempting the bookend transition (finishing the transient-launch transition), or handling the merge of the new transition (so it doesn't queue). No newline at end of file libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +86 −29 Original line number Diff line number Diff line Loading @@ -47,6 +47,7 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.IApplicationThread; import android.app.PendingIntent; import android.app.WindowConfiguration; import android.content.Context; import android.content.Intent; import android.graphics.Color; Loading Loading @@ -320,7 +321,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, "RecentsTransitionHandler.mergeAnimation: no controller found"); return; } controller.merge(info, startT, finishT, mergeTarget, finishCallback); controller.merge(info, startT, finishT, finishCallback); } @Override Loading Loading @@ -408,7 +409,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, // next called. private Pair<int[], TaskSnapshot[]> mPendingPauseSnapshotsForCancel; // Used to track a pending finish transition // Used to track a pending finish transition, this is only non-null if // enableRecentsBookendTransition() is enabled private IBinder mPendingFinishTransition; private IResultReceiver mPendingRunnerFinishCb; Loading Loading @@ -917,7 +919,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, */ @SuppressLint("NewApi") void merge(TransitionInfo info, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, IBinder mergeTarget, SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCallback) { if (mFinishCB == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, Loading @@ -927,17 +929,26 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, return; } if (Flags.enableRecentsBookendTransition() && info.getType() == TRANSIT_END_RECENTS_TRANSITION && mergeTarget == mTransition) { // This is a pending finish, so merge the end transition to trigger completing the // cleanup of the recents transition if (Flags.enableRecentsBookendTransition()) { if (info.getType() == TRANSIT_END_RECENTS_TRANSITION) { // This is a pending finish, so merge the end transition to trigger completing // the cleanup of the recents transition ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: TRANSIT_END_RECENTS_TRANSITION", mInstanceId); finishCallback.onTransitionFinished(null /* wct */); consumeMerge(info, startT, finishT, finishCallback); return; } else if (mPendingFinishTransition != null) { // This transition is interrupting a pending finish that was already sent, so // pre-empt the pending finish transition since the state has already changed // in the core ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: Awaiting TRANSIT_END_RECENTS_TRANSITION", mInstanceId); onFinishInner(null /* wct */); return; } } if (info.getType() == TRANSIT_SLEEP) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, Loading Loading @@ -1210,16 +1221,12 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } return; } // At this point, we are accepting the merge. startT.apply(); // Since we're accepting the merge, update the finish transaction so that changes via // that transaction will be applied on top of those of the merged transitions mFinishTransaction = finishT; consumeMerge(info, startT, finishT, finishCallback); // Notify Launcher of the new opening tasks if necessary boolean passTransitionInfo = ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue(); if (!passTransitionInfo) { // not using the incoming anim-only surfaces info.releaseAnimSurfaces(); } if (appearedTargets != null) { try { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, Loading @@ -1229,6 +1236,27 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, Slog.e(TAG, "Error sending appeared tasks to recents animation", e); } } } /** * Consumes the merge of the other given transition. */ private void consumeMerge(TransitionInfo info, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: consuming merge", mInstanceId); startT.apply(); // Since we're accepting the merge, update the finish transaction so that changes via // that transaction will be applied on top of those of the merged transitions mFinishTransaction = finishT; boolean passTransitionInfo = ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue(); if (!passTransitionInfo) { // not using the incoming anim-only surfaces info.releaseAnimSurfaces(); } finishCallback.onTransitionFinished(null /* wct */); } Loading Loading @@ -1346,9 +1374,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, final SurfaceControl.Transaction t = mFinishTransaction; final WindowContainerTransaction wct = new WindowContainerTransaction(); // The following code must set this if it is changing anything in core that might affect // transitions as a part of finishing the recents transition boolean requiresBookendTransition = false; if (mKeyguardLocked && mRecentsTask != null) { if (toHome) wct.reorder(mRecentsTask, true /* toTop */); else wct.restoreTransientOrder(mRecentsTask); // We are manipulating the window hierarchy, which should only be done with the // bookend transition requiresBookendTransition = true; } if (returningToApp) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " returning to app"); Loading @@ -1365,6 +1400,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, if (!mKeyguardLocked && mRecentsTask != null) { wct.restoreTransientOrder(mRecentsTask); } // We are manipulating the window hierarchy, which should only be done with the // bookend transition requiresBookendTransition = true; } else if (toHome && mOpeningSeparateHome && mPausingTasks != null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " 3p launching home"); // Special situation where 3p launcher was changed during recents (this happens Loading @@ -1384,6 +1422,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, if (!mKeyguardLocked && mRecentsTask != null) { wct.restoreTransientOrder(mRecentsTask); } // We are manipulating the window hierarchy, which should only be done with the // bookend transition requiresBookendTransition = true; } else { if (mPausingSeparateHome) { if (mOpeningTasks.isEmpty()) { Loading Loading @@ -1484,6 +1525,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, if (Flags.enableRecentsBookendTransition()) { if (!wct.isEmpty()) { if (requiresBookendTransition) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.finishInner: " + "Queuing TRANSIT_END_RECENTS_TRANSITION", mInstanceId); Loading @@ -1491,6 +1533,13 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mPendingFinishTransition = mTransitions.startTransition( TRANSIT_END_RECENTS_TRANSITION, wct, new PendingFinishTransitionHandler()); } else { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.finishInner: Non-transition affecting wct", mInstanceId); mPendingRunnerFinishCb = runnerFinishCb; onFinishInner(wct); } } else { // If there's no work to do, just go ahead and clean up mPendingRunnerFinishCb = runnerFinishCb; Loading Loading @@ -1631,6 +1680,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] PendingFinishTransitionHandler.startAnimation: " + "Started pending finish transition", mInstanceId); return false; } Loading @@ -1644,10 +1696,15 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, @Override public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, @Nullable SurfaceControl.Transaction finishTransaction) { if (mPendingFinishTransition == null) { // The cleanup was pre-empted by an earlier transition, nothing there is nothing // to do here return; } // Once we have merged (or not if the WCT didn't result in any changes), then we can // run the pending finish logic ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.onTransitionConsumed: " "[%d] PendingFinishTransitionHandler.onTransitionConsumed: " + "Consumed pending finish transition", mInstanceId); onFinishInner(null /* wct */); } Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +79 −6 Original line number Diff line number Diff line Loading @@ -17,16 +17,20 @@ package com.android.wm.shell.recents; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX; import static com.android.wm.shell.Flags.FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED; import static com.android.wm.shell.transition.Transitions.TRANSIT_END_RECENTS_TRANSITION; import static com.android.wm.shell.transition.Transitions.TRANSIT_START_RECENTS_TRANSITION; import static com.google.common.truth.Truth.assertThat; Loading Loading @@ -308,8 +312,7 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { mRecentsTransitionHandler.findController(transition).merge( mergeTransitionInfo, new StubTransaction(), finishT, transition, new StubTransaction(), mock(Transitions.TransitionFinishCallback.class)); mMainExecutor.flushAll(); Loading @@ -317,6 +320,69 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { /* appearedTargets= */ any(), eq(mergeTransitionInfo)); } @Test @EnableFlags(FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION) public void testMerge_consumeBookendTransition() throws Exception { // Start and finish the transition final IRecentsAnimationRunner animationRunner = mock(IRecentsAnimationRunner.class); final IBinder transition = startRecentsTransition(/* synthetic= */ false, animationRunner); mRecentsTransitionHandler.startAnimation( transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), mock(Transitions.TransitionFinishCallback.class)); mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, false /* sendUserLeaveHint */, mock(IResultReceiver.class)); mMainExecutor.flushAll(); // Merge the bookend transition TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_END_RECENTS_TRANSITION) .addChange(TRANSIT_OPEN, new TestRunningTaskInfoBuilder().build()) .build(); SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); Transitions.TransitionFinishCallback finishCallback = mock(Transitions.TransitionFinishCallback.class); mRecentsTransitionHandler.findController(transition).merge( mergeTransitionInfo, new StubTransaction(), finishT, finishCallback); mMainExecutor.flushAll(); // Verify that we've merged verify(finishCallback).onTransitionFinished(any()); } @Test @EnableFlags(FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION) public void testMerge_pendingBookendTransition_mergesTransition() throws Exception { // Start and finish the transition final IRecentsAnimationRunner animationRunner = mock(IRecentsAnimationRunner.class); final IBinder transition = startRecentsTransition(/* synthetic= */ false, animationRunner); mRecentsTransitionHandler.startAnimation( transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), mock(Transitions.TransitionFinishCallback.class)); mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, false /* sendUserLeaveHint */, mock(IResultReceiver.class)); mMainExecutor.flushAll(); // Merge a new transition while we have a pending finish TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN, new TestRunningTaskInfoBuilder().build()) .build(); SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); Transitions.TransitionFinishCallback finishCallback = mock(Transitions.TransitionFinishCallback.class); mRecentsTransitionHandler.findController(transition).merge( mergeTransitionInfo, new StubTransaction(), finishT, finishCallback); mMainExecutor.flushAll(); // Verify that we've cleaned up the original transition assertNull(mRecentsTransitionHandler.findController(transition)); } @Test @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) public void testMergeAndFinish_openingFreeformTasks_setsCornerRadius() { Loading @@ -336,7 +402,6 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { mergeTransitionInfo, new StubTransaction(), finishT, transition, mock(Transitions.TransitionFinishCallback.class)); mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, false /* sendUserLeaveHint */, mock(IResultReceiver.class)); Loading Loading @@ -385,15 +450,23 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { } private TransitionInfo createTransitionInfo() { final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() final ActivityManager.RunningTaskInfo homeTask = new TestRunningTaskInfoBuilder() .setTopActivityType(ACTIVITY_TYPE_HOME) .build(); final ActivityManager.RunningTaskInfo appTask = new TestRunningTaskInfoBuilder() .setTopActivityType(ACTIVITY_TYPE_STANDARD) .build(); final TransitionInfo.Change homeChange = new TransitionInfo.Change( task.token, new SurfaceControl()); homeTask.token, new SurfaceControl()); homeChange.setMode(TRANSIT_TO_FRONT); homeChange.setTaskInfo(task); homeChange.setTaskInfo(homeTask); final TransitionInfo.Change appChange = new TransitionInfo.Change( appTask.token, new SurfaceControl()); appChange.setMode(TRANSIT_TO_FRONT); appChange.setTaskInfo(appTask); return new TransitionInfoBuilder(TRANSIT_START_RECENTS_TRANSITION) .addChange(homeChange) .addChange(appChange) .build(); } Loading services/core/java/com/android/server/wm/TaskFragment.java +3 −1 Original line number Diff line number Diff line Loading @@ -1889,10 +1889,12 @@ class TaskFragment extends WindowContainer<WindowContainer> { // Even if the transient activity is occluded, defer pausing (addToStopping will still // be called) it until the transient transition is done. So the current resuming // activity won't need to wait for additional pause complete. ProtoLog.d(WM_DEBUG_STATES, "startPausing: Skipping pause for transient " + "resumed activity=%s", mResumedActivity); return false; } ProtoLog.d(WM_DEBUG_STATES, "startPausing: taskFrag =%s " + "mResumedActivity=%s", this, ProtoLog.d(WM_DEBUG_STATES, "startPausing: taskFrag=%s mResumedActivity=%s", this, mResumedActivity); if (mPausingActivity != null) { Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/docs/README.md +1 −0 Original line number Diff line number Diff line Loading @@ -9,6 +9,7 @@ particular order): 4) [Threading model in the Shell](threading.md) 5) [Making changes in the Shell](changes.md) 6) [Extending the Shell for Products/OEMs](extending.md) 6) [Shell transitions](transitions.md) 7) [Debugging in the Shell](debugging.md) 8) [Testing in the Shell](testing.md) Loading
libs/WindowManager/Shell/src/com/android/wm/shell/docs/transitions.md 0 → 100644 +118 −0 Original line number Diff line number Diff line # Shell transitions [Back to home](README.md) --- ## General General guides for using Shell Transitions can be found here: - [Shell transitions animation guide](http://go/shell-transit-anim) - [Hitchhiker's guide to transitions](http://go/transitions-book) ## Transient-launch transitions <span style="color:orange">Use with care!</span> Transient-launch transitions are a way to handle non-atomic (ie. gestural) transitions by allowing WM Core to put participating activities into a transiently visible or hidden state for the duration of the animation and adding the ability to cancel the transition. For example, if you are launching an activity normally, WM Core will be updated at the start of the animation which includes pausing the previous activity and resuming the next activity (and subsequently the transition will reconcile that state via an animation). If you are transiently launching an activity though, WM Core will ensure that both the leaving activity and the incoming activity will be RESUMED for the duration of the transition duration. In addition, WM Core will track the position of the transient-launch activity in the window hierarchy prior to the launch, and allow Shell to restore it to that position if the transitions needs to be canceled. Starting a transient-launch transition can be done via the activity options (since the activity may not have been started yet): ```kotlin val opts = ActivityOptions.makeBasic().setTransientLaunch() val wct = WindowContainerTransaction() wct.sendPendingIntent(pendingIntent, new Intent(), opts.toBundle()) transitions.startTransition(TRANSIT_OPEN, wct, ...) ``` And restoring the transient order via a WCT: ```kotlin val wct = WindowContainerTransaction() wct.restoreTransientOrder(transientLaunchContainerToken) transitions.startTransition(TRANSIT_RESTORE, wct, ...) ``` ### <span style="color:orange">Considerations</span> Usage of transient-launch transitions should be done with consideration, there are a few gotchas that might result in subtle and hard-to-reproduce issues. #### Understanding the flow When starting a transient-launch transition, there are several possible outcomes: 1) The transition finishes as normal: The user is committing the transition to the state requested at the start of the transition. In such cases, you can simply finish the transition and the states of the transiently shown/hidden activities will be updated to match the original state that a non-transient transition would have (ie. closing activities will be stopped). 2) The transition is interrupted: A change in the system results in the window hierarchy changing in a way which may or may not affect the transient-launch activity. eg. We transiently-launch home from app A, but then app B launches. In this case, WM attempts to create a new transition reflecting the window hierarchy changes (ie. if B occludes Home in the above example, then the transition will have Home TO_BACK, and B TO_FRONT). At this point, the transition handler can choose to merge the incoming transition or not (to queue it after this current transition). Take note of the next section for concerns re. bookend transitions. 3) The transition is canceled: The user is canceling the transition to the previous state. In such cases, you need to store the `WindowContainerToken` for the task associated with the transient-launch activity, and restore the transient order via the `WindowContainerTransaction` API above. In some cases, if anything has been reordered since (ie. due to other merged transitions), then you may also want to use `WindowContainerTransaction#reorder()` to place all the relevant containers to their original order (provided via the change-order in the initial launch transition). #### Finishing the transient-launch transition When restoring the transient order in the 3rd flow above, it is recommended to do it in a new transition and <span style="color:orange">**not**</span> via the WindowContainerTransaction in `TransitionFinishCallback#onTransitionFinished()` provided when starting the transition. Changes to the window hierarchy via the finish transaction are not applied in sync with other transitions that are collecting and aplying, and are also not observable in Shell in any way. Starting a new transition instead ensures both. (The finish transaction can still be used if there are non-transition affecting properties (ie. container properties) that need to be updated as a part of finishing the transient-launch transition). So the general idea is when restoring is: 1) Start transient-launch transition START_T 2) ... 3) All done, start bookend transition END_T 4) Handler receives END_T, merges it and then finishes START_T In practice it's not quite that simple, due to the ordering of transitions and extra care must be taken when using a new transition to prevent deadlocking when merging transitions. When a new transition arrives while a transient-launch transition is playing, the handler can choose to handle/merge the transition into the ongoing one, or skip merging to queue it up to be played after. In the above flow, we can see how this might result in a deadlock: Queueing END during merge: 1) Start transient-launch transition START_T 2) ... 3) Incoming transition OTHER_T, choose to cancel START_T -> start bookend transition END_T, but don't merge OTHER_T 3) Waiting for END_T... <span style="color:red">Deadlock!</span> Interrupt while pending END: 1) Start transient-launch transition START_T 2) ... 3) All done, start bookend transition END_T 3) Incoming transition OTHER_T occurs before END_T, but don't merge OTHER_T 3) Waiting for END_T... <span style="color:red">Deadlock!</span> This means that when using transient-launch transitions with a bookend transition <span style="color:orange">requires</span> you to handle any incoming transitions if the bookend is ever queued (or already posted) after it. You can do so by preempting the bookend transition (finishing the transient-launch transition), or handling the merge of the new transition (so it doesn't queue). No newline at end of file
libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +86 −29 Original line number Diff line number Diff line Loading @@ -47,6 +47,7 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.IApplicationThread; import android.app.PendingIntent; import android.app.WindowConfiguration; import android.content.Context; import android.content.Intent; import android.graphics.Color; Loading Loading @@ -320,7 +321,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, "RecentsTransitionHandler.mergeAnimation: no controller found"); return; } controller.merge(info, startT, finishT, mergeTarget, finishCallback); controller.merge(info, startT, finishT, finishCallback); } @Override Loading Loading @@ -408,7 +409,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, // next called. private Pair<int[], TaskSnapshot[]> mPendingPauseSnapshotsForCancel; // Used to track a pending finish transition // Used to track a pending finish transition, this is only non-null if // enableRecentsBookendTransition() is enabled private IBinder mPendingFinishTransition; private IResultReceiver mPendingRunnerFinishCb; Loading Loading @@ -917,7 +919,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, */ @SuppressLint("NewApi") void merge(TransitionInfo info, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, IBinder mergeTarget, SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCallback) { if (mFinishCB == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, Loading @@ -927,17 +929,26 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, return; } if (Flags.enableRecentsBookendTransition() && info.getType() == TRANSIT_END_RECENTS_TRANSITION && mergeTarget == mTransition) { // This is a pending finish, so merge the end transition to trigger completing the // cleanup of the recents transition if (Flags.enableRecentsBookendTransition()) { if (info.getType() == TRANSIT_END_RECENTS_TRANSITION) { // This is a pending finish, so merge the end transition to trigger completing // the cleanup of the recents transition ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: TRANSIT_END_RECENTS_TRANSITION", mInstanceId); finishCallback.onTransitionFinished(null /* wct */); consumeMerge(info, startT, finishT, finishCallback); return; } else if (mPendingFinishTransition != null) { // This transition is interrupting a pending finish that was already sent, so // pre-empt the pending finish transition since the state has already changed // in the core ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: Awaiting TRANSIT_END_RECENTS_TRANSITION", mInstanceId); onFinishInner(null /* wct */); return; } } if (info.getType() == TRANSIT_SLEEP) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, Loading Loading @@ -1210,16 +1221,12 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } return; } // At this point, we are accepting the merge. startT.apply(); // Since we're accepting the merge, update the finish transaction so that changes via // that transaction will be applied on top of those of the merged transitions mFinishTransaction = finishT; consumeMerge(info, startT, finishT, finishCallback); // Notify Launcher of the new opening tasks if necessary boolean passTransitionInfo = ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue(); if (!passTransitionInfo) { // not using the incoming anim-only surfaces info.releaseAnimSurfaces(); } if (appearedTargets != null) { try { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, Loading @@ -1229,6 +1236,27 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, Slog.e(TAG, "Error sending appeared tasks to recents animation", e); } } } /** * Consumes the merge of the other given transition. */ private void consumeMerge(TransitionInfo info, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: consuming merge", mInstanceId); startT.apply(); // Since we're accepting the merge, update the finish transaction so that changes via // that transaction will be applied on top of those of the merged transitions mFinishTransaction = finishT; boolean passTransitionInfo = ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue(); if (!passTransitionInfo) { // not using the incoming anim-only surfaces info.releaseAnimSurfaces(); } finishCallback.onTransitionFinished(null /* wct */); } Loading Loading @@ -1346,9 +1374,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, final SurfaceControl.Transaction t = mFinishTransaction; final WindowContainerTransaction wct = new WindowContainerTransaction(); // The following code must set this if it is changing anything in core that might affect // transitions as a part of finishing the recents transition boolean requiresBookendTransition = false; if (mKeyguardLocked && mRecentsTask != null) { if (toHome) wct.reorder(mRecentsTask, true /* toTop */); else wct.restoreTransientOrder(mRecentsTask); // We are manipulating the window hierarchy, which should only be done with the // bookend transition requiresBookendTransition = true; } if (returningToApp) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " returning to app"); Loading @@ -1365,6 +1400,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, if (!mKeyguardLocked && mRecentsTask != null) { wct.restoreTransientOrder(mRecentsTask); } // We are manipulating the window hierarchy, which should only be done with the // bookend transition requiresBookendTransition = true; } else if (toHome && mOpeningSeparateHome && mPausingTasks != null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " 3p launching home"); // Special situation where 3p launcher was changed during recents (this happens Loading @@ -1384,6 +1422,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, if (!mKeyguardLocked && mRecentsTask != null) { wct.restoreTransientOrder(mRecentsTask); } // We are manipulating the window hierarchy, which should only be done with the // bookend transition requiresBookendTransition = true; } else { if (mPausingSeparateHome) { if (mOpeningTasks.isEmpty()) { Loading Loading @@ -1484,6 +1525,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, if (Flags.enableRecentsBookendTransition()) { if (!wct.isEmpty()) { if (requiresBookendTransition) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.finishInner: " + "Queuing TRANSIT_END_RECENTS_TRANSITION", mInstanceId); Loading @@ -1491,6 +1533,13 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mPendingFinishTransition = mTransitions.startTransition( TRANSIT_END_RECENTS_TRANSITION, wct, new PendingFinishTransitionHandler()); } else { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.finishInner: Non-transition affecting wct", mInstanceId); mPendingRunnerFinishCb = runnerFinishCb; onFinishInner(wct); } } else { // If there's no work to do, just go ahead and clean up mPendingRunnerFinishCb = runnerFinishCb; Loading Loading @@ -1631,6 +1680,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] PendingFinishTransitionHandler.startAnimation: " + "Started pending finish transition", mInstanceId); return false; } Loading @@ -1644,10 +1696,15 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, @Override public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, @Nullable SurfaceControl.Transaction finishTransaction) { if (mPendingFinishTransition == null) { // The cleanup was pre-empted by an earlier transition, nothing there is nothing // to do here return; } // Once we have merged (or not if the WCT didn't result in any changes), then we can // run the pending finish logic ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.onTransitionConsumed: " "[%d] PendingFinishTransitionHandler.onTransitionConsumed: " + "Consumed pending finish transition", mInstanceId); onFinishInner(null /* wct */); } Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +79 −6 Original line number Diff line number Diff line Loading @@ -17,16 +17,20 @@ package com.android.wm.shell.recents; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX; import static com.android.wm.shell.Flags.FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED; import static com.android.wm.shell.transition.Transitions.TRANSIT_END_RECENTS_TRANSITION; import static com.android.wm.shell.transition.Transitions.TRANSIT_START_RECENTS_TRANSITION; import static com.google.common.truth.Truth.assertThat; Loading Loading @@ -308,8 +312,7 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { mRecentsTransitionHandler.findController(transition).merge( mergeTransitionInfo, new StubTransaction(), finishT, transition, new StubTransaction(), mock(Transitions.TransitionFinishCallback.class)); mMainExecutor.flushAll(); Loading @@ -317,6 +320,69 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { /* appearedTargets= */ any(), eq(mergeTransitionInfo)); } @Test @EnableFlags(FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION) public void testMerge_consumeBookendTransition() throws Exception { // Start and finish the transition final IRecentsAnimationRunner animationRunner = mock(IRecentsAnimationRunner.class); final IBinder transition = startRecentsTransition(/* synthetic= */ false, animationRunner); mRecentsTransitionHandler.startAnimation( transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), mock(Transitions.TransitionFinishCallback.class)); mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, false /* sendUserLeaveHint */, mock(IResultReceiver.class)); mMainExecutor.flushAll(); // Merge the bookend transition TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_END_RECENTS_TRANSITION) .addChange(TRANSIT_OPEN, new TestRunningTaskInfoBuilder().build()) .build(); SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); Transitions.TransitionFinishCallback finishCallback = mock(Transitions.TransitionFinishCallback.class); mRecentsTransitionHandler.findController(transition).merge( mergeTransitionInfo, new StubTransaction(), finishT, finishCallback); mMainExecutor.flushAll(); // Verify that we've merged verify(finishCallback).onTransitionFinished(any()); } @Test @EnableFlags(FLAG_ENABLE_RECENTS_BOOKEND_TRANSITION) public void testMerge_pendingBookendTransition_mergesTransition() throws Exception { // Start and finish the transition final IRecentsAnimationRunner animationRunner = mock(IRecentsAnimationRunner.class); final IBinder transition = startRecentsTransition(/* synthetic= */ false, animationRunner); mRecentsTransitionHandler.startAnimation( transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), mock(Transitions.TransitionFinishCallback.class)); mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, false /* sendUserLeaveHint */, mock(IResultReceiver.class)); mMainExecutor.flushAll(); // Merge a new transition while we have a pending finish TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN, new TestRunningTaskInfoBuilder().build()) .build(); SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); Transitions.TransitionFinishCallback finishCallback = mock(Transitions.TransitionFinishCallback.class); mRecentsTransitionHandler.findController(transition).merge( mergeTransitionInfo, new StubTransaction(), finishT, finishCallback); mMainExecutor.flushAll(); // Verify that we've cleaned up the original transition assertNull(mRecentsTransitionHandler.findController(transition)); } @Test @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) public void testMergeAndFinish_openingFreeformTasks_setsCornerRadius() { Loading @@ -336,7 +402,6 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { mergeTransitionInfo, new StubTransaction(), finishT, transition, mock(Transitions.TransitionFinishCallback.class)); mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, false /* sendUserLeaveHint */, mock(IResultReceiver.class)); Loading Loading @@ -385,15 +450,23 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { } private TransitionInfo createTransitionInfo() { final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() final ActivityManager.RunningTaskInfo homeTask = new TestRunningTaskInfoBuilder() .setTopActivityType(ACTIVITY_TYPE_HOME) .build(); final ActivityManager.RunningTaskInfo appTask = new TestRunningTaskInfoBuilder() .setTopActivityType(ACTIVITY_TYPE_STANDARD) .build(); final TransitionInfo.Change homeChange = new TransitionInfo.Change( task.token, new SurfaceControl()); homeTask.token, new SurfaceControl()); homeChange.setMode(TRANSIT_TO_FRONT); homeChange.setTaskInfo(task); homeChange.setTaskInfo(homeTask); final TransitionInfo.Change appChange = new TransitionInfo.Change( appTask.token, new SurfaceControl()); appChange.setMode(TRANSIT_TO_FRONT); appChange.setTaskInfo(appTask); return new TransitionInfoBuilder(TRANSIT_START_RECENTS_TRANSITION) .addChange(homeChange) .addChange(appChange) .build(); } Loading
services/core/java/com/android/server/wm/TaskFragment.java +3 −1 Original line number Diff line number Diff line Loading @@ -1889,10 +1889,12 @@ class TaskFragment extends WindowContainer<WindowContainer> { // Even if the transient activity is occluded, defer pausing (addToStopping will still // be called) it until the transient transition is done. So the current resuming // activity won't need to wait for additional pause complete. ProtoLog.d(WM_DEBUG_STATES, "startPausing: Skipping pause for transient " + "resumed activity=%s", mResumedActivity); return false; } ProtoLog.d(WM_DEBUG_STATES, "startPausing: taskFrag =%s " + "mResumedActivity=%s", this, ProtoLog.d(WM_DEBUG_STATES, "startPausing: taskFrag=%s mResumedActivity=%s", this, mResumedActivity); if (mPausingActivity != null) { Loading