Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 14fa66be authored by Winson Chung's avatar Winson Chung Committed by Android (Google) Code Review
Browse files

Merge "Fix a few issues with the recents transition" into main

parents e3ac890d ce1681f1
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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)

+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
+86 −29
Original line number Diff line number Diff line
@@ -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;
@@ -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
@@ -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;

@@ -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,
@@ -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,
@@ -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,
@@ -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 */);
        }

@@ -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");
@@ -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
@@ -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()) {
@@ -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);
@@ -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;
@@ -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;
            }

@@ -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 */);
            }
+79 −6
Original line number Diff line number Diff line
@@ -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;
@@ -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();

@@ -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() {
@@ -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));
@@ -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();
    }

+3 −1
Original line number Diff line number Diff line
@@ -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) {