/*
 * 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<Transitions>,
        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<TransitionHandler> mHandlers = new ArrayList<>();

    private final ArrayList<TransitionObserver> mObservers = new ArrayList<>();

    private HomeTransitionObserver mHomeTransitionObserver;

    /** 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 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<ActiveTransition> 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<ActiveTransition> 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<IBinder, ActiveTransition> mKnownTransitions = new ArrayMap<>();

    /** Keeps track of transitions which have been started, but aren't ready yet. */
    private final ArrayList<ActiveTransition> 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<ActiveTransition> mReadyDuringSync = new ArrayList<>();

    private final ArrayList<Track> 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.
     *
     * <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 (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<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 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();
                if (existing.mFinishT != null) {
                    existing.mFinishT.merge(finishT);
                } else {
                    existing.mFinishT = 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<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) {
        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;
    }
}
