Merge "Move recentsAnimationController to shell" into udc-dev
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index 3d5230d..57b5b8f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -82,6 +82,7 @@
 import com.android.wm.shell.pip.phone.PipTouchHandler;
 import com.android.wm.shell.recents.RecentTasks;
 import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
 import com.android.wm.shell.splitscreen.SplitScreen;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.startingsurface.StartingSurface;
@@ -520,6 +521,9 @@
                         desktopModeTaskRepository, mainExecutor));
     }
 
+    @BindsOptionalOf
+    abstract RecentsTransitionHandler optionalRecentsTransitionHandler();
+
     //
     // Shell transitions
     //
@@ -803,6 +807,7 @@
             Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
             Optional<FreeformComponents> freeformComponents,
             Optional<RecentTasksController> recentTasksOptional,
+            Optional<RecentsTransitionHandler> recentsTransitionHandlerOptional,
             Optional<OneHandedController> oneHandedControllerOptional,
             Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
             Optional<ActivityEmbeddingController> activityEmbeddingOptional,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 7a83d10..cc0da28 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -83,6 +83,7 @@
 import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 import com.android.wm.shell.pip.phone.PipTouchHandler;
 import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
@@ -528,9 +529,20 @@
             ShellInit shellInit,
             Optional<SplitScreenController> splitScreenOptional,
             Optional<PipTouchHandler> pipTouchHandlerOptional,
+            Optional<RecentsTransitionHandler> recentsTransitionHandler,
             Transitions transitions) {
         return new DefaultMixedHandler(shellInit, transitions, splitScreenOptional,
-                pipTouchHandlerOptional);
+                pipTouchHandlerOptional, recentsTransitionHandler);
+    }
+
+    @WMSingleton
+    @Provides
+    static RecentsTransitionHandler provideRecentsTransitionHandler(
+            ShellInit shellInit,
+            Transitions transitions,
+            Optional<RecentTasksController> recentTasksController) {
+        return new RecentsTransitionHandler(shellInit, transitions,
+                recentTasksController.orElse(null));
     }
 
     //
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
index 1a6c1d6..4048c5b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasks.aidl
@@ -17,6 +17,11 @@
 package com.android.wm.shell.recents;
 
 import android.app.ActivityManager.RunningTaskInfo;
+import android.app.IApplicationThread;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+import android.view.IRecentsAnimationRunner;
 
 import com.android.wm.shell.recents.IRecentTasksListener;
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
@@ -45,4 +50,10 @@
      * Gets the set of running tasks.
      */
     RunningTaskInfo[] getRunningTasks(int maxNum) = 4;
+
+    /**
+     * Starts a recents transition.
+     */
+    oneway void startRecentsTransition(in PendingIntent intent, in Intent fillIn, in Bundle options,
+                    IApplicationThread appThread, IRecentsAnimationRunner listener) = 5;
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 0d9faa3..c5bfd87 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -24,13 +24,18 @@
 
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
+import android.app.IApplicationThread;
+import android.app.PendingIntent;
 import android.app.TaskInfo;
 import android.content.ComponentName;
 import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
 import android.os.RemoteException;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseIntArray;
+import android.view.IRecentsAnimationRunner;
 
 import androidx.annotation.BinderThread;
 import androidx.annotation.NonNull;
@@ -79,6 +84,7 @@
     private final TaskStackListenerImpl mTaskStackListener;
     private final RecentTasksImpl mImpl = new RecentTasksImpl();
     private final ActivityTaskManager mActivityTaskManager;
+    private RecentsTransitionHandler mTransitionHandler = null;
     private IRecentTasksListener mListener;
     private final boolean mIsDesktopMode;
 
@@ -150,6 +156,10 @@
         mDesktopModeTaskRepository.ifPresent(it -> it.addActiveTaskListener(this));
     }
 
+    void setTransitionHandler(RecentsTransitionHandler handler) {
+        mTransitionHandler = handler;
+    }
+
     /**
      * Adds a split pair. This call does not validate the taskIds, only that they are not the same.
      */
@@ -492,5 +502,18 @@
                     true /* blocking */);
             return tasks[0];
         }
+
+        @Override
+        public void startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options,
+                IApplicationThread appThread, IRecentsAnimationRunner listener) {
+            if (mController.mTransitionHandler == null) {
+                Slog.e(TAG, "Used shell-transitions startRecentsTransition without"
+                        + " shell-transitions");
+                return;
+            }
+            executeRemoteCallWithTaskPermission(mController, "startRecentsTransition",
+                    (controller) -> controller.mTransitionHandler.startRecentsTransition(
+                            intent, fillIn, options, appThread, listener));
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
new file mode 100644
index 0000000..da8c805
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java
@@ -0,0 +1,759 @@
+/*
+ * Copyright (C) 2023 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.recents;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.view.WindowManager.TRANSIT_CHANGE;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED;
+import static android.view.WindowManager.TRANSIT_SLEEP;
+import static android.view.WindowManager.TRANSIT_TO_FRONT;
+
+import android.annotation.Nullable;
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.app.ActivityTaskManager;
+import android.app.IApplicationThread;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.ArrayMap;
+import android.util.Slog;
+import android.view.IRecentsAnimationController;
+import android.view.IRecentsAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.window.PictureInPictureSurfaceTransaction;
+import android.window.TaskSnapshot;
+import android.window.TransitionInfo;
+import android.window.TransitionRequestInfo;
+import android.window.WindowContainerToken;
+import android.window.WindowContainerTransaction;
+
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+import com.android.wm.shell.util.TransitionUtil;
+
+import java.util.ArrayList;
+
+/**
+ * Handles the Recents (overview) animation. Only one of these can run at a time. A recents
+ * transition must be created via {@link #startRecentsTransition}. Anything else will be ignored.
+ */
+public class RecentsTransitionHandler implements Transitions.TransitionHandler {
+    private static final String TAG = "RecentsTransitionHandler";
+
+    private final Transitions mTransitions;
+    private final ShellExecutor mExecutor;
+    private IApplicationThread mAnimApp = null;
+    private final ArrayList<RecentsController> mControllers = new ArrayList<>();
+
+    /**
+     * List of other handlers which might need to mix recents with other things. These are checked
+     * in the order they are added. Ideally there should only be one.
+     */
+    private final ArrayList<RecentsMixedHandler> mMixers = new ArrayList<>();
+
+    public RecentsTransitionHandler(ShellInit shellInit, Transitions transitions,
+            @Nullable RecentTasksController recentTasksController) {
+        mTransitions = transitions;
+        mExecutor = transitions.getMainExecutor();
+        if (!Transitions.ENABLE_SHELL_TRANSITIONS) return;
+        if (recentTasksController == null) return;
+        shellInit.addInitCallback(() -> {
+            recentTasksController.setTransitionHandler(this);
+            transitions.addHandler(this);
+        }, this);
+    }
+
+    /** Register a mixer handler. {@see RecentsMixedHandler}*/
+    public void addMixer(RecentsMixedHandler mixer) {
+        mMixers.add(mixer);
+    }
+
+    /** Unregister a Mixed Handler */
+    public void removeMixer(RecentsMixedHandler mixer) {
+        mMixers.remove(mixer);
+    }
+
+    void startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options,
+            IApplicationThread appThread, IRecentsAnimationRunner listener) {
+        // only care about latest one.
+        mAnimApp = appThread;
+        WindowContainerTransaction wct = new WindowContainerTransaction();
+        wct.sendPendingIntent(intent, fillIn, options);
+        final RecentsController controller = new RecentsController(listener);
+        RecentsMixedHandler mixer = null;
+        Transitions.TransitionHandler mixedHandler = null;
+        for (int i = 0; i < mMixers.size(); ++i) {
+            mixedHandler = mMixers.get(i).handleRecentsRequest(wct);
+            if (mixedHandler != null) {
+                mixer = mMixers.get(i);
+                break;
+            }
+        }
+        final IBinder transition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct,
+                mixedHandler == null ? this : mixedHandler);
+        if (mixer != null) {
+            mixer.setRecentsTransition(transition);
+        }
+        if (transition == null) {
+            controller.cancel();
+            return;
+        }
+        controller.setTransition(transition);
+        mControllers.add(controller);
+    }
+
+    @Override
+    public WindowContainerTransaction handleRequest(IBinder transition,
+            TransitionRequestInfo request) {
+        // do not directly handle requests. Only entry point should be via startRecentsTransition
+        return null;
+    }
+
+    private int findController(IBinder transition) {
+        for (int i = mControllers.size() - 1; i >= 0; --i) {
+            if (mControllers.get(i).mTransition == transition) return i;
+        }
+        return -1;
+    }
+
+    @Override
+    public boolean startAnimation(IBinder transition, TransitionInfo info,
+            SurfaceControl.Transaction startTransaction,
+            SurfaceControl.Transaction finishTransaction,
+            Transitions.TransitionFinishCallback finishCallback) {
+        final int controllerIdx = findController(transition);
+        if (controllerIdx < 0) return false;
+        final RecentsController controller = mControllers.get(controllerIdx);
+        Transitions.setRunningRemoteTransitionDelegate(mAnimApp);
+        mAnimApp = null;
+        if (!controller.start(info, startTransaction, finishTransaction, finishCallback)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void mergeAnimation(IBinder transition, TransitionInfo info,
+            SurfaceControl.Transaction t, IBinder mergeTarget,
+            Transitions.TransitionFinishCallback finishCallback) {
+        final int targetIdx = findController(mergeTarget);
+        if (targetIdx < 0) return;
+        final RecentsController controller = mControllers.get(targetIdx);
+        controller.merge(info, t, finishCallback);
+    }
+
+    @Override
+    public void onTransitionConsumed(IBinder transition, boolean aborted,
+            SurfaceControl.Transaction finishTransaction) {
+        final int idx = findController(transition);
+        if (idx < 0) return;
+        mControllers.get(idx).cancel();
+    }
+
+    /** There is only one of these and it gets reset on finish. */
+    private class RecentsController extends IRecentsAnimationController.Stub {
+        private IRecentsAnimationRunner mListener;
+        private IBinder.DeathRecipient mDeathHandler;
+        private Transitions.TransitionFinishCallback mFinishCB = null;
+        private SurfaceControl.Transaction mFinishTransaction = 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<TaskState> mPausingTasks = null;
+
+        /**
+         * List of tasks that we are switching to. Upon finish, these will remain visible and
+         * on top.
+         */
+        private ArrayList<TaskState> mOpeningTasks = null;
+
+        private WindowContainerToken mPipTask = null;
+        private WindowContainerToken mRecentsTask = null;
+        private int mRecentsTaskId = -1;
+        private TransitionInfo mInfo = null;
+        private boolean mOpeningSeparateHome = false;
+        private ArrayMap<SurfaceControl, SurfaceControl> mLeashMap = null;
+        private PictureInPictureSurfaceTransaction mPipTransaction = null;
+        private IBinder mTransition = null;
+        private boolean mKeyguardLocked = false;
+        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;
+
+        RecentsController(IRecentsAnimationRunner listener) {
+            mListener = listener;
+            mDeathHandler = () -> mExecutor.execute(() -> {
+                if (mListener == null) return;
+                if (mFinishCB != null) {
+                    finish(mWillFinishToHome, false /* leaveHint */);
+                }
+            });
+            try {
+                mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */);
+            } catch (RemoteException e) {
+                mListener = null;
+            }
+        }
+
+        void setTransition(IBinder transition) {
+            mTransition = transition;
+        }
+
+        void cancel() {
+            // restoring (to-home = false) involves submitting more WM changes, so by default, use
+            // toHome = true when canceling.
+            cancel(true /* toHome */);
+        }
+
+        void cancel(boolean toHome) {
+            if (mFinishCB != null && mListener != null) {
+                try {
+                    mListener.onAnimationCanceled(null, null);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Error canceling recents animation", e);
+                }
+                finish(toHome, false /* userLeave */);
+            } else {
+                cleanUp();
+            }
+        }
+
+        /**
+         * Sends a cancel message to the recents animation with snapshots. Used to trigger a
+         * "replace-with-screenshot" like behavior.
+         */
+        private boolean sendCancelWithSnapshots() {
+            int[] taskIds = null;
+            TaskSnapshot[] snapshots = null;
+            if (mPausingTasks.size() > 0) {
+                taskIds = new int[mPausingTasks.size()];
+                snapshots = new TaskSnapshot[mPausingTasks.size()];
+                try {
+                    for (int i = 0; i < mPausingTasks.size(); ++i) {
+                        snapshots[i] = ActivityTaskManager.getService().takeTaskSnapshot(
+                                mPausingTasks.get(0).mTaskInfo.taskId);
+                    }
+                } catch (RemoteException e) {
+                    taskIds = null;
+                    snapshots = null;
+                }
+            }
+            try {
+                mListener.onAnimationCanceled(taskIds, snapshots);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Error canceling recents animation", e);
+                return false;
+            }
+            return true;
+        }
+
+        void cleanUp() {
+            if (mListener != null && mDeathHandler != null) {
+                mListener.asBinder().unlinkToDeath(mDeathHandler, 0 /* flags */);
+                mDeathHandler = null;
+            }
+            mListener = null;
+            mFinishCB = null;
+            // clean-up leash surfacecontrols and anything that might reference them.
+            if (mLeashMap != null) {
+                for (int i = 0; i < mLeashMap.size(); ++i) {
+                    mLeashMap.valueAt(i).release();
+                }
+                mLeashMap = null;
+            }
+            mFinishTransaction = null;
+            mPausingTasks = null;
+            mOpeningTasks = null;
+            mInfo = null;
+            mTransition = null;
+            mControllers.remove(this);
+        }
+
+        boolean start(TransitionInfo info, SurfaceControl.Transaction t,
+                SurfaceControl.Transaction finishT, Transitions.TransitionFinishCallback finishCB) {
+            if (mListener == null || mTransition == null) {
+                cleanUp();
+                return false;
+            }
+            // First see if this is a valid recents transition.
+            boolean hasPausingTasks = false;
+            for (int i = 0; i < info.getChanges().size(); ++i) {
+                final TransitionInfo.Change change = info.getChanges().get(i);
+                if (TransitionUtil.isWallpaper(change)) continue;
+                if (TransitionUtil.isClosingType(change.getMode())) {
+                    hasPausingTasks = true;
+                    continue;
+                }
+                final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+                if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_RECENTS) {
+                    mRecentsTask = taskInfo.token;
+                    mRecentsTaskId = taskInfo.taskId;
+                } else if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) {
+                    mRecentsTask = taskInfo.token;
+                    mRecentsTaskId = taskInfo.taskId;
+                }
+            }
+            if (mRecentsTask == null || !hasPausingTasks) {
+                // Recents is already running apparently, so this is a no-op.
+                Slog.e(TAG, "Tried to start recents while it is already running. recents="
+                        + mRecentsTask);
+                cleanUp();
+                return false;
+            }
+
+            mInfo = info;
+            mFinishCB = finishCB;
+            mFinishTransaction = finishT;
+            mPausingTasks = new ArrayList<>();
+            mOpeningTasks = new ArrayList<>();
+            mLeashMap = new ArrayMap<>();
+            mKeyguardLocked = (info.getFlags() & TRANSIT_FLAG_KEYGUARD_LOCKED) != 0;
+
+            final ArrayList<RemoteAnimationTarget> apps = new ArrayList<>();
+            final ArrayList<RemoteAnimationTarget> wallpapers = new ArrayList<>();
+            TransitionUtil.LeafTaskFilter leafTaskFilter = new TransitionUtil.LeafTaskFilter();
+            // About layering: we divide up the "layer space" into 3 regions (each the size of
+            // the change count). This lets us categorize things into above/below/between
+            // while maintaining their relative ordering.
+            for (int i = 0; i < info.getChanges().size(); ++i) {
+                final TransitionInfo.Change change = info.getChanges().get(i);
+                final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+                if (TransitionUtil.isWallpaper(change)) {
+                    final RemoteAnimationTarget target = TransitionUtil.newTarget(change,
+                            // wallpapers go into the "below" layer space
+                            info.getChanges().size() - i, info, t, mLeashMap);
+                    wallpapers.add(target);
+                    // Make all the wallpapers opaque since we want them visible from the start
+                    t.setAlpha(target.leash, 1);
+                } else if (leafTaskFilter.test(change)) {
+                    // start by putting everything into the "below" layer space.
+                    final RemoteAnimationTarget target = TransitionUtil.newTarget(change,
+                            info.getChanges().size() - i, info, t, mLeashMap);
+                    apps.add(target);
+                    if (TransitionUtil.isClosingType(change.getMode())) {
+                        // raise closing (pausing) task to "above" layer so it isn't covered
+                        t.setLayer(target.leash, info.getChanges().size() * 3 - i);
+                        mPausingTasks.add(new TaskState(change, target.leash));
+                        if (taskInfo.pictureInPictureParams != null
+                                && taskInfo.pictureInPictureParams.isAutoEnterEnabled()) {
+                            mPipTask = taskInfo.token;
+                        }
+                    } else if (taskInfo != null
+                            && taskInfo.topActivityType == ACTIVITY_TYPE_RECENTS) {
+                        // There's a 3p launcher, so make sure recents goes above that.
+                        t.setLayer(target.leash, info.getChanges().size() * 3 - i);
+                    } else if (taskInfo != null && taskInfo.topActivityType == ACTIVITY_TYPE_HOME) {
+                        // do nothing
+                    } else if (TransitionUtil.isOpeningType(change.getMode())) {
+                        mOpeningTasks.add(new TaskState(change, target.leash));
+                    }
+                }
+            }
+            t.apply();
+            try {
+                mListener.onAnimationStart(this,
+                        apps.toArray(new RemoteAnimationTarget[apps.size()]),
+                        wallpapers.toArray(new RemoteAnimationTarget[wallpapers.size()]),
+                        new Rect(0, 0, 0, 0), new Rect());
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Error starting recents animation", e);
+                cancel();
+            }
+            return true;
+        }
+
+        @SuppressLint("NewApi")
+        void merge(TransitionInfo info, SurfaceControl.Transaction t,
+                Transitions.TransitionFinishCallback finishCallback) {
+            if (mFinishCB == null) {
+                // This was no-op'd (likely a repeated start) and we've already sent finish.
+                return;
+            }
+            if (info.getType() == TRANSIT_SLEEP) {
+                // A sleep event means we need to stop animations immediately, so cancel here.
+                cancel();
+                return;
+            }
+            ArrayList<TransitionInfo.Change> openingTasks = null;
+            ArrayList<TransitionInfo.Change> closingTasks = null;
+            mOpeningSeparateHome = false;
+            TransitionInfo.Change recentsOpening = null;
+            boolean foundRecentsClosing = false;
+            boolean hasChangingApp = false;
+            final TransitionUtil.LeafTaskFilter leafTaskFilter =
+                    new TransitionUtil.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 (TransitionUtil.isOpeningType(change.getMode())) {
+                    if (mRecentsTask.equals(change.getContainer())) {
+                        recentsOpening = change;
+                    } else if (isLeafTask) {
+                        if (taskInfo.topActivityType == ACTIVITY_TYPE_HOME) {
+                            // This is usually a 3p launcher
+                            mOpeningSeparateHome = true;
+                        }
+                        if (openingTasks == null) {
+                            openingTasks = new ArrayList<>();
+                        }
+                        openingTasks.add(change);
+                    }
+                } else if (TransitionUtil.isClosingType(change.getMode())) {
+                    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) {
+                    // Finish recents animation if the display is changed, so the default
+                    // transition handler can play the animation such as rotation effect.
+                    if (change.hasFlags(TransitionInfo.FLAG_IS_DISPLAY)) {
+                        cancel(mWillFinishToHome);
+                        return;
+                    }
+                    hasChangingApp = true;
+                }
+            }
+            if (hasChangingApp && foundRecentsClosing) {
+                // This happens when a visible app is expanding (usually PiP). In this case,
+                // that transition probably has a special-purpose animation, so finish recents
+                // now and let it do its animation (since recents is going to be occluded).
+                sendCancelWithSnapshots();
+                mExecutor.executeDelayed(
+                        () -> finishInner(true /* toHome */, false /* userLeaveHint */), 0);
+                return;
+            }
+            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) {
+                    Slog.e(TAG, "Returning to recents while recents is already idle.");
+                }
+                if (closingTasks == null || closingTasks.size() == 0) {
+                    Slog.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) {
+                        Slog.e(TAG, "Back to existing recents animation from an unrecognized "
+                                + "task: " + change.getTaskInfo().taskId);
+                        continue;
+                    }
+                    mPausingTasks.add(mOpeningTasks.remove(openingIdx));
+                    didMergeThings = true;
+                }
+            }
+            RemoteAnimationTarget[] appearedTargets = null;
+            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;
+                appearedTargets = new RemoteAnimationTarget[openingTasks.size()];
+                for (int i = 0; i < openingTasks.size(); ++i) {
+                    final TransitionInfo.Change change = openingTasks.get(i);
+                    int pausingIdx = TaskState.indexOf(mPausingTasks, change);
+                    if (pausingIdx >= 0) {
+                        // Something is showing/opening a previously-pausing app.
+                        appearedTargets[i] = TransitionUtil.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.
+                        appearedTargets[i] = TransitionUtil.newTarget(
+                                change, layer, info, t, mLeashMap);
+                        // reparent into the original `mInfo` since that's where we are animating.
+                        final int rootIdx = TransitionUtil.rootIndexFor(change, mInfo);
+                        t.reparent(appearedTargets[i].leash, mInfo.getRoot(rootIdx).getLeash());
+                        t.setLayer(appearedTargets[i].leash, layer);
+                        mOpeningTasks.add(new TaskState(change, appearedTargets[i].leash));
+                    }
+                }
+                didMergeThings = true;
+                mState = STATE_NEW_TASK;
+            }
+            if (!didMergeThings) {
+                // Didn't recognize anything in incoming transition so don't merge it.
+                Slog.w(TAG, "Don't know how to merge this transition.");
+                return;
+            }
+            // At this point, we are accepting the merge.
+            t.apply();
+            // not using the incoming anim-only surfaces
+            info.releaseAnimSurfaces();
+            finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
+            if (appearedTargets == null) return;
+            try {
+                mListener.onTasksAppeared(appearedTargets);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Error sending appeared tasks to recents animation", e);
+            }
+        }
+
+        @Override
+        public TaskSnapshot screenshotTask(int taskId) {
+            try {
+                return ActivityTaskManager.getService().takeTaskSnapshot(taskId);
+            } catch (RemoteException e) {
+                Slog.e(TAG, "Failed to screenshot task", e);
+            }
+            return null;
+        }
+
+        @Override
+        public void setInputConsumerEnabled(boolean enabled) {
+            mExecutor.execute(() -> {
+                if (mFinishCB == null || !enabled) return;
+                // 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) {
+                    Slog.e(TAG, "Failed to set focused task", e);
+                }
+            });
+        }
+
+        @Override
+        public void setAnimationTargetsBehindSystemBars(boolean behindSystemBars) {
+        }
+
+        @Override
+        public void setFinishTaskTransaction(int taskId,
+                PictureInPictureSurfaceTransaction finishTransaction, SurfaceControl overlay) {
+            mExecutor.execute(() -> {
+                if (mFinishCB == null) return;
+                mPipTransaction = finishTransaction;
+            });
+        }
+
+        @Override
+        @SuppressLint("NewApi")
+        public void finish(boolean toHome, boolean sendUserLeaveHint) {
+            mExecutor.execute(() -> finishInner(toHome, sendUserLeaveHint));
+        }
+
+        private void finishInner(boolean toHome, boolean sendUserLeaveHint) {
+            if (mFinishCB == null) {
+                Slog.e(TAG, "Duplicate call to finish");
+                return;
+            }
+            final Transitions.TransitionFinishCallback finishCB = mFinishCB;
+            mFinishCB = null;
+
+            final SurfaceControl.Transaction t = mFinishTransaction;
+            final WindowContainerTransaction wct = new WindowContainerTransaction();
+
+            if (mKeyguardLocked && mRecentsTask != null) {
+                if (toHome) wct.reorder(mRecentsTask, true /* toTop */);
+                else wct.restoreTransientOrder(mRecentsTask);
+            }
+            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) {
+                    // reverse order so that index 0 ends up on top
+                    wct.reorder(mPausingTasks.get(i).mToken, true /* onTop */);
+                    t.show(mPausingTasks.get(i).mTaskSurface);
+                }
+                if (!mKeyguardLocked && mRecentsTask != null) {
+                    wct.restoreTransientOrder(mRecentsTask);
+                }
+            } else if (toHome && mOpeningSeparateHome && mPausingTasks != null) {
+                // 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 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) {
+                        // Make sure it is on top.
+                        wct.reorder(state.mToken, true /* onTop */);
+                    }
+                    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
+                        // do special stuff in WMCore to accommodate.
+                        wct.setDoNotPip(mPausingTasks.get(i).mToken);
+                    }
+                    // Since we will reparent out of the leashes, pre-emptively hide the child
+                    // surface to match the leash. Otherwise, there will be a flicker before the
+                    // visibility gets committed in Core when using split-screen (in splitscreen,
+                    // the leaf-tasks are not "independent" so aren't hidden by normal setup).
+                    t.hide(mPausingTasks.get(i).mTaskSurface);
+                }
+                if (mPipTask != null && mPipTransaction != null && sendUserLeaveHint) {
+                    t.show(mInfo.getChange(mPipTask).getLeash());
+                    PictureInPictureSurfaceTransaction.apply(mPipTransaction,
+                            mInfo.getChange(mPipTask).getLeash(), t);
+                    mPipTask = null;
+                    mPipTransaction = null;
+                }
+            }
+            cleanUp();
+            finishCB.onTransitionFinished(wct.isEmpty() ? null : wct, null /* wctCB */);
+        }
+
+        @Override
+        public void setDeferCancelUntilNextTransition(boolean defer, boolean screenshot) {
+        }
+
+        @Override
+        public void cleanupScreenshot() {
+        }
+
+        @Override
+        public void setWillFinishToHome(boolean willFinishToHome) {
+            mExecutor.execute(() -> {
+                mWillFinishToHome = willFinishToHome;
+            });
+        }
+
+        /**
+         * @see IRecentsAnimationController#removeTask
+         */
+        @Override
+        public boolean removeTask(int taskId) {
+            return false;
+        }
+
+        /**
+         * @see IRecentsAnimationController#detachNavigationBarFromApp
+         */
+        @Override
+        public void detachNavigationBarFromApp(boolean moveHomeToTop) {
+            mExecutor.execute(() -> {
+                if (mTransition == null) return;
+                try {
+                    ActivityTaskManager.getService().detachNavigationBarFromApp(mTransition);
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Failed to detach the navigation bar from app", e);
+                }
+            });
+        }
+
+        /**
+         * @see IRecentsAnimationController#animateNavigationBarToApp(long)
+         */
+        @Override
+        public void animateNavigationBarToApp(long duration) {
+        }
+    };
+
+    /** Utility class to track the state of a task as-seen by recents. */
+    private static class TaskState {
+        WindowContainerToken mToken;
+        ActivityManager.RunningTaskInfo mTaskInfo;
+
+        /** The surface/leash of the task provided by Core. */
+        SurfaceControl mTaskSurface;
+
+        /** The (local) animation-leash created for this task. */
+        SurfaceControl mLeash;
+
+        TaskState(TransitionInfo.Change change, SurfaceControl leash) {
+            mToken = change.getContainer();
+            mTaskInfo = change.getTaskInfo();
+            mTaskSurface = change.getLeash();
+            mLeash = leash;
+        }
+
+        static int indexOf(ArrayList<TaskState> list, TransitionInfo.Change change) {
+            for (int i = list.size() - 1; i >= 0; --i) {
+                if (list.get(i).mToken.equals(change.getContainer())) {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        public String toString() {
+            return "" + mToken + " : " + mLeash;
+        }
+    }
+
+    /**
+     * An interface for a mixed handler to receive information about recents requests (since these
+     * come into this handler directly vs from WMCore request).
+     */
+    public interface RecentsMixedHandler {
+        /**
+         * Called when a recents request comes in. The handler can add operations to outWCT. If
+         * the handler wants to "accept" the transition, it should return itself; otherwise, it
+         * should return `null`.
+         *
+         * If a mixed-handler accepts this recents, it will be the de-facto handler for this
+         * transition and is required to call the associated {@link #startAnimation},
+         * {@link #mergeAnimation}, and {@link #onTransitionConsumed} methods.
+         */
+        Transitions.TransitionHandler handleRecentsRequest(WindowContainerTransaction outWCT);
+
+        /**
+         * Reports the transition token associated with the accepted recents request. If there was
+         * a problem starting the request, this will be called with `null`.
+         */
+        void setRecentsTransition(@Nullable IBinder transition);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index d094892..aa1e6ed 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -43,6 +43,7 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.phone.PipTouchHandler;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.recents.RecentsTransitionHandler;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.splitscreen.StageCoordinator;
 import com.android.wm.shell.sysui.ShellInit;
@@ -55,10 +56,12 @@
  * A handler for dealing with transitions involving multiple other handlers. For example: an
  * activity in split-screen going into PiP.
  */
-public class DefaultMixedHandler implements Transitions.TransitionHandler {
+public class DefaultMixedHandler implements Transitions.TransitionHandler,
+        RecentsTransitionHandler.RecentsMixedHandler {
 
     private final Transitions mPlayer;
     private PipTransitionController mPipHandler;
+    private RecentsTransitionHandler mRecentsHandler;
     private StageCoordinator mSplitHandler;
 
     private static class MixedTransition {
@@ -122,7 +125,8 @@
 
     public DefaultMixedHandler(@NonNull ShellInit shellInit, @NonNull Transitions player,
             Optional<SplitScreenController> splitScreenControllerOptional,
-            Optional<PipTouchHandler> pipTouchHandlerOptional) {
+            Optional<PipTouchHandler> pipTouchHandlerOptional,
+            Optional<RecentsTransitionHandler> recentsHandlerOptional) {
         mPlayer = player;
         if (Transitions.ENABLE_SHELL_TRANSITIONS && pipTouchHandlerOptional.isPresent()
                 && splitScreenControllerOptional.isPresent()) {
@@ -134,6 +138,10 @@
                 if (mSplitHandler != null) {
                     mSplitHandler.setMixedHandler(this);
                 }
+                mRecentsHandler = recentsHandlerOptional.orElse(null);
+                if (mRecentsHandler != null) {
+                    mRecentsHandler.addMixer(this);
+                }
             }, this);
         }
     }
@@ -200,6 +208,29 @@
         return null;
     }
 
+    @Override
+    public Transitions.TransitionHandler handleRecentsRequest(WindowContainerTransaction outWCT) {
+        if (mRecentsHandler != null && mSplitHandler.isSplitActive()) {
+            return this;
+        }
+        return null;
+    }
+
+    @Override
+    public void setRecentsTransition(IBinder transition) {
+        if (mSplitHandler.isSplitActive()) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a recents request while "
+                    + "Split-Screen is active, so treat it as Mixed.");
+            final MixedTransition mixed = new MixedTransition(
+                    MixedTransition.TYPE_RECENTS_DURING_SPLIT, transition);
+            mixed.mLeftoversHandler = mRecentsHandler;
+            mActiveTransitions.add(mixed);
+        } else {
+            throw new IllegalStateException("Accepted a recents transition but don't know how to"
+                    + " handle it");
+        }
+    }
+
     private TransitionInfo subCopy(@NonNull TransitionInfo info,
             @WindowManager.TransitionType int newType, boolean withChanges) {
         final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0);
@@ -563,6 +594,8 @@
             mPipHandler.onTransitionConsumed(transition, aborted, finishT);
         } else if (mixed.mType == MixedTransition.TYPE_RECENTS_DURING_SPLIT) {
             mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
+        } else if (mixed.mType == MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE) {
+            mixed.mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java
index 5e066fa..f8fb76a 100644
--- a/services/core/java/com/android/server/wm/ActivityStartController.java
+++ b/services/core/java/com/android/server/wm/ActivityStartController.java
@@ -50,8 +50,6 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.RemoteAnimationAdapter;
-import android.view.WindowManager;
-import android.window.RemoteTransition;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.ArrayUtils;
@@ -562,9 +560,8 @@
         final Task rootTask = mService.mRootWindowContainer.getDefaultTaskDisplayArea()
                 .getRootTask(WINDOWING_MODE_UNDEFINED, activityType);
         if (rootTask == null) return false;
-        final RemoteTransition remote = options.getRemoteTransition();
         final ActivityRecord r = rootTask.topRunningActivity();
-        if (r == null || r.isVisibleRequested() || !r.attachedToProcess() || remote == null
+        if (r == null || r.isVisibleRequested() || !r.attachedToProcess()
                 || !r.mActivityComponent.equals(intent.getComponent())
                 // Recents keeps invisible while device is locked.
                 || r.mDisplayContent.isKeyguardLocked()) {
@@ -573,47 +570,13 @@
         mService.mRootWindowContainer.startPowerModeLaunchIfNeeded(true /* forceSend */, r);
         final ActivityMetricsLogger.LaunchingState launchingState =
                 mSupervisor.getActivityMetricsLogger().notifyActivityLaunching(intent);
-        final Transition transition = new Transition(WindowManager.TRANSIT_TO_FRONT,
-                0 /* flags */, r.mTransitionController, mService.mWindowManager.mSyncEngine);
-        if (r.mTransitionController.isCollecting()) {
-            // Special case: we are entering recents while an existing transition is running. In
-            // this case, we know it's safe to "defer" the activity launch, so lets do so now so
-            // that it can get its own transition and thus update launcher correctly.
-            mService.mWindowManager.mSyncEngine.queueSyncSet(
-                    () -> {
-                        if (r.isAttached()) {
-                            r.mTransitionController.moveToCollecting(transition);
-                        }
-                    },
-                    () -> {
-                        if (r.isAttached() && transition.isCollecting()) {
-                            startExistingRecentsIfPossibleInner(options, r, rootTask,
-                                    launchingState, remote, transition);
-                        }
-                    });
-        } else {
-            r.mTransitionController.moveToCollecting(transition);
-            startExistingRecentsIfPossibleInner(options, r, rootTask, launchingState, remote,
-                    transition);
-        }
-        return true;
-    }
-
-    private void startExistingRecentsIfPossibleInner(ActivityOptions options, ActivityRecord r,
-            Task rootTask, ActivityMetricsLogger.LaunchingState launchingState,
-            RemoteTransition remoteTransition, Transition transition) {
         final Task task = r.getTask();
         mService.deferWindowLayout();
         try {
             final TransitionController controller = r.mTransitionController;
             if (controller.getTransitionPlayer() != null) {
-                controller.requestStartTransition(transition, task, remoteTransition,
-                        null /* displayChange */);
                 controller.collect(task);
                 controller.setTransientLaunch(r, TaskDisplayArea.getRootTaskAbove(rootTask));
-            } else {
-                // The transition player might be died when executing the queued transition.
-                transition.abort();
             }
             task.moveToFront("startExistingRecents");
             task.mInResumeTopActivity = true;
@@ -624,6 +587,7 @@
             task.mInResumeTopActivity = false;
             mService.continueWindowLayout();
         }
+        return true;
     }
 
     void registerRemoteAnimationForNextActivityStart(String packageName,
diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java
index ce29564..12be1d3 100644
--- a/services/core/java/com/android/server/wm/ActivityStarter.java
+++ b/services/core/java/com/android/server/wm/ActivityStarter.java
@@ -1582,19 +1582,19 @@
             }
         }
         if (isTransientLaunch) {
-            if (forceTransientTransition && newTransition != null) {
-                newTransition.collect(mLastStartActivityRecord);
-                newTransition.collect(mPriorAboveTask);
+            if (forceTransientTransition) {
+                transitionController.collect(mLastStartActivityRecord);
+                transitionController.collect(mPriorAboveTask);
             }
             // `started` isn't guaranteed to be the actual relevant activity, so we must wait
             // until after we launched to identify the relevant activity.
             transitionController.setTransientLaunch(mLastStartActivityRecord, mPriorAboveTask);
-            if (forceTransientTransition && newTransition != null) {
+            if (forceTransientTransition) {
                 final DisplayContent dc = mLastStartActivityRecord.getDisplayContent();
                 // update wallpaper target to TransientHide
                 dc.mWallpaperController.adjustWallpaperWindows();
                 // execute transition because there is no change
-                newTransition.setReady(dc, true /* ready */);
+                transitionController.setReady(dc, true /* ready */);
             }
         }
         if (!userLeaving) {
diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
index 992743a..555cd38 100644
--- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
+++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
@@ -1240,25 +1240,6 @@
             ProfilerInfo profilerInfo, Bundle bOptions, int userId, boolean validateIncomingUser) {
 
         final SafeActivityOptions opts = SafeActivityOptions.fromBundle(bOptions);
-        // A quick path (skip general intent/task resolving) to start recents animation if the
-        // recents (or home) activity is available in background.
-        if (opts != null && opts.getOriginalOptions().getTransientLaunch()
-                && isCallerRecents(Binder.getCallingUid())) {
-            final long origId = Binder.clearCallingIdentity();
-            try {
-                synchronized (mGlobalLock) {
-                    Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "startExistingRecents");
-                    if (mActivityStartController.startExistingRecentsIfPossible(
-                            intent, opts.getOriginalOptions())) {
-                        return ActivityManager.START_TASK_TO_FRONT;
-                    }
-                    // Else follow the standard launch procedure.
-                }
-            } finally {
-                Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
-                Binder.restoreCallingIdentity(origId);
-            }
-        }
 
         assertPackageMatchesCallingUid(callingPackage);
         enforceNotIsolatedCaller("startActivityAsUser");
@@ -5718,6 +5699,23 @@
                 boolean validateIncomingUser, PendingIntentRecord originatingPendingIntent,
                 BackgroundStartPrivileges backgroundStartPrivileges) {
             assertPackageMatchesCallingUid(callingPackage);
+            // A quick path (skip general intent/task resolving) to start recents animation if the
+            // recents (or home) activity is available in background.
+            if (options != null && options.getOriginalOptions() != null
+                    && options.getOriginalOptions().getTransientLaunch() && isCallerRecents(uid)) {
+                try {
+                    synchronized (mGlobalLock) {
+                        Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "startExistingRecents");
+                        if (mActivityStartController.startExistingRecentsIfPossible(
+                                intent, options.getOriginalOptions())) {
+                            return ActivityManager.START_TASK_TO_FRONT;
+                        }
+                        // Else follow the standard launch procedure.
+                    }
+                } finally {
+                    Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+                }
+            }
             return getActivityStartController().startActivityInPackage(uid, realCallingPid,
                     realCallingUid, callingPackage, callingFeatureId, intent, resolvedType,
                     resultTo, resultWho, requestCode, startFlags, options, userId, inTask,