/* * 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.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; 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_OCCLUDING; import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; 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.FLAGS_IS_NON_APP_WINDOW; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; import static android.window.TransitionInfo.FLAG_IS_BEHIND_STARTING_WINDOW; 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.window.flags.Flags.ensureWallpaperInTransitions; import static com.android.systemui.shared.Flags.returnAnimationFrameworkLibrary; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_SHELL_TRANSITIONS; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityTaskManager; import android.app.AppGlobals; import android.app.IApplicationThread; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; 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.ArrayMap; 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.TaskFragmentOrganizer; import android.window.TransitionFilter; import android.window.TransitionInfo; import android.window.TransitionMetrics; import android.window.TransitionRequestInfo; import android.window.WindowAnimationState; import android.window.WindowContainerTransaction; 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.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; 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.keyguard.KeyguardTransitionHandler; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.IHomeTransitionListener; import com.android.wm.shell.shared.IShellTransitions; import com.android.wm.shell.shared.ShellTransitions; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.shared.annotations.ExternalThread; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.tracing.LegacyTransitionTracer; import com.android.wm.shell.transition.tracing.PerfettoTransitionTracer; import com.android.wm.shell.transition.tracing.TransitionTracer; import java.io.PrintWriter; 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 --^ * * The READY and beyond lifecycle is managed per "track". Within a track, all the animations are * serialized as described; however, multiple tracks can play simultaneously. This implies that, * within a track, only one transition can be animating ("active") at a time. * * While a transition is animating in a track, transitions dispatched to the track 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" list and then the next * "ready" transition can attempt to merge. Once the "active" transition animation is finished, * the next "ready" transition can play. * * Track assignments are expected to be provided by WMCore and this generally tries to maintain * the same assignments. If, however, WMCore decides that a transition conflicts with >1 active * track, it will be marked as SYNC. This means that all currently active tracks must be flushed * before the SYNC transition can play. */ public class Transitions implements RemoteCallable, ShellCommandHandler.ShellCommandActionHandler { static final String TAG = "ShellTransitions"; /** Set to {@code true} to enable shell transitions. */ public static final boolean ENABLE_SHELL_TRANSITIONS = getShellTransitEnabled(); 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 for starting the drag to desktop mode. */ public static final int TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 10; /** Transition type for finalizing the drag to desktop mode. */ public static final int TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 11; /** Transition type to cancel the drag to desktop mode. */ public static final int TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 13; /** Transition type to animate the toggle resize between the max and default desktop sizes. */ public static final int TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE = WindowManager.TRANSIT_FIRST_CUSTOM + 14; /** Transition to resize PiP task. */ public static final int TRANSIT_RESIZE_PIP = TRANSIT_FIRST_CUSTOM + 16; /** * The task fragment drag resize transition used by activity embedding. */ public static final int TRANSIT_TASK_FRAGMENT_DRAG_RESIZE = // TRANSIT_FIRST_CUSTOM + 17 TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_DRAG_RESIZE; /** Remote Transition that split accepts but ultimately needs to be animated by the remote. */ public static final int TRANSIT_SPLIT_PASSTHROUGH = TRANSIT_FIRST_CUSTOM + 18; /** Transition type for desktop mode transitions. */ public static final int TRANSIT_DESKTOP_MODE_TYPES = WindowManager.TRANSIT_FIRST_CUSTOM + 100; private final ShellTaskOrganizer 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 ShellCommandHandler mShellCommandHandler; private final ShellController mShellController; private final ShellTransitionImpl mImpl = new ShellTransitionImpl(); private final SleepHandler mSleepHandler = new SleepHandler(); private final TransitionTracer mTransitionTracer; private boolean mIsRegistered = false; /** List of possible handlers. Ordered by specificity (eg. tapped back to front). */ private final ArrayList mHandlers = new ArrayList<>(); private final ArrayList mObservers = new ArrayList<>(); private HomeTransitionObserver mHomeTransitionObserver; /** List of {@link Runnable} instances to run when the last active transition has finished. */ private final ArrayList mRunWhenIdleQueue = new ArrayList<>(); private float mTransitionAnimationScaleSetting = 1.0f; /** * How much time we allow for an animation to finish itself on sync. 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 SYNC_ALLOWANCE_MS = 120; /** For testing only. Disables the force-finish timeout on sync. */ private boolean mDisableForceSync = false; private static final class ActiveTransition { final 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 mMerged; ActiveTransition(IBinder token) { mToken = token; } boolean isSync() { return (mInfo.getFlags() & TransitionInfo.FLAG_SYNC) != 0; } int getTrack() { return mInfo != null ? mInfo.getTrack() : -1; } @Override public String toString() { if (mInfo != null && mInfo.getDebugId() >= 0) { return "(#" + mInfo.getDebugId() + ") " + mToken + "@" + getTrack(); } return mToken.toString() + "@" + getTrack(); } } private static class Track { /** Keeps track of transitions which are ready to play but still waiting for their turn. */ final ArrayList mReadyTransitions = new ArrayList<>(); /** The currently playing transition in this track. */ ActiveTransition mActiveTransition = null; boolean isIdle() { return mActiveTransition == null && mReadyTransitions.isEmpty(); } } /** All transitions that we have created, but not yet finished. */ private final ArrayMap mKnownTransitions = new ArrayMap<>(); /** Keeps track of transitions which have been started, but aren't ready yet. */ private final ArrayList mPendingTransitions = new ArrayList<>(); /** * Transitions which are ready to play, but haven't been sent to a track yet because a sync * is ongoing. */ private final ArrayList mReadyDuringSync = new ArrayList<>(); private final ArrayList mTracks = new ArrayList<>(); public Transitions(@NonNull Context context, @NonNull ShellInit shellInit, @NonNull ShellController shellController, @NonNull ShellTaskOrganizer organizer, @NonNull TransactionPool pool, @NonNull DisplayController displayController, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, @NonNull HomeTransitionObserver observer) { this(context, shellInit, new ShellCommandHandler(), shellController, organizer, pool, displayController, mainExecutor, mainHandler, animExecutor, new RootTaskDisplayAreaOrganizer(mainExecutor, context, shellInit), observer); } public Transitions(@NonNull Context context, @NonNull ShellInit shellInit, @Nullable ShellCommandHandler shellCommandHandler, @NonNull ShellController shellController, @NonNull ShellTaskOrganizer organizer, @NonNull TransactionPool pool, @NonNull DisplayController displayController, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer, @NonNull HomeTransitionObserver observer) { mOrganizer = organizer; mContext = context; mMainExecutor = mainExecutor; mAnimExecutor = animExecutor; mDisplayController = displayController; mPlayerImpl = new TransitionPlayerImpl(); mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit, displayController, pool, mainExecutor, mainHandler, animExecutor, rootTDAOrganizer); mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor); mShellCommandHandler = shellCommandHandler; 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); mHomeTransitionObserver = observer; if (android.tracing.Flags.perfettoTransitionTracing()) { mTransitionTracer = new PerfettoTransitionTracer(); } else { mTransitionTracer = new LegacyTransitionTracer(); } } 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(); } mShellCommandHandler.addCommandCallback("transitions", this, this); mShellCommandHandler.addDumpCallback(this::dump, this); } 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 for all operations except takeovers when `filter` * matches an incoming transition. */ public void registerRemote(@NonNull TransitionFilter filter, @NonNull RemoteTransition remoteTransition) { mRemoteTransitionHandler.addFiltered(filter, remoteTransition); } /** * Register a remote transition to be used for all operations except takeovers when `filter` * matches an incoming transition. */ public void registerRemoteForTakeover(@NonNull TransitionFilter filter, @NonNull RemoteTransition remoteTransition) { mRemoteTransitionHandler.addFilteredForTakeover(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. * *

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 (isIdle()) { runnable.run(); } else { mRunWhenIdleQueue.add(runnable); } } void setDisableForceSyncForTest(boolean disable) { mDisableForceSync = disable; } /** * 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(FLAGS_IS_NON_APP_WINDOW & ~FLAG_IS_WALLPAPER)) { // Currently system windows are controlled by WindowState, so don't change their // surfaces. Otherwise their surfaces could be hidden or cropped unexpectedly. // This includes IME (associated with app), because there may not be a transition // associated with their visibility changes, and currently they don't need a // transition animation. continue; } if (change.hasFlags(FLAG_IS_WALLPAPER) && !ensureWallpaperInTransitions()) { // Wallpaper is always z-ordered at bottom, and historically is not animated by // transition handlers. continue; } final SurfaceControl leash = change.getLeash(); final int mode = info.getChanges().get(i).getMode(); if (mode == TRANSIT_TO_FRONT) { // When the window is moved to front, make sure the crop is updated to prevent it // from using the old crop. t.setPosition(leash, change.getEndRelOffset().x, change.getEndRelOffset().y); 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); t.setWindowCrop(leash, change.getEndAbsBounds().width(), change.getEndAbsBounds().height()); } 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); } finishT.show(leash); } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { finishT.hide(leash); } else if (isOpening && mode == TRANSIT_CHANGE) { // Just in case there is a race with another animation (eg. recents finish()). // Changes are visible->visible so it's a problem if it isn't visible. t.show(leash); } } } static int calculateAnimLayer(@NonNull TransitionInfo.Change change, int i, int numChanges, @WindowManager.TransitionType int transitType) { // Put animating stuff above this line and put static stuff below it. final int zSplitLine = numChanges + 1; final boolean isOpening = isOpeningType(transitType); final boolean isClosing = isClosingType(transitType); final int mode = change.getMode(); // Ensure wallpapers stay in the back if (change.hasFlags(FLAG_IS_WALLPAPER) && Flags.ensureWallpaperInTransitions()) { if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { return -zSplitLine + numChanges - i; } else { return -zSplitLine - i; } } // Put all the OPEN/SHOW on top if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) { if (isOpening) { // put on top return zSplitLine + numChanges - i; } else if (isClosing) { // put on bottom return zSplitLine - i; } else { // maintain relative ordering (put all changes in the animating layer) return zSplitLine + numChanges - i; } } else if (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK) { if (isOpening) { // put on bottom and leave visible return zSplitLine - i; } else { // put on top return zSplitLine + numChanges - i; } } else { // CHANGE or other if (isClosing || TransitionUtil.isOrderOnly(change)) { // Put below CLOSE mode (in the "static" section). return zSplitLine - i; } else { // Put above CLOSE mode. return zSplitLine + numChanges - i; } } } /** * 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) { final int type = info.getType(); for (int i = 0; i < info.getRootCount(); ++i) { t.show(info.getRoot(i).getLeash()); } final int numChanges = info.getChanges().size(); // 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(); // Don't reparent anything that isn't independent within its parents if (!TransitionInfo.isIndependent(change, info)) { continue; } boolean hasParent = change.getParent() != null; final TransitionInfo.Root root = TransitionUtil.getRootFor(change, info); if (!hasParent) { t.reparent(leash, root.getLeash()); t.setPosition(leash, change.getStartAbsBounds().left - root.getOffset().x, change.getStartAbsBounds().top - root.getOffset().y); } final int layer = calculateAnimLayer(change, i, numChanges, type); t.setLayer(leash, layer); } } private static int findByToken(ArrayList 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 if (!TransitionUtil.isOrderOnly(change) && !change.hasFlags(FLAG_IS_OCCLUDED)) { // Ignore the order only or occluded changes since they shouldn't be visible during // animation. For anything else, we need to animate if at-least one relevant // participant *is* animated, 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; } private Track getOrCreateTrack(int trackId) { while (trackId >= mTracks.size()) { mTracks.add(new Track()); } return mTracks.get(trackId); } @VisibleForTesting void onTransitionReady(@NonNull IBinder transitionToken, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { info.setUnreleasedWarningCallSiteForAllSurfaces("Transitions.onTransitionReady"); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady (#%d) %s: %s", info.getDebugId(), transitionToken, info); int activeIdx = findByToken(mPendingTransitions, transitionToken); if (activeIdx < 0) { final ActiveTransition existing = mKnownTransitions.get(transitionToken); if (existing != null) { Log.e(TAG, "Got duplicate transitionReady for " + transitionToken); // The transition is already somewhere else in the pipeline, so just return here. t.apply(); existing.mFinishT.merge(finishT); return; } // This usually means the system is in a bad state and may not recover; however, // there's an incentive to propagate bad states rather than crash, so we're kinda // required to do the same thing I guess. Log.wtf(TAG, "Got transitionReady for non-pending transition " + transitionToken + ". expecting one of " + Arrays.toString(mPendingTransitions.stream().map( activeTransition -> activeTransition.mToken).toArray())); final ActiveTransition fallback = new ActiveTransition(transitionToken); mKnownTransitions.put(transitionToken, fallback); mPendingTransitions.add(fallback); activeIdx = mPendingTransitions.size() - 1; } // Move from pending to ready final ActiveTransition active = mPendingTransitions.remove(activeIdx); active.mInfo = info; active.mStartT = t; active.mFinishT = finishT; if (activeIdx > 0) { Log.i(TAG, "Transition might be ready out-of-order " + activeIdx + " for " + active + ". This is ok if it's on a different track."); } if (!mReadyDuringSync.isEmpty()) { mReadyDuringSync.add(active); } else { dispatchReady(active); } } /** * Returns true if dispatching succeeded, otherwise false. Dispatching can fail if it is * blocked by a sync or sleep. */ boolean dispatchReady(ActiveTransition active) { final TransitionInfo info = active.mInfo; if (info.getType() == TRANSIT_SLEEP || active.isSync()) { // Adding to *front*! If we are here, it means that it was pulled off the front // so we are just putting it back; or, it is the first one so it doesn't matter. mReadyDuringSync.add(0, active); boolean hadPreceding = false; // Now flush all the tracks. for (int i = 0; i < mTracks.size(); ++i) { final Track tr = mTracks.get(i); if (tr.isIdle()) continue; hadPreceding = true; // Sleep starts a process of forcing all prior transitions to finish immediately ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Start finish-for-sync track %d", i); finishForSync(active.mToken, i, null /* forceFinish */); } if (hadPreceding) { return false; } // Actually able to process the sleep now, so re-remove it from the queue and continue // the normal flow. mReadyDuringSync.remove(active); } final Track track = getOrCreateTrack(info.getTrack()); track.mReadyTransitions.add(active); for (int i = 0; i < mObservers.size(); ++i) { mObservers.get(i).onTransitionReady( active.mToken, info, active.mStartT, active.mFinishT); } /* * Some transitions we always need to report to keyguard even if they are empty. * TODO (b/274954192): Remove this once keyguard dispatching fully moves to Shell. */ if (info.getRootCount() == 0 && !KeyguardTransitionHandler.handles(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 true; } final int changeSize = info.getChanges().size(); boolean taskChange = false; boolean transferStartingWindow = false; int animBehindStartingWindow = 0; 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.hasAllFlags(FLAG_IS_BEHIND_STARTING_WINDOW | FLAG_NO_ANIMATION) || change.hasAllFlags( FLAG_IS_BEHIND_STARTING_WINDOW | FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY)) { animBehindStartingWindow++; } if (!change.hasFlags(FLAG_IS_OCCLUDED)) { allOccluded = false; } else if (change.hasAllFlags(TransitionInfo.FLAGS_IS_OCCLUDED_NO_ANIMATION)) { // Remove the change because it should be invisible in the animation. info.getChanges().remove(i); continue; } // The change has already animated by back gesture, don't need to play transition // animation on it. if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { info.getChanges().remove(i); } } // 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 changes where // 1. none are tasks, and // 2. one is a starting-window recipient, or all change is behind starting window. if (!taskChange && (transferStartingWindow || animBehindStartingWindow == changeSize) && changeSize >= 1 // 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 true; } setupStartState(active.mInfo, active.mStartT, active.mFinishT); if (track.mReadyTransitions.size() > 1) { // There are already transitions waiting in the queue, so just return. return true; } processReadyQueue(track); return true; } private boolean areTracksIdle() { for (int i = 0; i < mTracks.size(); ++i) { if (!mTracks.get(i).isIdle()) return false; } return true; } private boolean isAnimating() { return !mReadyDuringSync.isEmpty() || !areTracksIdle(); } private boolean isIdle() { return mPendingTransitions.isEmpty() && !isAnimating(); } void processReadyQueue(Track track) { if (track.mReadyTransitions.isEmpty()) { if (track.mActiveTransition == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Track %d became idle", mTracks.indexOf(track)); if (areTracksIdle()) { if (!mReadyDuringSync.isEmpty()) { // Dispatch everything unless we hit another sync while (!mReadyDuringSync.isEmpty()) { ActiveTransition next = mReadyDuringSync.remove(0); boolean success = dispatchReady(next); // Hit a sync or sleep, so stop dispatching. if (!success) break; } } else if (mPendingTransitions.isEmpty()) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "All active transition " + "animations finished"); mKnownTransitions.clear(); // 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 = track.mReadyTransitions.get(0); if (track.mActiveTransition == null) { // The normal case, just play it. track.mReadyTransitions.remove(0); track.mActiveTransition = ready; if (ready.mAborted) { if (ready.mStartT != null) { ready.mStartT.apply(); } // finish now since there's nothing to animate. Calls back into processReadyQueue onFinish(ready.mToken, null); return; } playTransition(ready); // Attempt to merge any more queued-up transitions. processReadyQueue(track); return; } // An existing animation is playing, so see if we can merge. final ActiveTransition playing = track.mActiveTransition; 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); mTransitionTracer.logMergeRequested(ready.mInfo.getDebugId(), playing.mInfo.getDebugId()); playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT, playing.mToken, (wct) -> onMerged(playing, ready)); } private void onMerged(@NonNull ActiveTransition playing, @NonNull ActiveTransition merged) { if (playing.getTrack() != merged.getTrack()) { throw new IllegalStateException("Can't merge across tracks: " + merged + " into " + playing); } final Track track = mTracks.get(playing.getTrack()); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition was merged: %s into %s", merged, playing); int readyIdx = 0; if (track.mReadyTransitions.isEmpty() || track.mReadyTransitions.get(0) != merged) { Log.e(TAG, "Merged transition out-of-order? " + merged); readyIdx = track.mReadyTransitions.indexOf(merged); if (readyIdx < 0) { Log.e(TAG, "Merged a transition that is no-longer queued? " + merged); return; } } track.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); } mTransitionTracer.logMerged(merged.mInfo.getDebugId(), playing.mInfo.getDebugId()); // See if we should merge another transition. processReadyQueue(track); } private void playTransition(@NonNull ActiveTransition active) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Playing animation for %s", active); final var token = active.mToken; for (int i = 0; i < mObservers.size(); ++i) { mObservers.get(i).onTransitionStarting(token); } 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(token, active.mInfo, active.mStartT, active.mFinishT, (wct) -> onFinish(token, wct)); if (consumed) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler"); mTransitionTracer.logDispatched(active.mInfo.getDebugId(), active.mHandler); return; } } // Otherwise give every other handler a chance active.mHandler = dispatchTransition(token, active.mInfo, active.mStartT, active.mFinishT, (wct) -> onFinish(token, wct), 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)); mTransitionTracer.logDispatched(info.getDebugId(), 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 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) { final Track track = mTracks.get(transition.getTrack()); transition.mAborted = true; mTransitionTracer.logAborted(transition.mInfo.getDebugId()); 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 (track.mReadyTransitions.size() > 1) { // There are already transitions waiting in the queue, so just return. return; } processReadyQueue(track); } /** * 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(IBinder token, @Nullable WindowContainerTransaction wct) { final ActiveTransition active = mKnownTransitions.get(token); if (active == null) { Log.e(TAG, "Trying to finish a non-existent transition: " + token); return; } final Track track = mTracks.get(active.getTrack()); if (track == null || track.mActiveTransition != active) { 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; } track.mActiveTransition = null; 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); 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 */); releaseSurfaces(merged.mInfo); mKnownTransitions.remove(merged.mToken); } active.mMerged.clear(); } mKnownTransitions.remove(token); // Now that this is done, check the ready queue for more work. processReadyQueue(track); } void requestStartTransition(@NonNull IBinder transitionToken, @Nullable TransitionRequestInfo request) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Transition requested (#%d): %s %s", request.getDebugId(), transitionToken, request); if (mKnownTransitions.containsKey(transitionToken)) { throw new RuntimeException("Transition already started " + transitionToken); } final ActiveTransition active = new ActiveTransition(transitionToken); mKnownTransitions.put(transitionToken, active); 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.onDisplayRotateRequested(wct, change.getDisplayId(), change.getStartRotation(), change.getEndRotation()); } } } final boolean isOccludingKeyguard = request.getType() == TRANSIT_KEYGUARD_OCCLUDE || ((request.getFlags() & TRANSIT_FLAG_KEYGUARD_OCCLUDING) != 0); if (isOccludingKeyguard && request.getTriggerTask() != null && request.getTriggerTask().getWindowingMode() == WINDOWING_MODE_FREEFORM) { // This freeform task is on top of keyguard, so its windowing mode should be changed to // fullscreen. if (wct == null) { wct = new WindowContainerTransaction(); } wct.setWindowingMode(request.getTriggerTask().token, WINDOWING_MODE_FULLSCREEN); wct.setBounds(request.getTriggerTask().token, null); } mOrganizer.startTransition(transitionToken, wct != null && wct.isEmpty() ? null : wct); // 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. * @param handler if null, the transition will be dispatched to the registered set of transition * handlers to be handled */ 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(mOrganizer.startNewTransition(type, wct)); active.mHandler = handler; mKnownTransitions.put(active.mToken, active); mPendingTransitions.add(active); return active.mToken; } /** * Checks whether a handler exists capable of taking over the given transition, and returns it. * Otherwise it returns null. */ @Nullable public TransitionHandler getHandlerForTakeover( @NonNull IBinder transition, @NonNull TransitionInfo info) { if (!returnAnimationFrameworkLibrary()) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "Trying to get a handler for takeover but the flag is disabled"); return null; } for (TransitionHandler handler : mHandlers) { TransitionHandler candidate = handler.getHandlerForTakeover(transition, info); if (candidate != null) { return candidate; } } return null; } /** * 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 `SYNC_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 reason The token for the SLEEP transition that triggered this round of finishes. * We will continue looping round finishing transitions until this is ready. * @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 finishForSync(IBinder reason, int trackIdx, @Nullable ActiveTransition forceFinish) { if (!mKnownTransitions.containsKey(reason)) { Log.d(TAG, "finishForSleep: already played sync transition " + reason); return; } final Track track = mTracks.get(trackIdx); if (forceFinish != null) { final Track trk = mTracks.get(forceFinish.getTrack()); if (trk != track) { Log.e(TAG, "finishForSleep: mismatched Tracks between forceFinish and logic " + forceFinish.getTrack() + " vs " + trackIdx); } if (trk.mActiveTransition == forceFinish) { Log.e(TAG, "Forcing transition to finish due to sync 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.mToken, null); } } if (track.isIdle() || mReadyDuringSync.isEmpty()) { // Done finishing things. return; } final SurfaceControl.Transaction dummyT = new SurfaceControl.Transaction(); final TransitionInfo dummyInfo = new TransitionInfo(TRANSIT_SLEEP, 0 /* flags */); while (track.mActiveTransition != null && !mReadyDuringSync.isEmpty()) { final ActiveTransition playing = track.mActiveTransition; final ActiveTransition nextSync = mReadyDuringSync.get(0); if (!nextSync.isSync()) { Log.e(TAG, "Somehow blocked on a non-sync transition? " + nextSync); } // Attempt to merge a SLEEP info to signal that the playing transition needs to // fast-forward. ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt to merge sync %s" + " into %s via a SLEEP proxy", nextSync, playing); playing.mHandler.mergeAnimation(nextSync.mToken, dummyInfo, dummyT, playing.mToken, (wct) -> {}); // 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 (track.mActiveTransition == playing) { if (!mDisableForceSync) { // Give it a short amount of time to process it before forcing. mMainExecutor.executeDelayed( () -> finishForSync(reason, trackIdx, playing), SYNC_ALLOWANCE_MS); } break; } } } private SurfaceControl getHomeTaskOverlayContainer() { return mOrganizer.getHomeTaskOverlayContainer(); } /** * 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. */ void onTransitionFinished(@Nullable WindowContainerTransaction wct); } /** * 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) { } /** * Checks whether this handler is capable of taking over a transition matching `info`. * {@link TransitionHandler#takeOverAnimation(IBinder, TransitionInfo, * SurfaceControl.Transaction, TransitionFinishCallback, WindowAnimationState[])} is * guaranteed to succeed if called on the handler returned by this method. * * Note that the handler returned by this method can either be itself, or a different one * selected by this handler to take care of the transition on its behalf. * * @param transition The transition that should be taken over. * @param info Information about the transition to be taken over. * @return A handler capable of taking over a matching transition, or null. */ @Nullable default TransitionHandler getHandlerForTakeover( @NonNull IBinder transition, @NonNull TransitionInfo info) { return null; } /** * Attempt to take over a running transition. This must succeed if this handler was returned * by {@link TransitionHandler#getHandlerForTakeover(IBinder, TransitionInfo)}. * * @param transition The transition that should be taken over. * @param info Information about the what is changing in the transition. * @param transaction Contains surface changes that resulted from the transition. Any * additional changes should be added to this transaction and committed * inside this method. * @param finishCallback Call this at the end of the animation, if the take-over succeeds. * Note that this will be called instead of the callback originally * passed to startAnimation(), so the caller should make sure all * necessary cleanup happens here. This MUST be called on main thread. * @param states The animation states of the transition's window at the time this method was * called. * @return true if the transition was taken over, false if not. */ default boolean takeOverAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction transaction, @NonNull TransitionFinishCallback finishCallback, @NonNull WindowAnimationState[] states) { return false; } /** * 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 { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady(transaction=%d)", t.getId()); 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 registerRemoteForTakeover(@NonNull TransitionFilter filter, @NonNull RemoteTransition remoteTransition) { mMainExecutor.execute(() -> mRemoteTransitionHandler.addFilteredForTakeover( 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.mHomeTransitionObserver.invalidate(mTransitions); mTransitions = null; } @Override public void registerRemote(@NonNull TransitionFilter filter, @NonNull RemoteTransition remoteTransition) { executeRemoteCallWithTaskPermission(mTransitions, "registerRemote", (transitions) -> transitions.mRemoteTransitionHandler.addFiltered( filter, remoteTransition)); } @Override public void registerRemoteForTakeover(@NonNull TransitionFilter filter, @NonNull RemoteTransition remoteTransition) { executeRemoteCallWithTaskPermission(mTransitions, "registerRemoteForTakeover", (transitions) -> transitions.mRemoteTransitionHandler.addFilteredForTakeover( 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(); } @Override public void setHomeTransitionListener(IHomeTransitionListener listener) { executeRemoteCallWithTaskPermission(mTransitions, "setHomeTransitionListener", (transitions) -> { transitions.mHomeTransitionObserver.setHomeTransitionListener(transitions, listener); }); } @Override public SurfaceControl getHomeTaskOverlayContainer() { SurfaceControl[] result = new SurfaceControl[1]; executeRemoteCallWithTaskPermission(mTransitions, "getHomeTaskOverlayContainer", (controller) -> { result[0] = controller.getHomeTaskOverlayContainer(); }, true /* blocking */); // Return a copy as writing to parcel releases the original surface return new SurfaceControl(result[0], "Transitions.HomeOverlay"); } } private class SettingsObserver extends ContentObserver { SettingsObserver() { super(null); } @Override public void onChange(boolean selfChange) { super.onChange(selfChange); mTransitionAnimationScaleSetting = getTransitionAnimationScaleSetting(); mMainExecutor.execute(() -> dispatchAnimScaleSetting(mTransitionAnimationScaleSetting)); } } @Override public boolean onShellCommand(String[] args, PrintWriter pw) { switch (args[0]) { case "tracing": { if (!android.tracing.Flags.perfettoTransitionTracing()) { ((LegacyTransitionTracer) mTransitionTracer) .onShellCommand(Arrays.copyOfRange(args, 1, args.length), pw); } else { pw.println("Command not supported. Use the Perfetto command instead to start " + "and stop this trace instead."); return false; } return true; } default: { pw.println("Invalid command: " + args[0]); printShellCommandHelp(pw, ""); return false; } } } @Override public void printShellCommandHelp(PrintWriter pw, String prefix) { if (!android.tracing.Flags.perfettoTransitionTracing()) { pw.println(prefix + "tracing"); ((LegacyTransitionTracer) mTransitionTracer).printShellCommandHelp(pw, prefix + " "); } } private void dump(@NonNull PrintWriter pw, String prefix) { pw.println(prefix + TAG); final String innerPrefix = prefix + " "; pw.println(prefix + "Handlers:"); for (TransitionHandler handler : mHandlers) { pw.print(innerPrefix); pw.print(handler.getClass().getSimpleName()); pw.println(" (" + Integer.toHexString(System.identityHashCode(handler)) + ")"); } mRemoteTransitionHandler.dump(pw, prefix); pw.println(prefix + "Observers:"); for (TransitionObserver observer : mObservers) { pw.print(innerPrefix); pw.println(observer.getClass().getSimpleName()); } pw.println(prefix + "Pending Transitions:"); for (ActiveTransition transition : mPendingTransitions) { pw.print(innerPrefix + "token="); pw.println(transition.mToken); pw.print(innerPrefix + "id="); pw.println(transition.mInfo != null ? transition.mInfo.getDebugId() : -1); pw.print(innerPrefix + "handler="); pw.println(transition.mHandler != null ? transition.mHandler.getClass().getSimpleName() : null); } if (mPendingTransitions.isEmpty()) { pw.println(innerPrefix + "none"); } pw.println(prefix + "Ready-during-sync Transitions:"); for (ActiveTransition transition : mReadyDuringSync) { pw.print(innerPrefix + "token="); pw.println(transition.mToken); pw.print(innerPrefix + "id="); pw.println(transition.mInfo != null ? transition.mInfo.getDebugId() : -1); pw.print(innerPrefix + "handler="); pw.println(transition.mHandler != null ? transition.mHandler.getClass().getSimpleName() : null); } if (mReadyDuringSync.isEmpty()) { pw.println(innerPrefix + "none"); } pw.println(prefix + "Tracks:"); for (int i = 0; i < mTracks.size(); i++) { final ActiveTransition transition = mTracks.get(i).mActiveTransition; pw.println(innerPrefix + "Track #" + i); pw.print(innerPrefix + "active="); pw.println(transition); if (transition != null) { pw.print(innerPrefix + "hander="); pw.println(transition.mHandler); } } } private static boolean getShellTransitEnabled() { try { if (AppGlobals.getPackageManager().hasSystemFeature( PackageManager.FEATURE_AUTOMOTIVE, 0)) { return SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); } } catch (RemoteException re) { Log.w(TAG, "Error getting system features"); } return true; } }