blob: 387d39056ffc1fdd9011f5fe6239f5ca3fe6a6d1 [file] [log] [blame]
/*
* 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.Display;
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.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
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);
}
@VisibleForTesting
public IBinder startRecentsTransition(PendingIntent intent, Intent fillIn, Bundle options,
IApplicationThread appThread, IRecentsAnimationRunner listener) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"RecentsTransitionHandler.startRecentsTransition");
// 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.setTransition(transition);
mControllers.add(controller);
} else {
controller.cancel("startRecentsTransition");
}
return transition;
}
@Override
public WindowContainerTransaction handleRequest(IBinder transition,
TransitionRequestInfo request) {
// do not directly handle requests. Only entry point should be via startRecentsTransition
// TODO: Only log an error if the transition is a recents transition
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) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"RecentsTransitionHandler.startAnimation: no controller found");
return false;
}
final RecentsController controller = mControllers.get(controllerIdx);
Transitions.setRunningRemoteTransitionDelegate(mAnimApp);
mAnimApp = null;
if (!controller.start(info, startTransaction, finishTransaction, finishCallback)) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"RecentsTransitionHandler.startAnimation: failed to start animation");
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) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"RecentsTransitionHandler.mergeAnimation: no controller found");
return;
}
final RecentsController controller = mControllers.get(targetIdx);
controller.merge(info, t, finishCallback);
}
@Override
public void onTransitionConsumed(IBinder transition, boolean aborted,
SurfaceControl.Transaction finishTransaction) {
// Only one recents transition can be handled at a time, but currently the first transition
// will trigger a no-op in the second transition which holds the active recents animation
// runner on the launcher side. For now, cancel all existing animations to ensure we
// don't get into a broken state with an orphaned animation runner, and later we can try to
// merge the latest transition into the currently running one
for (int i = mControllers.size() - 1; i >= 0; i--) {
mControllers.get(i).cancel("onTransitionConsumed");
}
}
/** There is only one of these and it gets reset on finish. */
private class RecentsController extends IRecentsAnimationController.Stub {
private final int mInstanceId;
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 boolean mPausingSeparateHome = 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) {
mInstanceId = System.identityHashCode(this);
mListener = listener;
mDeathHandler = () -> {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.DeathRecipient: binder died", mInstanceId);
finish(mWillFinishToHome, false /* leaveHint */);
};
try {
mListener.asBinder().linkToDeath(mDeathHandler, 0 /* flags */);
} catch (RemoteException e) {
Slog.e(TAG, "RecentsController: failed to link to death", e);
mListener = null;
}
}
void setTransition(IBinder transition) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.setTransition: id=%s", mInstanceId, transition);
mTransition = transition;
}
void cancel(String reason) {
// restoring (to-home = false) involves submitting more WM changes, so by default, use
// toHome = true when canceling.
cancel(true /* toHome */, reason);
}
void cancel(boolean toHome, String reason) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.cancel: toHome=%b reason=%s",
mInstanceId, toHome, reason);
if (mListener != null) {
try {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.cancel: calling onAnimationCanceled",
mInstanceId);
mListener.onAnimationCanceled(null, null);
} catch (RemoteException e) {
Slog.e(TAG, "Error canceling recents animation", e);
}
}
if (mFinishCB != null) {
finishInner(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 {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.cancel: calling onAnimationCanceled with snapshots",
mInstanceId);
mListener.onAnimationCanceled(taskIds, snapshots);
} catch (RemoteException e) {
Slog.e(TAG, "Error canceling recents animation", e);
return false;
}
return true;
}
void cleanUp() {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.cleanup", mInstanceId);
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) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.start", mInstanceId);
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<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())) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
" adding pausing taskId=%d", taskInfo.taskId);
// 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.topActivityType == ACTIVITY_TYPE_HOME) {
// This can only happen if we have a separate recents/home (3p launcher)
mPausingSeparateHome = true;
}
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())) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
" adding opening taskId=%d", taskInfo.taskId);
mOpeningTasks.add(new TaskState(change, target.leash));
}
} else if (TransitionUtil.isDividerBar(change)) {
final RemoteAnimationTarget target = TransitionUtil.newTarget(change,
info.getChanges().size() - i, info, t, mLeashMap);
// Add this as a app and we will separate them on launcher side by window type.
apps.add(target);
}
}
t.apply();
try {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.start: calling onAnimationStart", mInstanceId);
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("onAnimationStart() failed");
}
return true;
}
@SuppressLint("NewApi")
void merge(TransitionInfo info, SurfaceControl.Transaction t,
Transitions.TransitionFinishCallback finishCallback) {
if (mFinishCB == null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.merge: skip, no finish callback",
mInstanceId);
// This was no-op'd (likely a repeated start) and we've already sent finish.
return;
}
if (info.getType() == TRANSIT_SLEEP) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.merge: transit_sleep", mInstanceId);
// A sleep event means we need to stop animations immediately, so cancel here.
cancel("transit_sleep");
return;
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.merge", mInstanceId);
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();
boolean hasTaskChange = false;
for (int i = 0; i < info.getChanges().size(); ++i) {
final TransitionInfo.Change change = info.getChanges().get(i);
final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
if (taskInfo != null
&& taskInfo.configuration.windowConfiguration.isAlwaysOnTop()) {
// Tasks that are always on top (e.g. bubbles), will handle their own transition
// as they are on top of everything else. So cancel the merge here.
cancel("task #" + taskInfo.taskId + " is always_on_top");
return;
}
hasTaskChange = hasTaskChange || taskInfo != null;
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, "display change");
return;
}
// Don't consider order-only changes as changing apps.
if (!TransitionUtil.isOrderOnly(change)) {
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) {
// Potentially cancelling a task-switch. Move the tasks back to mPausing if they
// are in mOpening.
for (int i = 0; i < closingTasks.size(); ++i) {
final TransitionInfo.Change change = closingTasks.get(i);
final int pausingIdx = TaskState.indexOf(mPausingTasks, change);
if (pausingIdx >= 0) {
mPausingTasks.remove(pausingIdx);
didMergeThings = true;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
" closing pausing taskId=%d", change.getTaskInfo().taskId);
continue;
}
int openingIdx = TaskState.indexOf(mOpeningTasks, change);
if (openingIdx < 0) {
Slog.w(TAG, "Closing a task that wasn't opening, this may be split or"
+ " something unexpected: " + change.getTaskInfo().taskId);
continue;
}
final TaskState openingTask = mOpeningTasks.remove(openingIdx);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
" pausing opening taskId=%d", openingTask.mTaskInfo.taskId);
mPausingTasks.add(openingTask);
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);
final TaskState pausingTask = mPausingTasks.remove(pausingIdx);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
" opening pausing taskId=%d", pausingTask.mTaskInfo.taskId);
mOpeningTasks.add(pausingTask);
// 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);
// Hide the animation leash, let listener show it.
t.hide(appearedTargets[i].leash);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
" opening new taskId=%d", appearedTargets[i].taskId);
mOpeningTasks.add(new TaskState(change, appearedTargets[i].leash));
}
}
didMergeThings = true;
mState = STATE_NEW_TASK;
}
if (mPausingTasks.isEmpty()) {
// The pausing tasks may be removed by the incoming closing tasks.
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.merge: empty pausing tasks", mInstanceId);
}
if (!hasTaskChange) {
// Activity only transition, so consume the merge as it doesn't affect the rest of
// recents.
Slog.d(TAG, "Got an activity only transition during recents, so apply directly");
mergeActivityOnly(info, t);
} else 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, foundRecentsClosing="
+ foundRecentsClosing);
if (foundRecentsClosing) {
mWillFinishToHome = false;
cancel(false /* toHome */, "didn't merge");
}
return;
}
// At this point, we are accepting the merge.
t.apply();
// not using the incoming anim-only surfaces
info.releaseAnimSurfaces();
if (appearedTargets != null) {
try {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.merge: calling onTasksAppeared", mInstanceId);
mListener.onTasksAppeared(appearedTargets);
} catch (RemoteException e) {
Slog.e(TAG, "Error sending appeared tasks to recents animation", e);
}
}
finishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
}
/** For now, just set-up a jump-cut to the new activity. */
private void mergeActivityOnly(TransitionInfo info, SurfaceControl.Transaction t) {
for (int i = 0; i < info.getChanges().size(); ++i) {
final TransitionInfo.Change change = info.getChanges().get(i);
if (TransitionUtil.isOpeningType(change.getMode())) {
t.show(change.getLeash());
t.setAlpha(change.getLeash(), 1.f);
} else if (TransitionUtil.isClosingType(change.getMode())) {
t.hide(change.getLeash());
}
}
}
@Override
public TaskSnapshot screenshotTask(int taskId) {
try {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.screenshotTask: taskId=%d", mInstanceId, taskId);
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) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"RecentsController.setInputConsumerEnabled: skip, cb?=%b enabled?=%b",
mFinishCB != null, enabled);
return;
}
final int displayId = mInfo.getRootCount() > 0 ? mInfo.getRoot(0).getDisplayId()
: Display.DEFAULT_DISPLAY;
// 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 {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.setInputConsumerEnabled: set focus to recents",
mInstanceId);
ActivityTaskManager.getService().focusTopTask(displayId);
} 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) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.setFinishTaskTransaction: taskId=%d",
mInstanceId, taskId);
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;
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.finishInner: toHome=%b userLeave=%b "
+ "willFinishToHome=%b state=%d",
mInstanceId, toHome, sendUserLeaveHint, mWillFinishToHome, mState);
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
// If a recents gesture starts on the 3p launcher, then the 3p launcher is the
// live tile (pausing app). If the gesture is "cancelled" we need to return to
// 3p launcher instead of "task-switching" away from it.
&& (!mWillFinishToHome || mPausingSeparateHome)
&& mPausingTasks != null && mState == STATE_NORMAL) {
if (mPausingSeparateHome) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
" returning to 3p home");
} else {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
" returning to app");
}
// 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) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " 3p launching home");
// 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 {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " normal finish");
// 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) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION,
"[%d] RecentsController.detachNavigationBarFromApp", mInstanceId);
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);
}
}