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 3d5230d5cf908..57b5b8f24fadf 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.PipUiEventLogger; 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 @@ public abstract class WMShellBaseModule { desktopModeTaskRepository, mainExecutor)); } + @BindsOptionalOf + abstract RecentsTransitionHandler optionalRecentsTransitionHandler(); + // // Shell transitions // @@ -803,6 +807,7 @@ public abstract class WMShellBaseModule { Optional unfoldTransitionHandler, Optional freeformComponents, Optional recentTasksOptional, + Optional recentsTransitionHandlerOptional, Optional oneHandedControllerOptional, Optional hideDisplayCutoutControllerOptional, Optional 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 7a83d101578fe..cc0da2840fa03 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.PipMotionHelper; 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 @@ public abstract class WMShellModule { ShellInit shellInit, Optional splitScreenOptional, Optional pipTouchHandlerOptional, + Optional recentsTransitionHandler, Transitions transitions) { return new DefaultMixedHandler(shellInit, transitions, splitScreenOptional, - pipTouchHandlerOptional); + pipTouchHandlerOptional, recentsTransitionHandler); + } + + @WMSingleton + @Provides + static RecentsTransitionHandler provideRecentsTransitionHandler( + ShellInit shellInit, + Transitions transitions, + Optional 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 1a6c1d65db03c..4048c5b8feabe 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 @@ interface IRecentTasks { * 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 0d9faa3c6f83c..c5bfd8753994b 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 static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_RE 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 @@ public class RecentTasksController implements TaskStackListenerCallback, 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 @@ public class RecentTasksController implements TaskStackListenerCallback, 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 @@ public class RecentTasksController implements TaskStackListenerCallback, 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 0000000000000..2ec64b009b3bf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -0,0 +1,760 @@ +/* + * 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 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 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 mPausingTasks = null; + + /** + * List of tasks that we are switching to. Upon finish, these will remain visible and + * on top. + */ + private ArrayList mOpeningTasks = null; + + private WindowContainerToken mPipTask = null; + private WindowContainerToken mRecentsTask = null; + private int mRecentsTaskId = -1; + private TransitionInfo mInfo = null; + private boolean mOpeningSeparateHome = false; + private ArrayMap 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 (mListener != null) { + try { + mListener.onAnimationCanceled(null, null); + } catch (RemoteException e) { + Slog.e(TAG, "Error canceling recents animation", e); + } + } + if (mFinishCB != null) { + 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."); + 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 apps = new ArrayList<>(); + final ArrayList 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 openingTasks = null; + ArrayList 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 != null && 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 != null && 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 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 aa851d1bd30fc..586cab07c5088 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.internal.protolog.common.ProtoLog; 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 @@ import java.util.Optional; * 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 class DefaultMixedHandler implements Transitions.TransitionHandler { public DefaultMixedHandler(@NonNull ShellInit shellInit, @NonNull Transitions player, Optional splitScreenControllerOptional, - Optional pipTouchHandlerOptional) { + Optional pipTouchHandlerOptional, + Optional recentsHandlerOptional) { mPlayer = player; if (Transitions.ENABLE_SHELL_TRANSITIONS && pipTouchHandlerOptional.isPresent() && splitScreenControllerOptional.isPresent()) { @@ -134,6 +138,10 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { if (mSplitHandler != null) { mSplitHandler.setMixedHandler(this); } + mRecentsHandler = recentsHandlerOptional.orElse(null); + if (mRecentsHandler != null) { + mRecentsHandler.addMixer(this); + } }, this); } } @@ -200,6 +208,29 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { 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); @@ -561,6 +592,8 @@ public class DefaultMixedHandler implements Transitions.TransitionHandler { 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 5e066faf0e90a..f8fb76acf81e4 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.provider.Settings; 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 @@ public class ActivityStartController { 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 @@ public class ActivityStartController { 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 @@ public class ActivityStartController { 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 ce29564d0b02e..12be1d3186a1e 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 @@ class ActivityStarter { } } 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 992743ab85930..555cd38806e63 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 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { 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 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { 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,