/*
 * Copyright (C) 2018 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.server.wm;

import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE;
import static android.view.WindowManager.TRANSIT_FLAG_APP_CRASHED;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE;
import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER;
import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND;
import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
import static android.view.WindowManager.TRANSIT_NONE;
import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_CLOSE;
import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN;
import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_RELAUNCH;
import static android.view.WindowManager.TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE;
import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY;
import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER;
import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_OCCLUDE;
import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE;
import static android.view.WindowManager.TRANSIT_OLD_NONE;
import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
import static android.view.WindowManager.TRANSIT_OLD_TASK_CLOSE;
import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
import static android.view.WindowManager.TRANSIT_OLD_TASK_FRAGMENT_OPEN;
import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN;
import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN_BEHIND;
import static android.view.WindowManager.TRANSIT_OLD_TASK_TO_BACK;
import static android.view.WindowManager.TRANSIT_OLD_TASK_TO_FRONT;
import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE;
import static android.view.WindowManager.TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN;
import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_CLOSE;
import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_CLOSE;
import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_INTRA_OPEN;
import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_OPEN;
import static android.view.WindowManager.TRANSIT_OPEN;
import static android.view.WindowManager.TRANSIT_RELAUNCH;
import static android.view.WindowManager.TRANSIT_TO_BACK;
import static android.view.WindowManager.TRANSIT_TO_FRONT;

import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS;
import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_APP_TRANSITIONS_ANIM;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_CONFIG;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT;
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SNAPSHOT;
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_SPLASH_SCREEN;
import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_WINDOWS_DRAWN;
import static com.android.server.wm.AppTransition.isNormalTransit;
import static com.android.server.wm.NonAppWindowAnimationAdapter.shouldAttachNavBarToApp;
import static com.android.server.wm.NonAppWindowAnimationAdapter.shouldStartNonAppWindowAnimationsForKeyguardExit;
import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
import static com.android.server.wm.WallpaperAnimationAdapter.shouldStartWallpaperAnimation;
import static com.android.server.wm.WindowContainer.AnimationFlags.PARENTS;
import static com.android.server.wm.WindowManagerDebugConfig.SHOW_LIGHT_TRANSACTIONS;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;

import android.annotation.IntDef;
import android.annotation.Nullable;
import android.os.Trace;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
import android.view.Display;
import android.view.RemoteAnimationAdapter;
import android.view.RemoteAnimationDefinition;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.WindowManager.TransitionFlags;
import android.view.WindowManager.TransitionOldType;
import android.view.WindowManager.TransitionType;
import android.view.animation.Animation;
import android.window.ITaskFragmentOrganizer;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.function.Predicate;

/**
 * Checks for app transition readiness, resolves animation attributes and performs visibility
 * change for apps that animate as part of an app transition.
 */
public class AppTransitionController {
    private static final String TAG = TAG_WITH_CLASS_NAME ? "AppTransitionController" : TAG_WM;
    private final WindowManagerService mService;
    private final DisplayContent mDisplayContent;
    private final WallpaperController mWallpaperControllerLocked;
    private RemoteAnimationDefinition mRemoteAnimationDefinition = null;
    private static final int KEYGUARD_GOING_AWAY_ANIMATION_DURATION = 400;

    private static final int TYPE_NONE = 0;
    private static final int TYPE_ACTIVITY = 1;
    private static final int TYPE_TASK_FRAGMENT = 2;
    private static final int TYPE_TASK = 3;

    @IntDef(prefix = { "TYPE_" }, value = {
            TYPE_NONE,
            TYPE_ACTIVITY,
            TYPE_TASK_FRAGMENT,
            TYPE_TASK
    })
    @Retention(RetentionPolicy.SOURCE)
    @interface TransitContainerType {}

    private final ArrayMap<WindowContainer, Integer> mTempTransitionReasons = new ArrayMap<>();
    private final ArrayList<WindowContainer> mTempTransitionWindows = new ArrayList<>();

    AppTransitionController(WindowManagerService service, DisplayContent displayContent) {
        mService = service;
        mDisplayContent = displayContent;
        mWallpaperControllerLocked = mDisplayContent.mWallpaperController;
    }

    void registerRemoteAnimations(RemoteAnimationDefinition definition) {
        mRemoteAnimationDefinition = definition;
    }

    /**
     * Returns the currently visible window that is associated with the wallpaper in case we are
     * transitioning from an activity with a wallpaper to one without.
     */
    private @Nullable WindowState getOldWallpaper() {
        final WindowState wallpaperTarget = mWallpaperControllerLocked.getWallpaperTarget();
        final @TransitionType int firstTransit =
                mDisplayContent.mAppTransition.getFirstAppTransition();

        final ArraySet<WindowContainer> openingWcs = getAnimationTargets(
                mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, true /* visible */);
        final boolean showWallpaper = wallpaperTarget != null
                && (wallpaperTarget.hasWallpaper()
                // Update task open transition to wallpaper transition when wallpaper is visible.
                // (i.e.launching app info activity from recent tasks)
                || ((firstTransit == TRANSIT_OPEN || firstTransit == TRANSIT_TO_FRONT)
                && (!openingWcs.isEmpty() && openingWcs.valueAt(0).asTask() != null)
                && mWallpaperControllerLocked.isWallpaperVisible()));
        // If wallpaper is animating or wallpaperTarget doesn't have SHOW_WALLPAPER flag set,
        // don't consider upgrading to wallpaper transition.
        return (mWallpaperControllerLocked.isWallpaperTargetAnimating() || !showWallpaper)
                ? null : wallpaperTarget;
    }

    /**
     * Handle application transition for given display.
     */
    void handleAppTransitionReady() {
        mTempTransitionReasons.clear();
        if (!transitionGoodToGo(mDisplayContent.mOpeningApps, mTempTransitionReasons)
                || !transitionGoodToGo(mDisplayContent.mChangingContainers, mTempTransitionReasons)
                || !transitionGoodToGoForTaskFragments()) {
            return;
        }
        Trace.traceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "AppTransitionReady");

        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "**** GOOD TO GO");
        // TODO(b/205335975): Remove window which stuck in animatingExit status. Find actual cause.
        mDisplayContent.forAllWindows(WindowState::cleanupAnimatingExitWindow,
                true /* traverseTopToBottom */);
        // TODO(new-app-transition): Remove code using appTransition.getAppTransition()
        final AppTransition appTransition = mDisplayContent.mAppTransition;

        mDisplayContent.mNoAnimationNotifyOnTransitionFinished.clear();

        appTransition.removeAppTransitionTimeoutCallbacks();

        mDisplayContent.mWallpaperMayChange = false;

        int appCount = mDisplayContent.mOpeningApps.size();
        for (int i = 0; i < appCount; ++i) {
            // Clearing the mAnimatingExit flag before entering animation. It's set to true if app
            // window is removed, or window relayout to invisible. This also affects window
            // visibility. We need to clear it *before* maybeUpdateTransitToWallpaper() as the
            // transition selection depends on wallpaper target visibility.
            mDisplayContent.mOpeningApps.valueAtUnchecked(i).clearAnimatingFlags();
        }
        appCount = mDisplayContent.mChangingContainers.size();
        for (int i = 0; i < appCount; ++i) {
            // Clearing for same reason as above.
            final ActivityRecord activity = getAppFromContainer(
                    mDisplayContent.mChangingContainers.valueAtUnchecked(i));
            if (activity != null) {
                activity.clearAnimatingFlags();
            }
        }

        // Adjust wallpaper before we pull the lower/upper target, since pending changes
        // (like the clearAnimatingFlags() above) might affect wallpaper target result.
        // Or, the opening app window should be a wallpaper target.
        mWallpaperControllerLocked.adjustWallpaperWindowsForAppTransitionIfNeeded(
                mDisplayContent.mOpeningApps);

        final @TransitionOldType int transit = getTransitCompatType(
                mDisplayContent.mAppTransition, mDisplayContent.mOpeningApps,
                mDisplayContent.mClosingApps, mDisplayContent.mChangingContainers,
                mWallpaperControllerLocked.getWallpaperTarget(), getOldWallpaper(),
                mDisplayContent.mSkipAppTransitionAnimation);
        mDisplayContent.mSkipAppTransitionAnimation = false;

        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                "handleAppTransitionReady: displayId=%d appTransition={%s}"
                + " openingApps=[%s] closingApps=[%s] transit=%s",
                mDisplayContent.mDisplayId,
                appTransition.toString(),
                mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps,
                AppTransition.appTransitionOldToString(transit));

        // Find the layout params of the top-most application window in the tokens, which is
        // what will control the animation theme. If all closing windows are obscured, then there is
        // no need to do an animation. This is the case, for example, when this transition is being
        // done behind a dream window.
        final ArraySet<Integer> activityTypes = collectActivityTypes(mDisplayContent.mOpeningApps,
                mDisplayContent.mClosingApps, mDisplayContent.mChangingContainers);
        final ActivityRecord animLpActivity = findAnimLayoutParamsToken(transit, activityTypes);
        final ActivityRecord topOpeningApp =
                getTopApp(mDisplayContent.mOpeningApps, false /* ignoreHidden */);
        final ActivityRecord topClosingApp =
                getTopApp(mDisplayContent.mClosingApps, false /* ignoreHidden */);
        final ActivityRecord topChangingApp =
                getTopApp(mDisplayContent.mChangingContainers, false /* ignoreHidden */);
        final WindowManager.LayoutParams animLp = getAnimLp(animLpActivity);

        // Check if there is any override
        if (!overrideWithTaskFragmentRemoteAnimation(transit, activityTypes)) {
            // Unfreeze the windows that were previously frozen for TaskFragment animation.
            unfreezeEmbeddedChangingWindows();
            overrideWithRemoteAnimationIfSet(animLpActivity, transit, activityTypes);
        }

        final boolean voiceInteraction = containsVoiceInteraction(mDisplayContent.mOpeningApps)
                || containsVoiceInteraction(mDisplayContent.mOpeningApps);

        final int layoutRedo;
        mService.mSurfaceAnimationRunner.deferStartingAnimations();
        try {
            applyAnimations(mDisplayContent.mOpeningApps, mDisplayContent.mClosingApps, transit,
                    animLp, voiceInteraction);
            handleClosingApps();
            handleOpeningApps();
            handleChangingApps(transit);

            appTransition.setLastAppTransition(transit, topOpeningApp,
                    topClosingApp, topChangingApp);

            final int flags = appTransition.getTransitFlags();
            layoutRedo = appTransition.goodToGo(transit, topOpeningApp);
            handleNonAppWindowsInTransition(transit, flags);
            appTransition.postAnimationCallback();
            appTransition.clear();
        } finally {
            mService.mSurfaceAnimationRunner.continueStartingAnimations();
        }

        mService.mTaskSnapshotController.onTransitionStarting(mDisplayContent);

        mDisplayContent.mOpeningApps.clear();
        mDisplayContent.mClosingApps.clear();
        mDisplayContent.mChangingContainers.clear();
        mDisplayContent.mUnknownAppVisibilityController.clear();

        // This has changed the visibility of windows, so perform
        // a new layout to get them all up-to-date.
        mDisplayContent.setLayoutNeeded();

        mDisplayContent.computeImeTarget(true /* updateImeTarget */);

        mService.mAtmService.mTaskSupervisor.getActivityMetricsLogger().notifyTransitionStarting(
                mTempTransitionReasons);

        Trace.traceEnd(Trace.TRACE_TAG_WINDOW_MANAGER);

        mDisplayContent.pendingLayoutChanges |=
                layoutRedo | FINISH_LAYOUT_REDO_LAYOUT | FINISH_LAYOUT_REDO_CONFIG;
    }

    /**
     * Get old transit type based on the current transit requests.
     *
     * @param appTransition {@link AppTransition} for managing app transition state.
     * @param openingApps {@link ActivityRecord}s which are becoming visible.
     * @param closingApps {@link ActivityRecord}s which are becoming invisible.
     * @param changingContainers {@link WindowContainer}s which are changed in configuration.
     * @param wallpaperTarget If non-null, this is the currently visible window that is associated
     *                        with the wallpaper.
     * @param oldWallpaper The currently visible window that is associated with the wallpaper in
     *                     case we are transitioning from an activity with a wallpaper to one
     *                     without. Otherwise null.
     */
    static @TransitionOldType int getTransitCompatType(AppTransition appTransition,
            ArraySet<ActivityRecord> openingApps, ArraySet<ActivityRecord> closingApps,
            ArraySet<WindowContainer> changingContainers, @Nullable WindowState wallpaperTarget,
            @Nullable WindowState oldWallpaper, boolean skipAppTransitionAnimation) {

        // Determine if closing and opening app token sets are wallpaper targets, in which case
        // special animations are needed.
        final boolean openingAppHasWallpaper = canBeWallpaperTarget(openingApps)
                && wallpaperTarget != null;
        final boolean closingAppHasWallpaper = canBeWallpaperTarget(closingApps)
                && wallpaperTarget != null;

        // Keyguard transit has highest priority.
        switch (appTransition.getKeyguardTransition()) {
            case TRANSIT_KEYGUARD_GOING_AWAY:
                return openingAppHasWallpaper ? TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER
                        : TRANSIT_OLD_KEYGUARD_GOING_AWAY;
            case TRANSIT_KEYGUARD_OCCLUDE:
                // When there is a closing app, the keyguard has already been occluded by an
                // activity, and another activity has started on top of that activity, so normal
                // app transition animation should be used.
                return closingApps.isEmpty() ? TRANSIT_OLD_KEYGUARD_OCCLUDE
                        : TRANSIT_OLD_ACTIVITY_OPEN;
            case TRANSIT_KEYGUARD_UNOCCLUDE:
                return TRANSIT_OLD_KEYGUARD_UNOCCLUDE;
        }

        // This is not keyguard transition and one of the app has request to skip app transition.
        if (skipAppTransitionAnimation) {
            return WindowManager.TRANSIT_OLD_UNSET;
        }
        final @TransitionFlags int flags = appTransition.getTransitFlags();
        final @TransitionType int firstTransit = appTransition.getFirstAppTransition();

        // Special transitions
        // TODO(new-app-transitions): Revisit if those can be rewritten by using flags.
        if (appTransition.containsTransitRequest(TRANSIT_CHANGE) && !changingContainers.isEmpty()) {
            @TransitContainerType int changingType =
                    getTransitContainerType(changingContainers.valueAt(0));
            switch (changingType) {
                case TYPE_TASK:
                    return TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE;
                case TYPE_TASK_FRAGMENT:
                    return TRANSIT_OLD_TASK_FRAGMENT_CHANGE;
                default:
                    throw new IllegalStateException(
                            "TRANSIT_CHANGE with unrecognized changing type=" + changingType);
            }
        }
        if ((flags & TRANSIT_FLAG_APP_CRASHED) != 0) {
            return TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE;
        }
        if (firstTransit == TRANSIT_NONE) {
            return TRANSIT_OLD_NONE;
        }

        /*
         * There are cases where we open/close a new task/activity, but in reality only a
         * translucent activity on top of existing activities is opening/closing. For that one, we
         * have a different animation because non of the task/activity animations actually work well
         * with translucent apps.
         */
        if (isNormalTransit(firstTransit)) {
            boolean allOpeningVisible = true;
            boolean allTranslucentOpeningApps = !openingApps.isEmpty();
            for (int i = openingApps.size() - 1; i >= 0; i--) {
                final ActivityRecord activity = openingApps.valueAt(i);
                if (!activity.isVisible()) {
                    allOpeningVisible = false;
                    if (activity.fillsParent()) {
                        allTranslucentOpeningApps = false;
                    }
                }
            }
            boolean allTranslucentClosingApps = !closingApps.isEmpty();
            for (int i = closingApps.size() - 1; i >= 0; i--) {
                if (closingApps.valueAt(i).fillsParent()) {
                    allTranslucentClosingApps = false;
                    break;
                }
            }

            if (allTranslucentClosingApps && allOpeningVisible) {
                return TRANSIT_OLD_TRANSLUCENT_ACTIVITY_CLOSE;
            }
            if (allTranslucentOpeningApps && closingApps.isEmpty()) {
                return TRANSIT_OLD_TRANSLUCENT_ACTIVITY_OPEN;
            }
        }

        final ActivityRecord topOpeningApp = getTopApp(openingApps,
                false /* ignoreHidden */);
        final ActivityRecord topClosingApp = getTopApp(closingApps,
                true /* ignoreHidden */);

        if (closingAppHasWallpaper && openingAppHasWallpaper) {
            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Wallpaper animation!");
            switch (firstTransit) {
                case TRANSIT_OPEN:
                case TRANSIT_TO_FRONT:
                    return TRANSIT_OLD_WALLPAPER_INTRA_OPEN;
                case TRANSIT_CLOSE:
                case TRANSIT_TO_BACK:
                    return TRANSIT_OLD_WALLPAPER_INTRA_CLOSE;
            }
        } else if (oldWallpaper != null && !openingApps.isEmpty()
                && !openingApps.contains(oldWallpaper.mActivityRecord)
                && closingApps.contains(oldWallpaper.mActivityRecord)
                && topClosingApp == oldWallpaper.mActivityRecord) {
            // We are transitioning from an activity with a wallpaper to one without.
            return TRANSIT_OLD_WALLPAPER_CLOSE;
        } else if (wallpaperTarget != null && wallpaperTarget.isVisible()
                && openingApps.contains(wallpaperTarget.mActivityRecord)
                && topOpeningApp == wallpaperTarget.mActivityRecord
                /* && transit != TRANSIT_TRANSLUCENT_ACTIVITY_CLOSE */) {
            // We are transitioning from an activity without
            // a wallpaper to now showing the wallpaper
            return TRANSIT_OLD_WALLPAPER_OPEN;
        }

        final ArraySet<WindowContainer> openingWcs = getAnimationTargets(
                openingApps, closingApps, true /* visible */);
        final ArraySet<WindowContainer> closingWcs = getAnimationTargets(
                openingApps, closingApps, false /* visible */);
        final WindowContainer<?> openingContainer = !openingWcs.isEmpty()
                ? openingWcs.valueAt(0) : null;
        final WindowContainer<?> closingContainer = !closingWcs.isEmpty()
                ? closingWcs.valueAt(0) : null;
        @TransitContainerType int openingType = getTransitContainerType(openingContainer);
        @TransitContainerType int closingType = getTransitContainerType(closingContainer);
        if (appTransition.containsTransitRequest(TRANSIT_TO_FRONT) && openingType == TYPE_TASK) {
            return TRANSIT_OLD_TASK_TO_FRONT;
        }
        if (appTransition.containsTransitRequest(TRANSIT_TO_BACK) && closingType == TYPE_TASK) {
            return TRANSIT_OLD_TASK_TO_BACK;
        }
        if (appTransition.containsTransitRequest(TRANSIT_OPEN)) {
            if (openingType == TYPE_TASK) {
                return (appTransition.getTransitFlags() & TRANSIT_FLAG_OPEN_BEHIND) != 0
                        ? TRANSIT_OLD_TASK_OPEN_BEHIND : TRANSIT_OLD_TASK_OPEN;
            }
            if (openingType == TYPE_ACTIVITY) {
                return TRANSIT_OLD_ACTIVITY_OPEN;
            }
            if (openingType == TYPE_TASK_FRAGMENT) {
                return TRANSIT_OLD_TASK_FRAGMENT_OPEN;
            }
        }
        if (appTransition.containsTransitRequest(TRANSIT_CLOSE)) {
            if (closingType == TYPE_TASK) {
                return TRANSIT_OLD_TASK_CLOSE;
            }
            if (closingType == TYPE_TASK_FRAGMENT) {
                return TRANSIT_OLD_TASK_FRAGMENT_CLOSE;
            }
            if (closingType == TYPE_ACTIVITY) {
                for (int i = closingApps.size() - 1; i >= 0; i--) {
                    if (closingApps.valueAt(i).visibleIgnoringKeyguard) {
                        return TRANSIT_OLD_ACTIVITY_CLOSE;
                    }
                }
                // Skip close activity transition since no closing app can be visible
                return WindowManager.TRANSIT_OLD_UNSET;
            }
        }
        if (appTransition.containsTransitRequest(TRANSIT_RELAUNCH)
                && !openingWcs.isEmpty() && !openingApps.isEmpty()) {
            return TRANSIT_OLD_ACTIVITY_RELAUNCH;
        }
        return TRANSIT_OLD_NONE;
    }

    @TransitContainerType
    private static int getTransitContainerType(@Nullable WindowContainer<?> container) {
        if (container == null) {
            return TYPE_NONE;
        }
        if (container.asTask() != null) {
            return TYPE_TASK;
        }
        if (container.asTaskFragment() != null) {
            return TYPE_TASK_FRAGMENT;
        }
        if (container.asActivityRecord() != null) {
            return TYPE_ACTIVITY;
        }
        return TYPE_NONE;
    }

    @Nullable
    private static WindowManager.LayoutParams getAnimLp(ActivityRecord activity) {
        final WindowState mainWindow = activity != null ? activity.findMainWindow() : null;
        return mainWindow != null ? mainWindow.mAttrs : null;
    }

    RemoteAnimationAdapter getRemoteAnimationOverride(@Nullable WindowContainer container,
            @TransitionOldType int transit, ArraySet<Integer> activityTypes) {
        if (container != null) {
            final RemoteAnimationDefinition definition = container.getRemoteAnimationDefinition();
            if (definition != null) {
                final RemoteAnimationAdapter adapter = definition.getAdapter(transit,
                        activityTypes);
                if (adapter != null) {
                    return adapter;
                }
            }
        }
        return mRemoteAnimationDefinition != null
                ? mRemoteAnimationDefinition.getAdapter(transit, activityTypes)
                : null;
    }

    private void unfreezeEmbeddedChangingWindows() {
        final ArraySet<WindowContainer> changingContainers = mDisplayContent.mChangingContainers;
        for (int i = changingContainers.size() - 1; i >= 0; i--) {
            final WindowContainer wc = changingContainers.valueAt(i);
            if (wc.isEmbedded()) {
                wc.mSurfaceFreezer.unfreeze(wc.getSyncTransaction());
            }
        }
    }

    private boolean transitionMayContainNonAppWindows(@TransitionOldType int transit) {
        // We don't want to have the client to animate any non-app windows.
        // Having {@code transit} of those types doesn't mean it will contain non-app windows, but
        // non-app windows will only be included with those transition types. And we don't currently
        // have any use case of those for TaskFragment transition.
        return shouldStartNonAppWindowAnimationsForKeyguardExit(transit)
                || shouldAttachNavBarToApp(mService, mDisplayContent, transit)
                || shouldStartWallpaperAnimation(mDisplayContent);
    }

    /**
     * Finds the common {@link android.window.TaskFragmentOrganizer} that organizes all app windows
     * in the current transition.
     * @return {@code null} if there is no such organizer, or if there are more than one.
     */
    @Nullable
    private ITaskFragmentOrganizer findTaskFragmentOrganizerForAllWindows() {
        mTempTransitionWindows.clear();
        mTempTransitionWindows.addAll(mDisplayContent.mClosingApps);
        mTempTransitionWindows.addAll(mDisplayContent.mOpeningApps);
        mTempTransitionWindows.addAll(mDisplayContent.mChangingContainers);

        // It should only animated by the organizer if all windows are below the same leaf Task.
        Task leafTask = null;
        for (int i = mTempTransitionWindows.size() - 1; i >= 0; i--) {
            final ActivityRecord r = getAppFromContainer(mTempTransitionWindows.get(i));
            if (r == null) {
                leafTask = null;
                break;
            }
            // The activity may be a child of embedded Task, but we want to find the owner Task.
            // As a result, find the organized TaskFragment first.
            final TaskFragment organizedTaskFragment = r.getOrganizedTaskFragment();
            // There are also cases where the Task contains non-embedded activity, such as launching
            // split TaskFragments from a non-embedded activity.
            // The hierarchy may looks like this:
            // - Task
            //    - Activity
            //    - TaskFragment
            //       - Activity
            //    - TaskFragment
            //       - Activity
            // We also want to have the organizer handle the transition for such case.
            final Task task = organizedTaskFragment != null
                    ? organizedTaskFragment.getTask()
                    : r.getTask();
            if (task == null) {
                leafTask = null;
                break;
            }
            // We don't want the organizer to handle transition of other non-embedded Task.
            if (leafTask != null && leafTask != task) {
                leafTask = null;
                break;
            }
            final ActivityRecord rootActivity = task.getRootActivity();
            // We don't want the organizer to handle transition when the whole app is closing.
            if (rootActivity == null) {
                leafTask = null;
                break;
            }
            // We don't want the organizer to handle transition of non-embedded activity of other
            // app.
            if (r.getUid() != task.effectiveUid && !r.isEmbedded()) {
                leafTask = null;
                break;
            }
            leafTask = task;
        }
        mTempTransitionWindows.clear();
        if (leafTask == null) {
            return null;
        }

        // We don't support remote animation for Task with multiple TaskFragmentOrganizers.
        final ITaskFragmentOrganizer[] organizer = new ITaskFragmentOrganizer[1];
        final boolean hasMultipleOrganizers = leafTask.forAllLeafTaskFragments(taskFragment -> {
            final ITaskFragmentOrganizer tfOrganizer = taskFragment.getTaskFragmentOrganizer();
            if (tfOrganizer == null) {
                return false;
            }
            if (organizer[0] != null && !organizer[0].asBinder().equals(tfOrganizer.asBinder())) {
                return true;
            }
            organizer[0] = tfOrganizer;
            return false;
        });
        if (hasMultipleOrganizers) {
            ProtoLog.e(WM_DEBUG_APP_TRANSITIONS, "We don't support remote animation for"
                    + " Task with multiple TaskFragmentOrganizers.");
            return null;
        }
        return organizer[0];
    }

    /**
     * Overrides the pending transition with the remote animation defined by the
     * {@link ITaskFragmentOrganizer} if all windows in the transition are children of
     * {@link TaskFragment} that are organized by the same organizer.
     *
     * @return {@code true} if the transition is overridden.
     */
    private boolean overrideWithTaskFragmentRemoteAnimation(@TransitionOldType int transit,
            ArraySet<Integer> activityTypes) {
        if (transitionMayContainNonAppWindows(transit)) {
            return false;
        }

        final ITaskFragmentOrganizer organizer = findTaskFragmentOrganizerForAllWindows();
        final RemoteAnimationDefinition definition = organizer != null
                ? mDisplayContent.mAtmService.mTaskFragmentOrganizerController
                    .getRemoteAnimationDefinition(organizer)
                : null;
        final RemoteAnimationAdapter adapter = definition != null
                ? definition.getAdapter(transit, activityTypes)
                : null;
        if (adapter == null) {
            return false;
        }
        mDisplayContent.mAppTransition.overridePendingAppTransitionRemote(adapter);
        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                "Override with TaskFragment remote animation for transit=%s",
                AppTransition.appTransitionOldToString(transit));
        return true;
    }

    /**
     * Overrides the pending transition with the remote animation defined for the transition in the
     * set of defined remote animations in the app window token.
     */
    private void overrideWithRemoteAnimationIfSet(@Nullable ActivityRecord animLpActivity,
            @TransitionOldType int transit, ArraySet<Integer> activityTypes) {
        if (transit == TRANSIT_OLD_CRASHING_ACTIVITY_CLOSE) {
            // The crash transition has higher priority than any involved remote animations.
            return;
        }
        final RemoteAnimationAdapter adapter =
                getRemoteAnimationOverride(animLpActivity, transit, activityTypes);
        if (adapter != null
                && mDisplayContent.mAppTransition.getRemoteAnimationController() == null) {
            mDisplayContent.mAppTransition.overridePendingAppTransitionRemote(adapter);
        }
    }

    @Nullable
    static Task findRootTaskFromContainer(WindowContainer wc) {
        return wc.asTaskFragment() != null ? wc.asTaskFragment().getRootTask()
                : wc.asActivityRecord().getRootTask();
    }

    @Nullable
    static ActivityRecord getAppFromContainer(WindowContainer wc) {
        return wc.asTaskFragment() != null ? wc.asTaskFragment().getTopNonFinishingActivity()
                : wc.asActivityRecord();
    }

    /**
     * @return The window token that determines the animation theme.
     */
    @Nullable
    private ActivityRecord findAnimLayoutParamsToken(@TransitionOldType int transit,
            ArraySet<Integer> activityTypes) {
        ActivityRecord result;
        final ArraySet<ActivityRecord> closingApps = mDisplayContent.mClosingApps;
        final ArraySet<ActivityRecord> openingApps = mDisplayContent.mOpeningApps;
        final ArraySet<WindowContainer> changingApps = mDisplayContent.mChangingContainers;

        // Remote animations always win, but fullscreen tokens override non-fullscreen tokens.
        result = lookForHighestTokenWithFilter(closingApps, openingApps, changingApps,
                w -> w.getRemoteAnimationDefinition() != null
                        && w.getRemoteAnimationDefinition().hasTransition(transit, activityTypes));
        if (result != null) {
            return result;
        }
        result = lookForHighestTokenWithFilter(closingApps, openingApps, changingApps,
                w -> w.fillsParent() && w.findMainWindow() != null);
        if (result != null) {
            return result;
        }
        return lookForHighestTokenWithFilter(closingApps, openingApps, changingApps,
                w -> w.findMainWindow() != null);
    }

    /**
     * @return The set of {@link android.app.WindowConfiguration.ActivityType}s contained in the set
     *         of apps in {@code array1}, {@code array2}, and {@code array3}.
     */
    private static ArraySet<Integer> collectActivityTypes(ArraySet<ActivityRecord> array1,
            ArraySet<ActivityRecord> array2, ArraySet<WindowContainer> array3) {
        final ArraySet<Integer> result = new ArraySet<>();
        for (int i = array1.size() - 1; i >= 0; i--) {
            result.add(array1.valueAt(i).getActivityType());
        }
        for (int i = array2.size() - 1; i >= 0; i--) {
            result.add(array2.valueAt(i).getActivityType());
        }
        for (int i = array3.size() - 1; i >= 0; i--) {
            result.add(array3.valueAt(i).getActivityType());
        }
        return result;
    }

    private static ActivityRecord lookForHighestTokenWithFilter(ArraySet<ActivityRecord> array1,
            ArraySet<ActivityRecord> array2, ArraySet<WindowContainer> array3,
            Predicate<ActivityRecord> filter) {
        final int array2base = array1.size();
        final int array3base = array2.size() + array2base;
        final int count = array3base + array3.size();
        int bestPrefixOrderIndex = Integer.MIN_VALUE;
        ActivityRecord bestToken = null;
        for (int i = 0; i < count; i++) {
            final WindowContainer wtoken = i < array2base
                    ? array1.valueAt(i)
                    : (i < array3base
                            ? array2.valueAt(i - array2base)
                            : array3.valueAt(i - array3base));
            final int prefixOrderIndex = wtoken.getPrefixOrderIndex();
            final ActivityRecord r = getAppFromContainer(wtoken);
            if (r != null && filter.test(r) && prefixOrderIndex > bestPrefixOrderIndex) {
                bestPrefixOrderIndex = prefixOrderIndex;
                bestToken = r;
            }
        }
        return bestToken;
    }

    private boolean containsVoiceInteraction(ArraySet<ActivityRecord> apps) {
        for (int i = apps.size() - 1; i >= 0; i--) {
            if (apps.valueAt(i).mVoiceInteraction) {
                return true;
            }
        }
        return false;
    }

    /**
     * Apply animation to the set of window containers.
     *
     * @param wcs The list of {@link WindowContainer}s to which an app transition animation applies.
     * @param apps The list of {@link ActivityRecord}s being transitioning.
     * @param transit The current transition type.
     * @param visible {@code true} if the apps becomes visible, {@code false} if the apps becomes
     *                invisible.
     * @param animLp Layout parameters in which an app transition animation runs.
     * @param voiceInteraction {@code true} if one of the apps in this transition belongs to a voice
     *                         interaction session driving task.
     */
    private void applyAnimations(ArraySet<WindowContainer> wcs, ArraySet<ActivityRecord> apps,
            @TransitionOldType int transit, boolean visible, LayoutParams animLp,
            boolean voiceInteraction) {
        final int wcsCount = wcs.size();
        for (int i = 0; i < wcsCount; i++) {
            final WindowContainer wc = wcs.valueAt(i);
            // If app transition animation target is promoted to higher level, SurfaceAnimator
            // triggers WC#onAnimationFinished only on the promoted target. So we need to take care
            // of triggering AR#onAnimationFinished on each ActivityRecord which is a part of the
            // app transition.
            final ArrayList<ActivityRecord> transitioningDescendants = new ArrayList<>();
            for (int j = 0; j < apps.size(); ++j) {
                final ActivityRecord app = apps.valueAt(j);
                if (app.isDescendantOf(wc)) {
                    transitioningDescendants.add(app);
                }
            }
            wc.applyAnimation(animLp, transit, visible, voiceInteraction, transitioningDescendants);
        }
    }

    /**
     * Find WindowContainers to be animated from a set of opening and closing apps. We will promote
     * animation targets to higher level in the window hierarchy if possible.
     *
     * @param visible {@code true} to get animation targets for opening apps, {@code false} to get
     *                            animation targets for closing apps.
     * @return {@link WindowContainer}s to be animated.
     */
    @VisibleForTesting
    static ArraySet<WindowContainer> getAnimationTargets(
            ArraySet<ActivityRecord> openingApps, ArraySet<ActivityRecord> closingApps,
            boolean visible) {

        // The candidates of animation targets, which might be able to promote to higher level.
        final LinkedList<WindowContainer> candidates = new LinkedList<>();
        final ArraySet<ActivityRecord> apps = visible ? openingApps : closingApps;
        for (int i = 0; i < apps.size(); ++i) {
            final ActivityRecord app = apps.valueAt(i);
            if (app.shouldApplyAnimation(visible)) {
                candidates.add(app);
                ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                        "Changing app %s visible=%b performLayout=%b",
                        app, app.isVisible(), false);
            }
        }

        final ArraySet<ActivityRecord> otherApps = visible ? closingApps : openingApps;
        // Ancestors of closing apps while finding animation targets for opening apps, or ancestors
        // of opening apps while finding animation targets for closing apps.
        final ArraySet<WindowContainer> otherAncestors = new ArraySet<>();
        for (int i = 0; i < otherApps.size(); ++i) {
            for (WindowContainer wc = otherApps.valueAt(i); wc != null; wc = wc.getParent()) {
                otherAncestors.add(wc);
            }
        }

        // The final animation targets which cannot promote to higher level anymore.
        final ArraySet<WindowContainer> targets = new ArraySet<>();
        final ArrayList<WindowContainer> siblings = new ArrayList<>();
        while (!candidates.isEmpty()) {
            final WindowContainer current = candidates.removeFirst();
            final WindowContainer parent = current.getParent();
            siblings.clear();
            siblings.add(current);
            boolean canPromote = true;

            if (parent == null || !parent.canCreateRemoteAnimationTarget()
                    || !parent.canBeAnimationTarget()
                    // We cannot promote the animation on Task's parent when the task is in
                    // clearing task in case the animating get stuck when performing the opening
                    // task that behind it.
                    || (current.asTask() != null && current.asTask().mInRemoveTask)) {
                canPromote = false;
            } else {
                // In case a descendant of the parent belongs to the other group, we cannot promote
                // the animation target from "current" to the parent.
                //
                // Example: Imagine we're checking if we can animate a Task instead of a set of
                // ActivityRecords. In case an activity starts a new activity within a same Task,
                // an ActivityRecord of an existing activity belongs to the opening apps, at the
                // same time, the other ActivityRecord of a new activity belongs to the closing
                // apps. In this case, we cannot promote the animation target to Task level, but
                // need to animate each individual activity.
                //
                // [Task] +- [ActivityRecord1] (in opening apps)
                //        +- [ActivityRecord2] (in closing apps)
                if (otherAncestors.contains(parent)) {
                    canPromote = false;
                }

                // Find all siblings of the current WindowContainer in "candidates", move them into
                // a separate list "siblings", and checks if an animation target can be promoted
                // to its parent.
                //
                // We can promote an animation target to its parent if and only if all visible
                // siblings will be animating.
                //
                // Example: Imagine that a Task contains two visible activity record, but only one
                // of them is included in the opening apps and the other belongs to neither opening
                // or closing apps. This happens when an activity launches another translucent
                // activity in the same Task. In this case, we cannot animate Task, but have to
                // animate each activity, otherwise an activity behind the translucent activity also
                // animates.
                //
                // [Task] +- [ActivityRecord1] (visible, in opening apps)
                //        +- [ActivityRecord2] (visible, not in opening apps)
                for (int j = 0; j < parent.getChildCount(); ++j) {
                    final WindowContainer sibling = parent.getChildAt(j);
                    if (candidates.remove(sibling)) {
                        siblings.add(sibling);
                    } else if (sibling != current && sibling.isVisible()) {
                        canPromote = false;
                    }
                }
            }

            if (canPromote) {
                candidates.add(parent);
            } else {
                targets.addAll(siblings);
            }
        }
        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS_ANIM, "getAnimationTarget in=%s, out=%s",
                apps, targets);
        return targets;
    }

    /**
     * Apply an app transition animation based on a set of {@link ActivityRecord}
     *
     * @param openingApps The list of opening apps to which an app transition animation applies.
     * @param closingApps The list of closing apps to which an app transition animation applies.
     * @param transit The current transition type.
     * @param animLp Layout parameters in which an app transition animation runs.
     * @param voiceInteraction {@code true} if one of the apps in this transition belongs to a voice
     *                         interaction session driving task.
     */
    private void applyAnimations(ArraySet<ActivityRecord> openingApps,
            ArraySet<ActivityRecord> closingApps, @TransitionOldType int transit,
            LayoutParams animLp, boolean voiceInteraction) {
        if (transit == WindowManager.TRANSIT_OLD_UNSET
                || (openingApps.isEmpty() && closingApps.isEmpty())) {
            return;
        }

        final ArraySet<WindowContainer> openingWcs = getAnimationTargets(
                openingApps, closingApps, true /* visible */);
        final ArraySet<WindowContainer> closingWcs = getAnimationTargets(
                openingApps, closingApps, false /* visible */);
        applyAnimations(openingWcs, openingApps, transit, true /* visible */, animLp,
                voiceInteraction);
        applyAnimations(closingWcs, closingApps, transit, false /* visible */, animLp,
                voiceInteraction);
        final RecentsAnimationController rac = mService.getRecentsAnimationController();
        if (rac != null) {
            rac.sendTasksAppeared();
        }

        for (int i = 0; i < openingApps.size(); ++i) {
            openingApps.valueAtUnchecked(i).mOverrideTaskTransition = false;
        }
        for (int i = 0; i < closingApps.size(); ++i) {
            closingApps.valueAtUnchecked(i).mOverrideTaskTransition = false;
        }

        final AccessibilityController accessibilityController =
                mDisplayContent.mWmService.mAccessibilityController;
        if (accessibilityController.hasCallbacks()) {
            accessibilityController.onAppWindowTransition(mDisplayContent.getDisplayId(), transit);
        }
    }

    private void handleOpeningApps() {
        final ArraySet<ActivityRecord> openingApps = mDisplayContent.mOpeningApps;
        final int appsCount = openingApps.size();

        for (int i = 0; i < appsCount; i++) {
            final ActivityRecord app = openingApps.valueAt(i);
            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Now opening app %s", app);

            app.commitVisibility(true /* visible */, false /* performLayout */);

            // In case a trampoline activity is used, it can happen that a new ActivityRecord is
            // added and a new app transition starts before the previous app transition animation
            // ends. So we cannot simply use app.isAnimating(PARENTS) to determine if the app must
            // to be added to the list of tokens to be notified of app transition complete.
            final WindowContainer wc = app.getAnimatingContainer(PARENTS,
                    ANIMATION_TYPE_APP_TRANSITION);
            if (wc == null || !wc.getAnimationSources().contains(app)) {
                // This token isn't going to be animating. Add it to the list of tokens to
                // be notified of app transition complete since the notification will not be
                // sent be the app window animator.
                mDisplayContent.mNoAnimationNotifyOnTransitionFinished.add(app.token);
            }
            app.updateReportedVisibilityLocked();
            app.waitingToShow = false;
            if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
                    ">>> OPEN TRANSACTION handleAppTransitionReady()");
            mService.openSurfaceTransaction();
            try {
                app.showAllWindowsLocked();
            } finally {
                mService.closeSurfaceTransaction("handleAppTransitionReady");
                if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
                        "<<< CLOSE TRANSACTION handleAppTransitionReady()");
            }

            if (mDisplayContent.mAppTransition.isNextAppTransitionThumbnailUp()) {
                app.attachThumbnailAnimation();
            } else if (mDisplayContent.mAppTransition.isNextAppTransitionOpenCrossProfileApps()) {
                app.attachCrossProfileAppsThumbnailAnimation();
            }
        }
    }

    private void handleClosingApps() {
        final ArraySet<ActivityRecord> closingApps = mDisplayContent.mClosingApps;
        final int appsCount = closingApps.size();

        for (int i = 0; i < appsCount; i++) {
            final ActivityRecord app = closingApps.valueAt(i);
            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Now closing app %s", app);

            app.commitVisibility(false /* visible */, false /* performLayout */);
            app.updateReportedVisibilityLocked();
            // Force the allDrawn flag, because we want to start
            // this guy's animations regardless of whether it's
            // gotten drawn.
            app.allDrawn = true;
            // Ensure that apps that are mid-starting are also scheduled to have their
            // starting windows removed after the animation is complete
            if (app.mStartingWindow != null && !app.mStartingWindow.mAnimatingExit) {
                app.removeStartingWindow();
            }

            if (mDisplayContent.mAppTransition.isNextAppTransitionThumbnailDown()) {
                app.attachThumbnailAnimation();
            }
        }
    }

    private void handleChangingApps(@TransitionOldType int transit) {
        final ArraySet<WindowContainer> apps = mDisplayContent.mChangingContainers;
        final int appsCount = apps.size();
        for (int i = 0; i < appsCount; i++) {
            WindowContainer wc = apps.valueAt(i);
            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Now changing app %s", wc);
            wc.applyAnimation(null, transit, true, false, null /* sources */);
        }
    }

    private void handleNonAppWindowsInTransition(@TransitionOldType int transit, int flags) {
        if (transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY
                && !WindowManagerService.sEnableRemoteKeyguardGoingAwayAnimation) {
            if ((flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER) != 0
                    && (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION) == 0
                    && (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION) == 0) {
                Animation anim = mService.mPolicy.createKeyguardWallpaperExit(
                        (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE) != 0);
                if (anim != null) {
                    anim.scaleCurrentDuration(mService.getTransitionAnimationScaleLocked());
                    mDisplayContent.mWallpaperController.startWallpaperAnimation(anim);
                }
            }
        }
        if ((transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY
                || transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER)
                && !WindowManagerService.sEnableRemoteKeyguardGoingAwayAnimation) {
            mDisplayContent.startKeyguardExitOnNonAppWindows(
                    transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
                    (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE) != 0,
                    (flags & TRANSIT_FLAG_KEYGUARD_GOING_AWAY_SUBTLE_ANIMATION) != 0);
        }
    }

    private boolean transitionGoodToGo(ArraySet<? extends WindowContainer> apps,
            ArrayMap<WindowContainer, Integer> outReasons) {
        ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                "Checking %d opening apps (frozen=%b timeout=%b)...", apps.size(),
                mService.mDisplayFrozen, mDisplayContent.mAppTransition.isTimeout());
        if (mDisplayContent.mAppTransition.isTimeout()) {
            return true;
        }
        final ScreenRotationAnimation screenRotationAnimation = mService.mRoot.getDisplayContent(
                Display.DEFAULT_DISPLAY).getRotationAnimation();

        // Imagine the case where we are changing orientation due to an app transition, but a
        // previous orientation change is still in progress. We won't process the orientation
        // change for our transition because we need to wait for the rotation animation to
        // finish.
        // If we start the app transition at this point, we will interrupt it halfway with a
        // new rotation animation after the old one finally finishes. It's better to defer the
        // app transition.
        if (screenRotationAnimation != null && screenRotationAnimation.isAnimating()
                && mDisplayContent.getDisplayRotation().needsUpdate()) {
            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                    "Delaying app transition for screen rotation animation to finish");
            return false;
        }
        for (int i = 0; i < apps.size(); i++) {
            WindowContainer wc = apps.valueAt(i);
            final ActivityRecord activity = getAppFromContainer(wc);
            if (activity == null) {
                continue;
            }
            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                    "Check opening app=%s: allDrawn=%b startingDisplayed=%b "
                            + "startingMoved=%b isRelaunching()=%b startingWindow=%s",
                    activity, activity.allDrawn, activity.startingDisplayed,
                    activity.startingMoved, activity.isRelaunching(),
                    activity.mStartingWindow);

            final boolean allDrawn = activity.allDrawn && !activity.isRelaunching();
            if (!allDrawn && !activity.startingDisplayed && !activity.startingMoved) {
                return false;
            }
            if (allDrawn) {
                outReasons.put(activity, APP_TRANSITION_WINDOWS_DRAWN);
            } else {
                outReasons.put(activity,
                        activity.mStartingData instanceof SplashScreenStartingData
                                ? APP_TRANSITION_SPLASH_SCREEN
                                : APP_TRANSITION_SNAPSHOT);
            }
        }

        // We also need to wait for the specs to be fetched, if needed.
        if (mDisplayContent.mAppTransition.isFetchingAppTransitionsSpecs()) {
            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "isFetchingAppTransitionSpecs=true");
            return false;
        }

        if (!mDisplayContent.mUnknownAppVisibilityController.allResolved()) {
            ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "unknownApps is not empty: %s",
                    mDisplayContent.mUnknownAppVisibilityController.getDebugMessage());
            return false;
        }

        // If the wallpaper is visible, we need to check it's ready too.
        return !mWallpaperControllerLocked.isWallpaperVisible()
                || mWallpaperControllerLocked.wallpaperTransitionReady();
    }

    private boolean transitionGoodToGoForTaskFragments() {
        if (mDisplayContent.mAppTransition.isTimeout()) {
            return true;
        }

        // Check all Tasks in this transition. This is needed because new TaskFragment created for
        // launching activity may not be in the tracking lists, but we still want to wait for the
        // activity launch to start the transition.
        final ArraySet<Task> rootTasks = new ArraySet<>();
        for (int i = mDisplayContent.mOpeningApps.size() - 1; i >= 0; i--) {
            rootTasks.add(mDisplayContent.mOpeningApps.valueAt(i).getRootTask());
        }
        for (int i = mDisplayContent.mClosingApps.size() - 1; i >= 0; i--) {
            rootTasks.add(mDisplayContent.mClosingApps.valueAt(i).getRootTask());
        }
        for (int i = mDisplayContent.mChangingContainers.size() - 1; i >= 0; i--) {
            rootTasks.add(
                    findRootTaskFromContainer(mDisplayContent.mChangingContainers.valueAt(i)));
        }

        // Organized TaskFragment can be empty for two situations:
        // 1. New created and is waiting for Activity launch. In this case, we want to wait for
        //    the Activity launch to trigger the transition.
        // 2. Last Activity is just removed. In this case, we want to wait for organizer to
        //    remove the TaskFragment because it may also want to change other TaskFragments in
        //    the same transition.
        for (int i = rootTasks.size() - 1; i >= 0; i--) {
            final Task rootTask = rootTasks.valueAt(i);
            if (rootTask == null) {
                // It is possible that one activity may have been removed from the hierarchy. No
                // need to check for this case.
                continue;
            }
            final boolean notReady = rootTask.forAllLeafTaskFragments(taskFragment -> {
                if (!taskFragment.isReadyToTransit()) {
                    ProtoLog.v(WM_DEBUG_APP_TRANSITIONS, "Organized TaskFragment is not ready= %s",
                            taskFragment);
                    return true;
                }
                return false;
            });
            if (notReady) {
                return false;
            }
        }
        return true;
    }

    /**
     * Identifies whether the current transition occurs within a single task or not. This is used
     * to determine whether animations should be clipped to the task bounds instead of root task
     * bounds.
     */
    @VisibleForTesting
    boolean isTransitWithinTask(@TransitionOldType int transit, Task task) {
        if (task == null
                || !mDisplayContent.mChangingContainers.isEmpty()) {
            // if there is no task, then we can't constrain to the task.
            // if anything is changing, it can animate outside its task.
            return false;
        }
        if (!(transit == TRANSIT_OLD_ACTIVITY_OPEN
                || transit == TRANSIT_OLD_ACTIVITY_CLOSE
                || transit == TRANSIT_OLD_ACTIVITY_RELAUNCH)) {
            // only activity-level transitions will be within-task.
            return false;
        }
        // check that all components are in the task.
        for (ActivityRecord activity : mDisplayContent.mOpeningApps) {
            Task activityTask = activity.getTask();
            if (activityTask != task) {
                return false;
            }
        }
        for (ActivityRecord activity : mDisplayContent.mClosingApps) {
            if (activity.getTask() != task) {
                return false;
            }
        }
        return true;
    }

    private static boolean canBeWallpaperTarget(ArraySet<ActivityRecord> apps) {
        for (int i = apps.size() - 1; i >= 0; i--) {
            if (apps.valueAt(i).windowsCanBeWallpaperTarget()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Finds the top app in a list of apps, using its {@link ActivityRecord#getPrefixOrderIndex} to
     * compare z-order.
     *
     * @param apps The list of apps to search.
     * @param ignoreInvisible If set to true, ignores apps that are not
     *                        {@link ActivityRecord#isVisible}.
     * @return The top {@link ActivityRecord}.
     */
    private static ActivityRecord getTopApp(ArraySet<? extends WindowContainer> apps,
            boolean ignoreInvisible) {
        int topPrefixOrderIndex = Integer.MIN_VALUE;
        ActivityRecord topApp = null;
        for (int i = apps.size() - 1; i >= 0; i--) {
            final ActivityRecord app = getAppFromContainer(apps.valueAt(i));
            if (app == null || ignoreInvisible && !app.isVisible()) {
                continue;
            }
            final int prefixOrderIndex = app.getPrefixOrderIndex();
            if (prefixOrderIndex > topPrefixOrderIndex) {
                topPrefixOrderIndex = prefixOrderIndex;
                topApp = app;
            }
        }
        return topApp;
    }
}
