blob: fa4de16b37f170c65ce63a3e1817a7acde1a1dbd [file] [log] [blame]
/*
* Copyright (C) 2021 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.transition;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_SLEEP;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;
import static android.view.WindowManager.fixScale;
import static android.window.TransitionInfo.FLAG_IS_OCCLUDED;
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
import static android.window.TransitionInfo.FLAG_NO_ANIMATION;
import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS;
import static com.android.wm.shell.util.TransitionUtil.isClosingType;
import static com.android.wm.shell.util.TransitionUtil.isOpeningType;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityTaskManager;
import android.app.IApplicationThread;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.provider.Settings;
import android.util.Log;
import android.util.Pair;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.ITransitionPlayer;
import android.window.RemoteTransition;
import android.window.TransitionFilter;
import android.window.TransitionInfo;
import android.window.TransitionMetrics;
import android.window.TransitionRequestInfo;
import android.window.WindowContainerTransaction;
import android.window.WindowContainerTransactionCallback;
import android.window.WindowOrganizer;
import androidx.annotation.BinderThread;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.common.annotations.ExternalThread;
import com.android.wm.shell.protolog.ShellProtoLogGroup;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.util.TransitionUtil;
import java.util.ArrayList;
import java.util.Arrays;
/**
* Plays transition animations. Within this player, each transition has a lifecycle.
* 1. When a transition is directly started or requested, it is added to "pending" state.
* 2. Once WMCore applies the transition and notifies, the transition moves to "ready" state.
* 3. When a transition starts animating, it is moved to the "active" state.
*
* Basically: --start--> PENDING --onTransitionReady--> READY --play--> ACTIVE --finish--> |
* --merge--> MERGED --^
*
* At the moment, only one transition can be animating at a time. While a transition is animating,
* transitions will be queued in the "ready" state for their turn. At the same time, whenever a
* transition makes it to the head of the "ready" queue, it will attempt to merge to with the
* "active" transition. If the merge succeeds, it will be moved to the "active" transition's
* "merged" and then the next "ready" transition can attempt to merge.
*
* Once the "active" transition animation is finished, it will be removed from the "active" list
* and then the next "ready" transition can play.
*/
public class Transitions implements RemoteCallable<Transitions> {
static final String TAG = "ShellTransitions";
/** Set to {@code true} to enable shell transitions. */
public static final boolean ENABLE_SHELL_TRANSITIONS =
SystemProperties.getBoolean("persist.wm.debug.shell_transit", true);
public static final boolean SHELL_TRANSITIONS_ROTATION = ENABLE_SHELL_TRANSITIONS
&& SystemProperties.getBoolean("persist.wm.debug.shell_transit_rotate", false);
/** Transition type for exiting PIP via the Shell, via pressing the expand button. */
public static final int TRANSIT_EXIT_PIP = TRANSIT_FIRST_CUSTOM + 1;
public static final int TRANSIT_EXIT_PIP_TO_SPLIT = TRANSIT_FIRST_CUSTOM + 2;
/** Transition type for removing PIP via the Shell, either via Dismiss bubble or Close. */
public static final int TRANSIT_REMOVE_PIP = TRANSIT_FIRST_CUSTOM + 3;
/** Transition type for launching 2 tasks simultaneously. */
public static final int TRANSIT_SPLIT_SCREEN_PAIR_OPEN = TRANSIT_FIRST_CUSTOM + 4;
/** Transition type for entering split by opening an app into side-stage. */
public static final int TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE = TRANSIT_FIRST_CUSTOM + 5;
/** Transition type for dismissing split-screen via dragging the divider off the screen. */
public static final int TRANSIT_SPLIT_DISMISS_SNAP = TRANSIT_FIRST_CUSTOM + 6;
/** Transition type for dismissing split-screen. */
public static final int TRANSIT_SPLIT_DISMISS = TRANSIT_FIRST_CUSTOM + 7;
/** Transition type for freeform to maximize transition. */
public static final int TRANSIT_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 8;
/** Transition type for maximize to freeform transition. */
public static final int TRANSIT_RESTORE_FROM_MAXIMIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 9;
/** Transition type to freeform in desktop mode. */
public static final int TRANSIT_ENTER_FREEFORM = WindowManager.TRANSIT_FIRST_CUSTOM + 10;
/** Transition type to freeform in desktop mode. */
public static final int TRANSIT_ENTER_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 11;
/** Transition type to fullscreen from desktop mode. */
public static final int TRANSIT_EXIT_DESKTOP_MODE = WindowManager.TRANSIT_FIRST_CUSTOM + 12;
private final WindowOrganizer mOrganizer;
private final Context mContext;
private final ShellExecutor mMainExecutor;
private final ShellExecutor mAnimExecutor;
private final TransitionPlayerImpl mPlayerImpl;
private final DefaultTransitionHandler mDefaultTransitionHandler;
private final RemoteTransitionHandler mRemoteTransitionHandler;
private final DisplayController mDisplayController;
private final ShellController mShellController;
private final ShellTransitionImpl mImpl = new ShellTransitionImpl();
private final SleepHandler mSleepHandler = new SleepHandler();
private boolean mIsRegistered = false;
/** List of possible handlers. Ordered by specificity (eg. tapped back to front). */
private final ArrayList<TransitionHandler> mHandlers = new ArrayList<>();
private final ArrayList<TransitionObserver> mObservers = new ArrayList<>();
/** List of {@link Runnable} instances to run when the last active transition has finished. */
private final ArrayList<Runnable> mRunWhenIdleQueue = new ArrayList<>();
private float mTransitionAnimationScaleSetting = 1.0f;
/**
* How much time we allow for an animation to finish itself on sleep. If it takes longer, we
* will force-finish it (on this end) which may leave it in a bad state but won't hang the
* device. This needs to be pretty small because it is an allowance for each queued animation,
* however it can't be too small since there is some potential IPC involved.
*/
private static final int SLEEP_ALLOWANCE_MS = 120;
private static final class ActiveTransition {
IBinder mToken;
TransitionHandler mHandler;
boolean mAborted;
TransitionInfo mInfo;
SurfaceControl.Transaction mStartT;
SurfaceControl.Transaction mFinishT;
/** Ordered list of transitions which have been merged into this one. */
private ArrayList<ActiveTransition> mMerged;
@Override
public String toString() {
if (mInfo != null && mInfo.getDebugId() >= 0) {
return "(#" + mInfo.getDebugId() + ")" + mToken;
}
return mToken.toString();
}
}
/** Keeps track of transitions which have been started, but aren't ready yet. */
private final ArrayList<ActiveTransition> mPendingTransitions = new ArrayList<>();
/** Keeps track of transitions which are ready to play but still waiting for their turn. */
private final ArrayList<ActiveTransition> mReadyTransitions = new ArrayList<>();
/** Keeps track of currently playing transitions. For now, there can only be 1 max. */
private final ArrayList<ActiveTransition> mActiveTransitions = new ArrayList<>();
public Transitions(@NonNull Context context,
@NonNull ShellInit shellInit,
@NonNull ShellController shellController,
@NonNull WindowOrganizer organizer,
@NonNull TransactionPool pool,
@NonNull DisplayController displayController,
@NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler,
@NonNull ShellExecutor animExecutor) {
mOrganizer = organizer;
mContext = context;
mMainExecutor = mainExecutor;
mAnimExecutor = animExecutor;
mDisplayController = displayController;
mPlayerImpl = new TransitionPlayerImpl();
mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit,
displayController, pool, mainExecutor, mainHandler, animExecutor);
mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor);
mShellController = shellController;
// The very last handler (0 in the list) should be the default one.
mHandlers.add(mDefaultTransitionHandler);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Default");
// Next lowest priority is remote transitions.
mHandlers.add(mRemoteTransitionHandler);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: Remote");
shellInit.addInitCallback(this::onInit, this);
}
private void onInit() {
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
mOrganizer.shareTransactionQueue();
}
mShellController.addExternalInterface(KEY_EXTRA_SHELL_SHELL_TRANSITIONS,
this::createExternalInterface, this);
ContentResolver resolver = mContext.getContentResolver();
mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
dispatchAnimScaleSetting(mTransitionAnimationScaleSetting);
resolver.registerContentObserver(
Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false,
new SettingsObserver());
if (Transitions.ENABLE_SHELL_TRANSITIONS) {
mIsRegistered = true;
// Register this transition handler with Core
try {
mOrganizer.registerTransitionPlayer(mPlayerImpl);
} catch (RuntimeException e) {
mIsRegistered = false;
throw e;
}
// Pre-load the instance.
TransitionMetrics.getInstance();
}
}
public boolean isRegistered() {
return mIsRegistered;
}
private float getTransitionAnimationScaleSetting() {
return fixScale(Settings.Global.getFloat(mContext.getContentResolver(),
Settings.Global.TRANSITION_ANIMATION_SCALE, mContext.getResources().getFloat(
R.dimen.config_appTransitionAnimationDurationScaleDefault)));
}
public ShellTransitions asRemoteTransitions() {
return mImpl;
}
private ExternalInterfaceBinder createExternalInterface() {
return new IShellTransitionsImpl(this);
}
@Override
public Context getContext() {
return mContext;
}
@Override
public ShellExecutor getRemoteCallExecutor() {
return mMainExecutor;
}
private void dispatchAnimScaleSetting(float scale) {
for (int i = mHandlers.size() - 1; i >= 0; --i) {
mHandlers.get(i).setAnimScaleSetting(scale);
}
}
/**
* Adds a handler candidate.
* @see TransitionHandler
*/
public void addHandler(@NonNull TransitionHandler handler) {
if (mHandlers.isEmpty()) {
throw new RuntimeException("Unexpected handler added prior to initialization, please "
+ "use ShellInit callbacks to ensure proper ordering");
}
mHandlers.add(handler);
// Set initial scale settings.
handler.setAnimScaleSetting(mTransitionAnimationScaleSetting);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "addHandler: %s",
handler.getClass().getSimpleName());
}
public ShellExecutor getMainExecutor() {
return mMainExecutor;
}
public ShellExecutor getAnimExecutor() {
return mAnimExecutor;
}
/** Only use this in tests. This is used to avoid running animations during tests. */
@VisibleForTesting
void replaceDefaultHandlerForTest(TransitionHandler handler) {
mHandlers.set(0, handler);
}
/** Register a remote transition to be used when `filter` matches an incoming transition */
public void registerRemote(@NonNull TransitionFilter filter,
@NonNull RemoteTransition remoteTransition) {
mRemoteTransitionHandler.addFiltered(filter, remoteTransition);
}
/** Unregisters a remote transition and all associated filters */
public void unregisterRemote(@NonNull RemoteTransition remoteTransition) {
mRemoteTransitionHandler.removeFiltered(remoteTransition);
}
RemoteTransitionHandler getRemoteTransitionHandler() {
return mRemoteTransitionHandler;
}
/** Registers an observer on the lifecycle of transitions. */
public void registerObserver(@NonNull TransitionObserver observer) {
mObservers.add(observer);
}
/** Unregisters the observer. */
public void unregisterObserver(@NonNull TransitionObserver observer) {
mObservers.remove(observer);
}
/** Boosts the process priority of remote animation player. */
public static void setRunningRemoteTransitionDelegate(IApplicationThread appThread) {
if (appThread == null) return;
try {
ActivityTaskManager.getService().setRunningRemoteTransitionDelegate(appThread);
} catch (SecurityException e) {
Log.e(TAG, "Unable to boost animation process. This should only happen"
+ " during unit tests");
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Runs the given {@code runnable} when the last active transition has finished, or immediately
* if there are currently no active transitions.
*
* <p>This method should be called on the Shell main-thread, where the given {@code runnable}
* will be executed when the last active transition is finished.
*/
public void runOnIdle(Runnable runnable) {
if (mActiveTransitions.isEmpty() && mPendingTransitions.isEmpty()
&& mReadyTransitions.isEmpty()) {
runnable.run();
} else {
mRunWhenIdleQueue.add(runnable);
}
}
/**
* Sets up visibility/alpha/transforms to resemble the starting state of an animation.
*/
private static void setupStartState(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) {
boolean isOpening = isOpeningType(info.getType());
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
if (change.hasFlags(TransitionInfo.FLAGS_IS_NON_APP_WINDOW)) {
// Currently system windows are controlled by WindowState, so don't change their
// surfaces. Otherwise their surfaces could be hidden or cropped unexpectedly.
// This includes Wallpaper (always z-ordered at bottom) and IME (associated with
// app), because there may not be a transition associated with their visibility
// changes, and currently they don't need transition animation.
continue;
}
final SurfaceControl leash = change.getLeash();
final int mode = info.getChanges().get(i).getMode();
if (mode == TRANSIT_TO_FRONT
&& ((change.getStartAbsBounds().height() != change.getEndAbsBounds().height()
|| change.getStartAbsBounds().width() != change.getEndAbsBounds().width()))) {
// When the window is moved to front with a different size, make sure the crop is
// updated to prevent it from using the old crop.
t.setWindowCrop(leash, change.getEndAbsBounds().width(),
change.getEndAbsBounds().height());
}
// Don't move anything that isn't independent within its parents
if (!TransitionInfo.isIndependent(change, info)) {
if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT || mode == TRANSIT_CHANGE) {
t.show(leash);
t.setMatrix(leash, 1, 0, 0, 1);
t.setAlpha(leash, 1.f);
t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y);
}
continue;
}
if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
t.show(leash);
t.setMatrix(leash, 1, 0, 0, 1);
if (isOpening
// If this is a transferred starting window, we want it immediately visible.
&& (change.getFlags() & FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT) == 0) {
t.setAlpha(leash, 0.f);
}
} else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
finishT.hide(leash);
}
}
}
/**
* Reparents all participants into a shared parent and orders them based on: the global transit
* type, their transit mode, and their destination z-order.
*/
private static void setupAnimHierarchy(@NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) {
boolean isOpening = isOpeningType(info.getType());
for (int i = 0; i < info.getRootCount(); ++i) {
t.show(info.getRoot(i).getLeash());
}
final int numChanges = info.getChanges().size();
// Put animating stuff above this line and put static stuff below it.
final int zSplitLine = numChanges + 1;
// changes should be ordered top-to-bottom in z
for (int i = numChanges - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
final SurfaceControl leash = change.getLeash();
final int mode = change.getMode();
// Don't reparent anything that isn't independent within its parents
if (!TransitionInfo.isIndependent(change, info)) {
continue;
}
boolean hasParent = change.getParent() != null;
final int rootIdx = TransitionUtil.rootIndexFor(change, info);
if (!hasParent) {
t.reparent(leash, info.getRoot(rootIdx).getLeash());
t.setPosition(leash,
change.getStartAbsBounds().left - info.getRoot(rootIdx).getOffset().x,
change.getStartAbsBounds().top - info.getRoot(rootIdx).getOffset().y);
}
final int layer;
// Put all the OPEN/SHOW on top
if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) {
// Wallpaper is always at the bottom.
layer = -zSplitLine;
} else if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
if (isOpening) {
// put on top
layer = zSplitLine + numChanges - i;
} else {
// put on bottom
layer = zSplitLine - i;
}
} else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) {
if (isOpening) {
// put on bottom and leave visible
layer = zSplitLine - i;
} else {
// put on top
layer = zSplitLine + numChanges - i;
}
} else { // CHANGE or other
layer = zSplitLine + numChanges - i;
}
t.setLayer(leash, layer);
}
}
private static int findByToken(ArrayList<ActiveTransition> list, IBinder token) {
for (int i = list.size() - 1; i >= 0; --i) {
if (list.get(i).mToken == token) return i;
}
return -1;
}
/**
* Look through a transition and see if all non-closing changes are no-animation. If so, no
* animation should play.
*/
static boolean isAllNoAnimation(TransitionInfo info) {
if (isClosingType(info.getType())) {
// no-animation is only relevant for launching (open) activities.
return false;
}
boolean hasNoAnimation = false;
final int changeSize = info.getChanges().size();
for (int i = changeSize - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
if (isClosingType(change.getMode())) {
// ignore closing apps since they are a side-effect of the transition and don't
// animate.
continue;
}
if (change.hasFlags(FLAG_NO_ANIMATION)) {
hasNoAnimation = true;
} else {
// at-least one relevant participant *is* animated, so we need to animate.
return false;
}
}
return hasNoAnimation;
}
/**
* Check if all changes in this transition are only ordering changes. If so, we won't animate.
*/
static boolean isAllOrderOnly(TransitionInfo info) {
for (int i = info.getChanges().size() - 1; i >= 0; --i) {
if (!TransitionUtil.isOrderOnly(info.getChanges().get(i))) return false;
}
return true;
}
@VisibleForTesting
void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady %s: %s",
transitionToken, info);
final int activeIdx = findByToken(mPendingTransitions, transitionToken);
if (activeIdx < 0) {
throw new IllegalStateException("Got transitionReady for non-pending transition "
+ transitionToken + ". expecting one of "
+ Arrays.toString(mPendingTransitions.stream().map(
activeTransition -> activeTransition.mToken).toArray()));
}
if (activeIdx > 0) {
Log.e(TAG, "Transition became ready out-of-order " + mPendingTransitions.get(activeIdx)
+ ". Expected order: " + Arrays.toString(mPendingTransitions.stream().map(
activeTransition -> activeTransition.mToken).toArray()));
}
// Move from pending to ready
final ActiveTransition active = mPendingTransitions.remove(activeIdx);
mReadyTransitions.add(active);
active.mInfo = info;
active.mStartT = t;
active.mFinishT = finishT;
for (int i = 0; i < mObservers.size(); ++i) {
mObservers.get(i).onTransitionReady(transitionToken, info, t, finishT);
}
if (info.getType() == TRANSIT_SLEEP) {
if (activeIdx > 0 || !mActiveTransitions.isEmpty() || mReadyTransitions.size() > 1) {
// Sleep starts a process of forcing all prior transitions to finish immediately
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Start finish-for-sleep");
finishForSleep(null /* forceFinish */);
return;
}
}
if (info.getRootCount() == 0 && !alwaysReportToKeyguard(info)) {
// No root-leashes implies that the transition is empty/no-op, so just do
// housekeeping and return.
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "No transition roots in %s so"
+ " abort", active);
onAbort(active);
return;
}
final int changeSize = info.getChanges().size();
boolean taskChange = false;
boolean transferStartingWindow = false;
boolean allOccluded = changeSize > 0;
for (int i = changeSize - 1; i >= 0; --i) {
final TransitionInfo.Change change = info.getChanges().get(i);
taskChange |= change.getTaskInfo() != null;
transferStartingWindow |= change.hasFlags(FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT);
if (!change.hasFlags(FLAG_IS_OCCLUDED)) {
allOccluded = false;
}
}
// There does not need animation when:
// A. Transfer starting window. Apply transfer starting window directly if there is no other
// task change. Since this is an activity->activity situation, we can detect it by selecting
// transitions with only 2 changes where neither are tasks and one is a starting-window
// recipient.
if (!taskChange && transferStartingWindow && changeSize == 2
// B. It's visibility change if the TRANSIT_TO_BACK/TO_FRONT happened when all
// changes are underneath another change.
|| ((info.getType() == TRANSIT_TO_BACK || info.getType() == TRANSIT_TO_FRONT)
&& allOccluded)) {
// Treat this as an abort since we are bypassing any merge logic and effectively
// finishing immediately.
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
"Non-visible anim so abort: %s", active);
onAbort(active);
return;
}
setupStartState(active.mInfo, active.mStartT, active.mFinishT);
if (mReadyTransitions.size() > 1) {
// There are already transitions waiting in the queue, so just return.
return;
}
processReadyQueue();
}
/**
* Some transitions we always need to report to keyguard even if they are empty.
* TODO (b/274954192): Remove this once keyguard dispatching moves to Shell.
*/
private static boolean alwaysReportToKeyguard(TransitionInfo info) {
// occlusion status of activities can change while screen is off so there will be no
// visibility change but we still need keyguardservice to be notified.
if (info.getType() == TRANSIT_KEYGUARD_UNOCCLUDE) return true;
// It's possible for some activities to stop with bad timing (esp. since we can't yet
// queue activity transitions initiated by apps) that results in an empty transition for
// keyguard going-away. In general, we should should always report Keyguard-going-away.
if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_GOING_AWAY) != 0) return true;
return false;
}
void processReadyQueue() {
if (mReadyTransitions.isEmpty()) {
// Check if idle.
if (mActiveTransitions.isEmpty() && mPendingTransitions.isEmpty()) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition "
+ "animations finished");
// Run all runnables from the run-when-idle queue.
for (int i = 0; i < mRunWhenIdleQueue.size(); i++) {
mRunWhenIdleQueue.get(i).run();
}
mRunWhenIdleQueue.clear();
}
return;
}
final ActiveTransition ready = mReadyTransitions.get(0);
if (mActiveTransitions.isEmpty()) {
// The normal case, just play it (currently we only support 1 active transition).
mReadyTransitions.remove(0);
mActiveTransitions.add(ready);
if (ready.mAborted) {
// finish now since there's nothing to animate. Calls back into processReadyQueue
onFinish(ready, null, null);
return;
}
playTransition(ready);
// Attempt to merge any more queued-up transitions.
processReadyQueue();
return;
}
// An existing animation is playing, so see if we can merge.
final ActiveTransition playing = mActiveTransitions.get(0);
if (ready.mAborted) {
// record as merged since it is no-op. Calls back into processReadyQueue
onMerged(playing, ready);
return;
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition %s ready while"
+ " %s is still animating. Notify the animating transition"
+ " in case they can be merged", ready, playing);
playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT,
playing.mToken, (wct, cb) -> onMerged(playing, ready));
}
private void onMerged(@NonNull ActiveTransition playing, @NonNull ActiveTransition merged) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s into %s",
merged, playing);
int readyIdx = 0;
if (mReadyTransitions.isEmpty() || mReadyTransitions.get(0) != merged) {
Log.e(TAG, "Merged transition out-of-order? " + merged);
readyIdx = mReadyTransitions.indexOf(merged);
if (readyIdx < 0) {
Log.e(TAG, "Merged a transition that is no-longer queued? " + merged);
return;
}
}
mReadyTransitions.remove(readyIdx);
if (playing.mMerged == null) {
playing.mMerged = new ArrayList<>();
}
playing.mMerged.add(merged);
// if it was aborted, then onConsumed has already been reported.
if (merged.mHandler != null && !merged.mAborted) {
merged.mHandler.onTransitionConsumed(merged.mToken, false /* abort */, merged.mFinishT);
}
for (int i = 0; i < mObservers.size(); ++i) {
mObservers.get(i).onTransitionMerged(merged.mToken, playing.mToken);
}
// See if we should merge another transition.
processReadyQueue();
}
private void playTransition(@NonNull ActiveTransition active) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Playing animation for %s", active);
for (int i = 0; i < mObservers.size(); ++i) {
mObservers.get(i).onTransitionStarting(active.mToken);
}
setupAnimHierarchy(active.mInfo, active.mStartT, active.mFinishT);
// If a handler already chose to run this animation, try delegating to it first.
if (active.mHandler != null) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try firstHandler %s",
active.mHandler);
boolean consumed = active.mHandler.startAnimation(active.mToken, active.mInfo,
active.mStartT, active.mFinishT, (wct, cb) -> onFinish(active, wct, cb));
if (consumed) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler");
return;
}
}
// Otherwise give every other handler a chance
active.mHandler = dispatchTransition(active.mToken, active.mInfo, active.mStartT,
active.mFinishT, (wct, cb) -> onFinish(active, wct, cb), active.mHandler);
}
/**
* Gives every handler (in order) a chance to animate until one consumes the transition.
* @return the handler which consumed the transition.
*/
TransitionHandler dispatchTransition(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT,
@NonNull TransitionFinishCallback finishCB, @Nullable TransitionHandler skip) {
for (int i = mHandlers.size() - 1; i >= 0; --i) {
if (mHandlers.get(i) == skip) continue;
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " try handler %s",
mHandlers.get(i));
boolean consumed = mHandlers.get(i).startAnimation(transition, info, startT, finishT,
finishCB);
if (consumed) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by %s",
mHandlers.get(i));
return mHandlers.get(i);
}
}
throw new IllegalStateException(
"This shouldn't happen, maybe the default handler is broken.");
}
/**
* Gives every handler (in order) a chance to handle request until one consumes the transition.
* @return the WindowContainerTransaction given by the handler which consumed the transition.
*/
public Pair<TransitionHandler, WindowContainerTransaction> dispatchRequest(
@NonNull IBinder transition, @NonNull TransitionRequestInfo request,
@Nullable TransitionHandler skip) {
for (int i = mHandlers.size() - 1; i >= 0; --i) {
if (mHandlers.get(i) == skip) continue;
WindowContainerTransaction wct = mHandlers.get(i).handleRequest(transition, request);
if (wct != null) {
return new Pair<>(mHandlers.get(i), wct);
}
}
return null;
}
/** Aborts a transition. This will still queue it up to maintain order. */
private void onAbort(ActiveTransition transition) {
// apply immediately since they may be "parallel" operations: We currently we use abort for
// thing which are independent to other transitions (like starting-window transfer).
transition.mStartT.apply();
transition.mFinishT.apply();
transition.mAborted = true;
if (transition.mHandler != null) {
// Notifies to clean-up the aborted transition.
transition.mHandler.onTransitionConsumed(
transition.mToken, true /* aborted */, null /* finishTransaction */);
}
releaseSurfaces(transition.mInfo);
// This still went into the queue (to maintain the correct finish ordering).
if (mReadyTransitions.size() > 1) {
// There are already transitions waiting in the queue, so just return.
return;
}
processReadyQueue();
}
/**
* Releases an info's animation-surfaces. These don't need to persist and we need to release
* them asap so that SF can free memory sooner.
*/
private void releaseSurfaces(@Nullable TransitionInfo info) {
if (info == null) return;
info.releaseAnimSurfaces();
}
private void onFinish(ActiveTransition active,
@Nullable WindowContainerTransaction wct,
@Nullable WindowContainerTransactionCallback wctCB) {
int activeIdx = mActiveTransitions.indexOf(active);
if (activeIdx < 0) {
Log.e(TAG, "Trying to finish a non-running transition. Either remote crashed or "
+ " a handler didn't properly deal with a merge. " + active,
new RuntimeException());
return;
} else if (activeIdx != 0) {
// Relevant right now since we only allow 1 active transition at a time.
Log.e(TAG, "Finishing a transition out of order. " + active);
}
mActiveTransitions.remove(activeIdx);
for (int i = 0; i < mObservers.size(); ++i) {
mObservers.get(i).onTransitionFinished(active.mToken, active.mAborted);
}
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition animation finished "
+ "(aborted=%b), notifying core %s", active.mAborted, active);
if (active.mStartT != null) {
// Applied by now, so clear immediately to remove any references. Do not set to null
// yet, though, since nullness is used later to disambiguate malformed transitions.
active.mStartT.clear();
}
// Merge all associated transactions together
SurfaceControl.Transaction fullFinish = active.mFinishT;
if (active.mMerged != null) {
for (int iM = 0; iM < active.mMerged.size(); ++iM) {
final ActiveTransition toMerge = active.mMerged.get(iM);
// Include start. It will be a no-op if it was already applied. Otherwise, we need
// it to maintain consistent state.
if (toMerge.mStartT != null) {
if (fullFinish == null) {
fullFinish = toMerge.mStartT;
} else {
fullFinish.merge(toMerge.mStartT);
}
}
if (toMerge.mFinishT != null) {
if (fullFinish == null) {
fullFinish = toMerge.mFinishT;
} else {
fullFinish.merge(toMerge.mFinishT);
}
}
}
}
if (fullFinish != null) {
fullFinish.apply();
}
// Now perform all the finish callbacks (starting with the playing one and then all the
// transitions merged into it).
releaseSurfaces(active.mInfo);
mOrganizer.finishTransition(active.mToken, wct, wctCB);
if (active.mMerged != null) {
for (int iM = 0; iM < active.mMerged.size(); ++iM) {
ActiveTransition merged = active.mMerged.get(iM);
mOrganizer.finishTransition(merged.mToken, null /* wct */, null /* wctCB */);
releaseSurfaces(merged.mInfo);
}
active.mMerged.clear();
}
// Now that this is done, check the ready queue for more work.
processReadyQueue();
}
private boolean isTransitionKnown(IBinder token) {
for (int i = 0; i < mPendingTransitions.size(); ++i) {
if (mPendingTransitions.get(i).mToken == token) return true;
}
for (int i = 0; i < mReadyTransitions.size(); ++i) {
if (mReadyTransitions.get(i).mToken == token) return true;
}
for (int i = 0; i < mActiveTransitions.size(); ++i) {
final ActiveTransition active = mActiveTransitions.get(i);
if (active.mToken == token) return true;
if (active.mMerged == null) continue;
for (int m = 0; m < active.mMerged.size(); ++m) {
if (active.mMerged.get(m).mToken == token) return true;
}
}
return false;
}
void requestStartTransition(@NonNull IBinder transitionToken,
@Nullable TransitionRequestInfo request) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested: %s %s",
transitionToken, request);
if (isTransitionKnown(transitionToken)) {
throw new RuntimeException("Transition already started " + transitionToken);
}
final ActiveTransition active = new ActiveTransition();
WindowContainerTransaction wct = null;
// If we have sleep, we use a special handler and we try to finish everything ASAP.
if (request.getType() == TRANSIT_SLEEP) {
mSleepHandler.handleRequest(transitionToken, request);
active.mHandler = mSleepHandler;
} else {
for (int i = mHandlers.size() - 1; i >= 0; --i) {
wct = mHandlers.get(i).handleRequest(transitionToken, request);
if (wct != null) {
active.mHandler = mHandlers.get(i);
break;
}
}
if (request.getDisplayChange() != null) {
TransitionRequestInfo.DisplayChange change = request.getDisplayChange();
if (change.getEndRotation() != change.getStartRotation()) {
// Is a rotation, so dispatch to all displayChange listeners
if (wct == null) {
wct = new WindowContainerTransaction();
}
mDisplayController.getChangeController().dispatchOnDisplayChange(wct,
change.getDisplayId(), change.getStartRotation(),
change.getEndRotation(), null /* newDisplayAreaInfo */);
}
}
}
mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct);
active.mToken = transitionToken;
// Currently, WMCore only does one transition at a time. If it makes a requestStart, it
// is already collecting that transition on core-side, so it will be the next one to
// become ready. There may already be pending transitions added as part of direct
// `startNewTransition` but if we have a request now, it means WM created the request
// transition before it acknowledged any of the pending `startNew` transitions. So, insert
// it at the front.
mPendingTransitions.add(0, active);
}
/** Start a new transition directly. */
public IBinder startTransition(@WindowManager.TransitionType int type,
@NonNull WindowContainerTransaction wct, @Nullable TransitionHandler handler) {
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Directly starting a new transition "
+ "type=%d wct=%s handler=%s", type, wct, handler);
final ActiveTransition active = new ActiveTransition();
active.mHandler = handler;
active.mToken = mOrganizer.startNewTransition(type, wct);
mPendingTransitions.add(active);
return active.mToken;
}
/**
* Finish running animations (almost) immediately when a SLEEP transition comes in. We use this
* as both a way to reduce unnecessary work (animations not visible while screen off) and as a
* failsafe to unblock "stuck" animations (in particular remote animations).
*
* This works by "merging" the sleep transition into the currently-playing transition (even if
* its out-of-order) -- turning SLEEP into a signal. If the playing transition doesn't finish
* within `SLEEP_ALLOWANCE_MS` from this merge attempt, this will then finish it directly (and
* send an abort/consumed message).
*
* This is then repeated until there are no more pending sleep transitions.
*
* @param forceFinish When non-null, this is the transition that we last sent the SLEEP merge
* signal to -- so it will be force-finished if it's still running.
*/
private void finishForSleep(@Nullable ActiveTransition forceFinish) {
if ((mActiveTransitions.isEmpty() && mReadyTransitions.isEmpty())
|| mSleepHandler.mSleepTransitions.isEmpty()) {
// Done finishing things.
// Prevent any weird leaks... shouldn't happen though.
mSleepHandler.mSleepTransitions.clear();
return;
}
if (forceFinish != null && mActiveTransitions.contains(forceFinish)) {
Log.e(TAG, "Forcing transition to finish due to sleep timeout: " + forceFinish);
forceFinish.mAborted = true;
// Last notify of it being consumed. Note: mHandler should never be null,
// but check just to be safe.
if (forceFinish.mHandler != null) {
forceFinish.mHandler.onTransitionConsumed(
forceFinish.mToken, true /* aborted */, null /* finishTransaction */);
}
onFinish(forceFinish, null, null);
}
final SurfaceControl.Transaction dummyT = new SurfaceControl.Transaction();
final TransitionInfo dummyInfo = new TransitionInfo(TRANSIT_SLEEP, 0 /* flags */);
while (!mActiveTransitions.isEmpty() && !mSleepHandler.mSleepTransitions.isEmpty()) {
final ActiveTransition playing = mActiveTransitions.get(0);
int sleepIdx = findByToken(mReadyTransitions, mSleepHandler.mSleepTransitions.get(0));
if (sleepIdx >= 0) {
// Try to signal that we are sleeping by attempting to merge the sleep transition
// into the playing one.
final ActiveTransition nextSleep = mReadyTransitions.get(sleepIdx);
ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt to merge SLEEP %s"
+ " into %s", nextSleep, playing);
playing.mHandler.mergeAnimation(nextSleep.mToken, dummyInfo, dummyT,
playing.mToken, (wct, cb) -> {});
} else {
Log.e(TAG, "Couldn't find sleep transition in ready list: "
+ mSleepHandler.mSleepTransitions.get(0));
}
// it's possible to complete immediately. If that happens, just repeat the signal
// loop until we either finish everything or start playing an animation that isn't
// finishing immediately.
if (!mActiveTransitions.isEmpty() && mActiveTransitions.get(0) == playing) {
// Give it a (very) short amount of time to process it before forcing.
mMainExecutor.executeDelayed(() -> finishForSleep(playing), SLEEP_ALLOWANCE_MS);
break;
}
}
}
/**
* Interface for a callback that must be called after a TransitionHandler finishes playing an
* animation.
*/
public interface TransitionFinishCallback {
/**
* This must be called on the main thread when a transition finishes playing an animation.
* The transition must not touch the surfaces after this has been called.
*
* @param wct A WindowContainerTransaction to run along with the transition clean-up.
* @param wctCB A sync callback that will be run when the transition clean-up is done and
* wct has been applied.
*/
void onTransitionFinished(@Nullable WindowContainerTransaction wct,
@Nullable WindowContainerTransactionCallback wctCB);
}
/**
* Interface for something which can handle a subset of transitions.
*/
public interface TransitionHandler {
/**
* Starts a transition animation. This is always called if handleRequest returned non-null
* for a particular transition. Otherwise, it is only called if no other handler before
* it handled the transition.
* @param startTransaction the transaction given to the handler to be applied before the
* transition animation. Note the handler is expected to call on
* {@link SurfaceControl.Transaction#apply()} for startTransaction.
* @param finishTransaction the transaction given to the handler to be applied after the
* transition animation. Unlike startTransaction, the handler is NOT
* expected to apply this transaction. The Transition system will
* apply it when finishCallback is called.
* @param finishCallback Call this when finished. This MUST be called on main thread.
* @return true if transition was handled, false if not (falls-back to default).
*/
boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction,
@NonNull TransitionFinishCallback finishCallback);
/**
* Attempts to merge a different transition's animation into an animation that this handler
* is currently playing. If a merge is not possible/supported, this should be a no-op.
*
* This gets called if another transition becomes ready while this handler is still playing
* an animation. This is called regardless of whether this handler claims to support that
* particular transition or not.
*
* When this happens, there are 2 options:
* 1. Do nothing. This effectively rejects the merge request. This is the "safest" option.
* 2. Merge the incoming transition into this one. The implementation is up to this
* handler. To indicate that this handler has "consumed" the merge transition, it
* must call the finishCallback immediately, or at-least before the original
* transition's finishCallback is called.
*
* @param transition This is the transition that wants to be merged.
* @param info Information about what is changing in the transition.
* @param t Contains surface changes that resulted from the transition.
* @param mergeTarget This is the transition that we are attempting to merge with (ie. the
* one this handler is currently already animating).
* @param finishCallback Call this if merged. This MUST be called on main thread.
*/
default void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
@NonNull TransitionFinishCallback finishCallback) { }
/**
* Potentially handles a startTransition request.
*
* @param transition The transition whose start is being requested.
* @param request Information about what is requested.
* @return WCT to apply with transition-start or null. If a WCT is returned here, this
* handler will be the first in line to animate.
*/
@Nullable
WindowContainerTransaction handleRequest(@NonNull IBinder transition,
@NonNull TransitionRequestInfo request);
/**
* Called when a transition which was already "claimed" by this handler has been merged
* into another animation or has been aborted. Gives this handler a chance to clean-up any
* expectations.
*
* @param transition The transition been consumed.
* @param aborted Whether the transition is aborted or not.
* @param finishTransaction The transaction to be applied after the transition animated.
*/
default void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
@Nullable SurfaceControl.Transaction finishTransaction) { }
/**
* Sets transition animation scale settings value to handler.
*
* @param scale The setting value of transition animation scale.
*/
default void setAnimScaleSetting(float scale) {}
}
/**
* Interface for something that needs to know the lifecycle of some transitions, but never
* handles any transition by itself.
*/
public interface TransitionObserver {
/**
* Called when the transition is ready to play. It may later be merged into other
* transitions. Note this doesn't mean this transition will be played anytime soon.
*
* @param transition the unique token of this transition
* @param startTransaction the transaction given to the handler to be applied before the
* transition animation. This will be applied when the transition
* handler that handles this transition starts the transition.
* @param finishTransaction the transaction given to the handler to be applied after the
* transition animation. The Transition system will apply it when
* finishCallback is called by the transition handler.
*/
void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info,
@NonNull SurfaceControl.Transaction startTransaction,
@NonNull SurfaceControl.Transaction finishTransaction);
/**
* Called when the transition is starting to play. It isn't called for merged transitions.
*
* @param transition the unique token of this transition
*/
void onTransitionStarting(@NonNull IBinder transition);
/**
* Called when a transition is merged into another transition. There won't be any following
* lifecycle calls for the merged transition.
*
* @param merged the unique token of the transition that's merged to another one
* @param playing the unique token of the transition that accepts the merge
*/
void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing);
/**
* Called when the transition is finished. This isn't called for merged transitions.
*
* @param transition the unique token of this transition
* @param aborted {@code true} if this transition is aborted; {@code false} otherwise.
*/
void onTransitionFinished(@NonNull IBinder transition, boolean aborted);
}
@BinderThread
private class TransitionPlayerImpl extends ITransitionPlayer.Stub {
@Override
public void onTransitionReady(IBinder iBinder, TransitionInfo transitionInfo,
SurfaceControl.Transaction t, SurfaceControl.Transaction finishT)
throws RemoteException {
mMainExecutor.execute(() -> Transitions.this.onTransitionReady(
iBinder, transitionInfo, t, finishT));
}
@Override
public void requestStartTransition(IBinder iBinder,
TransitionRequestInfo request) throws RemoteException {
mMainExecutor.execute(() -> Transitions.this.requestStartTransition(iBinder, request));
}
}
/**
* The interface for calls from outside the Shell, within the host process.
*/
@ExternalThread
private class ShellTransitionImpl implements ShellTransitions {
@Override
public void registerRemote(@NonNull TransitionFilter filter,
@NonNull RemoteTransition remoteTransition) {
mMainExecutor.execute(() -> {
mRemoteTransitionHandler.addFiltered(filter, remoteTransition);
});
}
@Override
public void unregisterRemote(@NonNull RemoteTransition remoteTransition) {
mMainExecutor.execute(() -> {
mRemoteTransitionHandler.removeFiltered(remoteTransition);
});
}
}
/**
* The interface for calls from outside the host process.
*/
@BinderThread
private static class IShellTransitionsImpl extends IShellTransitions.Stub
implements ExternalInterfaceBinder {
private Transitions mTransitions;
IShellTransitionsImpl(Transitions transitions) {
mTransitions = transitions;
}
/**
* Invalidates this instance, preventing future calls from updating the controller.
*/
@Override
public void invalidate() {
mTransitions = null;
}
@Override
public void registerRemote(@NonNull TransitionFilter filter,
@NonNull RemoteTransition remoteTransition) {
executeRemoteCallWithTaskPermission(mTransitions, "registerRemote",
(transitions) -> {
transitions.mRemoteTransitionHandler.addFiltered(filter, remoteTransition);
});
}
@Override
public void unregisterRemote(@NonNull RemoteTransition remoteTransition) {
executeRemoteCallWithTaskPermission(mTransitions, "unregisterRemote",
(transitions) -> {
transitions.mRemoteTransitionHandler.removeFiltered(remoteTransition);
});
}
@Override
public IBinder getShellApplyToken() {
return SurfaceControl.Transaction.getDefaultApplyToken();
}
}
private class SettingsObserver extends ContentObserver {
SettingsObserver() {
super(null);
}
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting();
mMainExecutor.execute(() -> dispatchAnimScaleSetting(mTransitionAnimationScaleSetting));
}
}
}