/*
 * 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.back;

import static android.app.ActivityTaskManager.INVALID_TASK_ID;
import static android.view.Display.INVALID_DISPLAY;
import static android.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;
import static android.view.WindowManager.TRANSIT_CHANGE;
import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION;
import static android.window.BackEvent.EDGE_NONE;
import static android.window.DesktopExperienceFlags.ENABLE_INDEPENDENT_BACK_IN_PROJECTED;
import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED;
import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP;
import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;

import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME;
import static com.android.window.flags.Flags.predictiveBackDelayWmTransition;
import static com.android.window.flags.Flags.predictiveBackStopKeycodeBackForwarding;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;

import android.animation.ValueAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
import android.app.TaskInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Point;
import android.graphics.Rect;
import android.hardware.input.InputManager;
import android.hardware.input.KeyGestureEvent;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
import android.view.IRemoteAnimationRunner;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.RemoteAnimationTarget;
import android.view.SurfaceControl;
import android.view.WindowManager;
import android.window.BackAnimationAdapter;
import android.window.BackEvent;
import android.window.BackMotionEvent;
import android.window.BackNavigationInfo;
import android.window.BackTouchTracker;
import android.window.IBackAnimationFinishedCallback;
import android.window.IBackAnimationHandoffHandler;
import android.window.IBackAnimationRunner;
import android.window.IOnBackInvokedCallback;
import android.window.TransitionInfo;
import android.window.TransitionRequestInfo;
import android.window.WindowAnimationState;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.util.LatencyTracker;
import com.android.internal.view.AppearanceRegion;
import com.android.wm.shell.R;
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.shared.TransitionUtil;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ConfigurationChangeListener;
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.Transitions;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

/**
 * Controls the window animation run when a user initiates a back gesture.
 */
public class BackAnimationController implements RemoteCallable<BackAnimationController>,
        ConfigurationChangeListener {
    private static final String TAG = "ShellBackPreview";

    /**
     * Max duration to wait for an animation to finish before triggering the real back.
     */
    private static final long MAX_ANIMATION_DURATION = 2000;
    private long mMaxAnimationDuration = MAX_ANIMATION_DURATION;
    // Note: Must keep a reference when register to ValueAnimator.
    private final ValueAnimator.DurationScaleChangeListener mAnimationScaleChangeListener;
    private final LatencyTracker mLatencyTracker;
    @ShellMainThread private final Handler mHandler;

    /** True when a back gesture is ongoing */
    @VisibleForTesting public boolean mBackGestureStarted = false;

    /** Tracks if an uninterruptible animation is in progress */
    private boolean mPostCommitAnimationInProgress = false;
    private boolean mRealCallbackInvoked = false;

    /** Tracks if we should start the back gesture on the next motion move event */
    private boolean mShouldStartOnNextMoveEvent = false;
    private boolean mOnBackStartDispatched = false;
    private boolean mThresholdCrossed = false;
    private boolean mPointersPilfered = false;
    private boolean mBackAnimationTriggered = false;
    private final boolean mRequirePointerPilfer;

    /** Registry for the back animations */
    private final ShellBackAnimationRegistry mShellBackAnimationRegistry;

    @Nullable
    private BackNavigationInfo mBackNavigationInfo;
    private boolean mReceivedNullNavigationInfo = false;
    private final IActivityTaskManager mActivityTaskManager;
    private final Context mContext;
    private final ShellController mShellController;
    private final ShellCommandHandler mShellCommandHandler;
    private final ShellExecutor mShellExecutor;
    private final WindowManager mWindowManager;
    private final Transitions mTransitions;
    private final InputManager mInputManager;
    @VisibleForTesting
    final BackTransitionHandler mBackTransitionHandler;
    @VisibleForTesting
    final Rect mTouchableArea = new Rect();

    /**
     * Tracks the current user back gesture.
     */
    private BackTouchTracker mCurrentTracker = new BackTouchTracker();

    /**
     * Tracks the next back gesture in case a new user gesture has started while the back animation
     * (and navigation) associated with {@link #mCurrentTracker} have not yet finished.
     */
    private BackTouchTracker mQueuedTracker = new BackTouchTracker();
    private final TransitionIdleRunner mTransitionIdleRunner = new TransitionIdleRunner();

    private final BackTransitionObserver mBackTransitionObserver =
            new BackTransitionObserver();

    private final Runnable mAnimationTimeoutRunnable = () -> {
        ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation didn't finish in %d ms. Resetting...",
                mMaxAnimationDuration);
        finishBackAnimation();
    };

    private IBackAnimationFinishedCallback mBackAnimationFinishedCallback;
    @VisibleForTesting
    BackAnimationAdapter mBackAnimationAdapter;

    @Nullable
    private IOnBackInvokedCallback mActiveCallback;
    @Nullable
    @VisibleForTesting
    RemoteAnimationTarget[] mApps;

    @VisibleForTesting
    final RemoteCallback mNavigationObserver = new RemoteCallback(
            new RemoteCallback.OnResultListener() {
                @Override
                public void onResult(@Nullable Bundle result) {
                    mShellExecutor.execute(() -> {
                        if (mBackGestureStarted && result != null && result.getBoolean(
                                BackNavigationInfo.KEY_TOUCH_GESTURE_TRANSFERRED)) {
                            // Host app won't able to process motion event anymore, so pilfer
                            // pointers anyway.
                            if (mBackNavigationInfo != null) {
                                mBackNavigationInfo.disableAppProgressGenerationAllowed();
                            }
                            tryPilferPointers();
                            return;
                        }
                        if (!mBackGestureStarted || mPostCommitAnimationInProgress) {
                            // If an uninterruptible animation is already in progress, we should
                            // ignore this due to it may cause focus lost. (alpha = 0)
                            return;
                        }
                        ProtoLog.i(WM_SHELL_BACK_PREVIEW, "Navigation window gone.");
                        setTriggerBack(false);
                        // Trigger close transition if necessary.
                        mBackTransitionHandler.onAnimationFinished();
                        resetTouchTracker();
                        // Don't wait for animation start
                        mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
                    });
                }
            });

    private final BackAnimationBackground mAnimationBackground;
    private StatusBarCustomizer mCustomizer;
    private boolean mTrackingLatency;

    // Keep previous navigation type before remove mBackNavigationInfo.
    @BackNavigationInfo.BackTargetType
    private int mPreviousNavigationType;
    private Runnable mPilferPointerCallback;
    private BackAnimation.TopUiRequest mRequestTopUiCallback;

    private final IBackAnimationHandoffHandler mHandoffHandler =
            new IBackAnimationHandoffHandler.Stub() {
                @Override
                public void handOffAnimation(
                        RemoteAnimationTarget[] targets, WindowAnimationState[] states) {
                    mBackTransitionHandler.handOffAnimation(targets, states);
                }
            };

    public BackAnimationController(
            @NonNull ShellInit shellInit,
            @NonNull ShellController shellController,
            @NonNull @ShellMainThread ShellExecutor shellExecutor,
            Context context,
            @NonNull BackAnimationBackground backAnimationBackground,
            ShellBackAnimationRegistry shellBackAnimationRegistry,
            ShellCommandHandler shellCommandHandler,
            Transitions transitions,
            @ShellMainThread Handler handler) {
        this(
                shellInit,
                shellController,
                shellExecutor,
                ActivityTaskManager.getService(),
                context,
                backAnimationBackground,
                shellBackAnimationRegistry,
                shellCommandHandler,
                transitions,
                handler);
    }

    @VisibleForTesting
    BackAnimationController(
            @NonNull ShellInit shellInit,
            @NonNull ShellController shellController,
            @NonNull @ShellMainThread ShellExecutor shellExecutor,
            @NonNull IActivityTaskManager activityTaskManager,
            Context context,
            @NonNull BackAnimationBackground backAnimationBackground,
            ShellBackAnimationRegistry shellBackAnimationRegistry,
            ShellCommandHandler shellCommandHandler,
            Transitions transitions,
            @NonNull @ShellMainThread Handler handler) {
        mShellController = shellController;
        mShellExecutor = shellExecutor;
        mActivityTaskManager = activityTaskManager;
        mContext = context;
        mRequirePointerPilfer =
                context.getResources().getBoolean(R.bool.config_backAnimationRequiresPointerPilfer);
        shellInit.addInitCallback(this::onInit, this);
        mAnimationBackground = backAnimationBackground;
        mShellBackAnimationRegistry = shellBackAnimationRegistry;
        mLatencyTracker = LatencyTracker.getInstance(mContext);
        mShellCommandHandler = shellCommandHandler;
        mWindowManager = context.getSystemService(WindowManager.class);
        mInputManager = context.getSystemService(InputManager.class);
        mTransitions = transitions;
        mBackTransitionHandler = new BackTransitionHandler(mTransitions);
        mTransitions.addHandler(mBackTransitionHandler);
        mHandler = handler;
        mTransitions.registerObserver(mBackTransitionObserver);
        mBackTransitionObserver.setBackTransitionHandler(mBackTransitionHandler);
        updateTouchableArea();
        mAnimationScaleChangeListener = scale -> mShellExecutor.execute(
                this::updateAnimationScale);
    }

    private void onInit() {
        createAdapter();
        mShellController.addExternalInterface(IBackAnimation.DESCRIPTOR,
                this::createExternalInterface, this);
        mShellCommandHandler.addDumpCallback(this::dump, this);
        mShellController.addConfigurationChangeListener(this);
        registerBackGestureDelegate();
        ValueAnimator.registerDurationScaleChangeListener(mAnimationScaleChangeListener);
        updateAnimationScale();
    }

    private void updateAnimationScale() {
        final float scale = Math.max(Math.min(ValueAnimator.getDurationScale(), 20), 1);
        final double result = scale * MAX_ANIMATION_DURATION;
        mMaxAnimationDuration = Math.round(result);
    }

    public BackAnimation getBackAnimationImpl() {
        return mBackAnimation;
    }

    private ExternalInterfaceBinder createExternalInterface() {
        return new IBackAnimationImpl(this);
    }

    private final BackAnimationImpl mBackAnimation = new BackAnimationImpl();

    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        mShellBackAnimationRegistry.onConfigurationChanged(newConfig);
        updateTouchableArea();
    }

    private void updateTouchableArea() {
        mTouchableArea.set(mWindowManager.getCurrentWindowMetrics().getBounds());
    }

    @Override
    public Context getContext() {
        return mContext;
    }

    @Override
    public ShellExecutor getRemoteCallExecutor() {
        return mShellExecutor;
    }

    private class BackAnimationImpl implements BackAnimation {
        @Override
        public void onBackMotion(
                float touchX,
                float touchY,
                int keyAction,
                @BackEvent.SwipeEdge int swipeEdge,
                int displayId
        ) {
            mShellExecutor.execute(
                    () -> onMotionEvent(touchX, touchY, keyAction, swipeEdge, displayId));
        }

        @Override
        public void onThresholdCrossed() {
            BackAnimationController.this.onThresholdCrossed();
        }

        @Override
        public void setTriggerBack(boolean triggerBack) {
            mShellExecutor.execute(() -> BackAnimationController.this.setTriggerBack(triggerBack));
        }

        @Override
        public void setSwipeThresholds(
                float linearDistance,
                float maxDistance,
                float nonLinearFactor) {
            mShellExecutor.execute(() -> BackAnimationController.this.setSwipeThresholds(
                    linearDistance, maxDistance, nonLinearFactor));
        }

        @Override
        public void setStatusBarCustomizer(StatusBarCustomizer customizer) {
            mCustomizer = customizer;
            mAnimationBackground.setStatusBarCustomizer(customizer);
        }

        @Override
        public void setPilferPointerCallback(Runnable callback) {
            mShellExecutor.execute(() -> {
                mPilferPointerCallback = callback;
            });
        }

        @Override
        public void setTopUiRequestCallback(TopUiRequest topUiRequest) {
            mShellExecutor.execute(() -> mRequestTopUiCallback = topUiRequest);
        }
    }

    private class IBackAnimationImpl extends IBackAnimation.Stub
            implements ExternalInterfaceBinder {
        private BackAnimationController mController;

        IBackAnimationImpl(BackAnimationController controller) {
            mController = controller;
        }

        @Override
        public void setBackToLauncherCallback(IOnBackInvokedCallback callback,
                IRemoteAnimationRunner runner) {
            executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback",
                    (controller) -> controller.registerAnimation(
                            BackNavigationInfo.TYPE_RETURN_TO_HOME,
                            new BackAnimationRunner(
                                    callback,
                                    runner,
                                    controller.mContext,
                                    CUJ_PREDICTIVE_BACK_HOME,
                                    mHandler)));
        }

        @Override
        public void clearBackToLauncherCallback() {
            executeRemoteCallWithTaskPermission(mController, "clearBackToLauncherCallback",
                    (controller) -> controller.unregisterAnimation(
                            BackNavigationInfo.TYPE_RETURN_TO_HOME));
        }

        public void customizeStatusBarAppearance(AppearanceRegion appearance) {
            executeRemoteCallWithTaskPermission(mController, "useLauncherSysBarFlags",
                    (controller) -> controller.customizeStatusBarAppearance(appearance));
        }

        @Override
        public void invalidate() {
            mController = null;
        }
    }

    private void customizeStatusBarAppearance(AppearanceRegion appearance) {
        if (mCustomizer != null) {
            mCustomizer.customizeStatusBarAppearance(appearance);
        }
    }

    void registerAnimation(@BackNavigationInfo.BackTargetType int type,
            @NonNull BackAnimationRunner runner) {
        mShellBackAnimationRegistry.registerAnimation(type, runner);
    }

    void unregisterAnimation(@BackNavigationInfo.BackTargetType int type) {
        mShellBackAnimationRegistry.unregisterAnimation(type);
    }

    private BackTouchTracker getActiveTracker() {
        if (mCurrentTracker.isActive()) return mCurrentTracker;
        if (mQueuedTracker.isActive()) return mQueuedTracker;
        return null;
    }

    @VisibleForTesting
    public void onThresholdCrossed() {
        mThresholdCrossed = true;
        // There was no focus window when calling startBackNavigation, still pilfer pointers so
        // the next focus window won't receive motion events.
        if (mBackNavigationInfo == null && mReceivedNullNavigationInfo) {
            tryPilferPointers();
            return;
        }
        // Dispatch onBackStarted, only to app callbacks.
        // System callbacks will receive onBackStarted when the remote animation starts.
        final boolean shouldDispatchToAnimator = shouldDispatchToAnimator();
        if (!shouldDispatchToAnimator && mActiveCallback != null) {
            mCurrentTracker.updateStartLocation();
            tryDispatchOnBackStarted(mActiveCallback, mCurrentTracker.createStartEvent());
            if (mBackNavigationInfo != null && !isAppProgressGenerationAllowed()) {
                tryPilferPointers();
            }
        } else if (shouldDispatchToAnimator) {
            tryPilferPointers();
        }

        startPredictiveBackAnimationIfNeeded();
    }

    private void startPredictiveBackAnimationIfNeeded() {
        if (!predictiveBackDelayWmTransition() || !mThresholdCrossed) {
            return;
        }
        mShellExecutor.execute(() -> {
            if (mBackAnimationTriggered || !shouldDispatchToAnimator()) {
                return;
            }
            mBackAnimationTriggered = true;
            boolean started;
            try {
                started = mActivityTaskManager.startPredictiveBackAnimation();
            } catch (RemoteException r) {
                Log.e(TAG, "Failed to start predictive animation", r);
                started = false;
            }
            if (!started) {
                ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Failed to start predictive back animation.");
                finishBackNavigation(mCurrentTracker.getTriggerBack());
                return;
            }
            final int backType = mBackNavigationInfo.getType();
            if (!mShellBackAnimationRegistry.startGesture(backType)) {
                mActiveCallback = null;
            }
            requestTopUi(true, backType);
        });
    }

    private boolean isAppProgressGenerationAllowed() {
        return mBackNavigationInfo.isAppProgressGenerationAllowed()
                && mBackNavigationInfo.getTouchableRegion().equals(mTouchableArea);
    }

    /**
     * Called when a new motion event needs to be transferred to this
     * {@link BackAnimationController}
     */
    public void onMotionEvent(
            float touchX,
            float touchY,
            int keyAction,
            @BackEvent.SwipeEdge int swipeEdge,
            int displayId) {

        if (ENABLE_INDEPENDENT_BACK_IN_PROJECTED.isTrue()) {
            mBackAnimationAdapter.mOriginDisplayId = displayId;
        }

        BackTouchTracker activeTouchTracker = getActiveTracker();
        if (activeTouchTracker != null) {
            activeTouchTracker.update(touchX, touchY);
        }

        // two gestures are waiting to be processed at the moment, skip any further user touches
        if (mCurrentTracker.isFinished() && mQueuedTracker.isFinished()) {
            ProtoLog.d(WM_SHELL_BACK_PREVIEW,
                    "Ignoring MotionEvent because two gestures are already being queued.");
            return;
        } else if (mBackGestureStarted && mCurrentTracker.isInInitialState()
                && mQueuedTracker.isInInitialState()) {
            ProtoLog.e(WM_SHELL_BACK_PREVIEW,
                    "Both touch trackers in initial state and mBackGestureStarted=true");
            mBackGestureStarted = false;
        }

        if (keyAction == MotionEvent.ACTION_DOWN) {
            if (!mBackGestureStarted) {
                if (swipeEdge == EDGE_NONE) {
                    // start animation immediately for non-gestural sources (without ACTION_MOVE
                    // events)
                    if (!predictiveBackDelayWmTransition()) {
                        mThresholdCrossed = true;
                    }
                    mPointersPilfered = true;
                    onGestureStarted(touchX, touchY, swipeEdge);
                    if (predictiveBackDelayWmTransition()) {
                        onThresholdCrossed();
                    }
                    mShouldStartOnNextMoveEvent = false;
                } else {
                    mShouldStartOnNextMoveEvent = true;
                }
            }
        } else if (keyAction == MotionEvent.ACTION_MOVE) {
            if (!mBackGestureStarted && mShouldStartOnNextMoveEvent) {
                // Let the animation initialized here to make sure the onPointerDownOutsideFocus
                // could be happened when ACTION_DOWN, it may change the current focus that we
                // would access it when startBackNavigation.
                onGestureStarted(touchX, touchY, swipeEdge);
                mShouldStartOnNextMoveEvent = false;
            }
            onMove(swipeEdge);
        } else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) {
            ProtoLog.d(WM_SHELL_BACK_PREVIEW,
                    "Finishing gesture with event action: %d", keyAction);
            if (keyAction == MotionEvent.ACTION_CANCEL) {
                setTriggerBack(false);
            }
            onGestureFinished();
        }
    }

    private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
        boolean interruptCancelPostCommitAnimation = mPostCommitAnimationInProgress
                && mCurrentTracker.isFinished() && !mCurrentTracker.getTriggerBack()
                && mQueuedTracker.isInInitialState();
        if (interruptCancelPostCommitAnimation) {
            // If a system animation is currently in the post-commit phase animating an
            // onBackCancelled event, let's interrupt it and start animating a new back gesture
            resetTouchTracker();
        }
        BackTouchTracker touchTracker;
        if (mCurrentTracker.isInInitialState()) {
            touchTracker = mCurrentTracker;
        } else if (mQueuedTracker.isInInitialState()) {
            touchTracker = mQueuedTracker;
        } else {
            ProtoLog.w(WM_SHELL_BACK_PREVIEW,
                    "Cannot start tracking new gesture with neither tracker in initial state.");
            return;
        }
        touchTracker.setGestureStartLocation(touchX, touchY, swipeEdge);
        touchTracker.setState(BackTouchTracker.TouchTrackerState.ACTIVE);
        mBackGestureStarted = true;

        if (interruptCancelPostCommitAnimation) {
            // post-commit cancel is currently running. let's interrupt it and dispatch a new
            // onBackStarted event.
            mPostCommitAnimationInProgress = false;
            mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
            startSystemAnimation();
        } else {
            startBackNavigation(touchTracker);
            startPredictiveBackAnimationIfNeeded();
        }
    }

    private void startBackNavigation(@NonNull BackTouchTracker touchTracker) {
        if (touchTracker != mCurrentTracker) {
            // Only start the back navigation if no other gesture is being processed. Otherwise,
            // the back navigation will fall back to legacy back event injection.
            return;
        }
        try {
            startLatencyTracking();
            if (mBackAnimationAdapter != null
                    && mShellBackAnimationRegistry.hasSupportedAnimatorsChanged()) {
                mBackAnimationAdapter.updateSupportedAnimators(
                        mShellBackAnimationRegistry.getSupportedAnimators());
            }
            mBackNavigationInfo = mActivityTaskManager.startBackNavigation(
                    mNavigationObserver, mBackAnimationAdapter);
            onBackNavigationInfoReceived(mBackNavigationInfo, touchTracker);
        } catch (RemoteException remoteException) {
            Log.e(TAG, "Failed to initAnimation", remoteException);
            finishBackNavigation(touchTracker.getTriggerBack());
        }
    }

    private class TransitionIdleRunner implements Runnable {
        int mRequestCount;

        @Override
        public void run() {
            if (mRequestCount == 0 || !mCurrentTracker.isActive()) {
                return;
            }
            if (mRequestCount > 2) {
                // Break from recursive call.
                mRequestCount = 0;
                return;
            }
            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Gesture hasn't finish after transition "
                    + "idle, start back navigation again.");
            mRequestCount = 0;
            startBackNavigation(mCurrentTracker);
            startPredictiveBackAnimationIfNeeded();
        }
    }

    private void onBackNavigationInfoReceived(@Nullable BackNavigationInfo backNavigationInfo,
            @NonNull BackTouchTracker touchTracker) {
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo);
        if (backNavigationInfo == null) {
            ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Received BackNavigationInfo is null.");
            mReceivedNullNavigationInfo = true;
            cancelLatencyTracking();
            tryPilferPointers();
            return;
        }
        final int backType = backNavigationInfo.getType();
        if (backType == BackNavigationInfo.TYPE_IN_TRANSITION) {
            mBackNavigationInfo = null;
            tryPilferPointers();
            mTransitionIdleRunner.mRequestCount++;
            mTransitions.runOnIdle(() -> mShellExecutor.executeDelayed(
                    mTransitionIdleRunner, 0));
            return;
        }
        final boolean shouldDispatchToAnimator = shouldDispatchToAnimator();
        if (shouldDispatchToAnimator) {
            if (!predictiveBackDelayWmTransition()) {
                if (!mShellBackAnimationRegistry.startGesture(backType)) {
                    mActiveCallback = null;
                }
                requestTopUi(true, backType);
            }
            tryPilferPointers();
        } else {
            mActiveCallback = mBackNavigationInfo.getOnBackInvokedCallback();
            // App is handling back animation. Cancel system animation latency tracking.
            cancelLatencyTracking();
            tryDispatchOnBackStarted(mActiveCallback, touchTracker.createStartEvent());
            if (!isAppProgressGenerationAllowed()) {
                tryPilferPointers();
            }
        }
    }

    private void onMove(@BackEvent.SwipeEdge int swipeEdge) {
        if (predictiveBackDelayWmTransition() && mCurrentTracker.isActive()) {
            mCurrentTracker.updateSwipeEdge(swipeEdge);
        }
        if (!mBackGestureStarted
                || mBackNavigationInfo == null
                || mActiveCallback == null
                || !mOnBackStartDispatched) {
            return;
        }
        // Skip dispatching if the move corresponds to the queued instead of the current gesture
        if (mQueuedTracker.isActive()) return;
        final BackMotionEvent backEvent = mCurrentTracker.createProgressEvent();
        dispatchOnBackProgressed(mActiveCallback, backEvent);
    }

    private void injectBackKey(int displayId) {
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "injectBackKey");
        sendBackEvent(KeyEvent.ACTION_DOWN, displayId);
        sendBackEvent(KeyEvent.ACTION_UP, displayId);
    }

    @SuppressLint("MissingPermission")
    private void sendBackEvent(int action, int displayId) {
        final long when = SystemClock.uptimeMillis();
        final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK, 0 /* repeat */,
                0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
                KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
                InputDevice.SOURCE_KEYBOARD);
        if (ENABLE_INDEPENDENT_BACK_IN_PROJECTED.isTrue()) {
            ev.setDisplayId(displayId);
        }

        if (!mContext.getSystemService(InputManager.class)
                .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
            ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Inject input event fail");
        }
    }

    private boolean shouldDispatchToAnimator() {
        return mBackNavigationInfo != null && mBackNavigationInfo.isPrepareRemoteAnimation();
    }

    private void tryPilferPointers() {
        if (mPointersPilfered || !mThresholdCrossed) {
            return;
        }
        if (mPilferPointerCallback != null) {
            mPilferPointerCallback.run();
        }
        mPointersPilfered = true;
    }

    private void tryDispatchOnBackStarted(
            IOnBackInvokedCallback callback,
            BackMotionEvent backEvent) {
        if (mOnBackStartDispatched
                || callback == null
                || (!mThresholdCrossed && mRequirePointerPilfer)) {
            return;
        }
        dispatchOnBackStarted(callback, backEvent);
    }

    private void dispatchOnBackStarted(
            IOnBackInvokedCallback callback,
            BackMotionEvent backEvent) {
        if (callback == null) {
            return;
        }
        try {
            callback.onBackStarted(backEvent);
            if (mBackTransitionHandler.canHandOffAnimation()) {
                callback.setHandoffHandler(mHandoffHandler);
            } else {
                callback.setHandoffHandler(null);
            }
            mOnBackStartDispatched = true;
        } catch (RemoteException e) {
            Log.e(TAG, "dispatchOnBackStarted error: ", e);
        }
    }

    private void dispatchOnBackInvoked(IOnBackInvokedCallback callback) {
        if (callback == null) {
            return;
        }
        try {
            callback.onBackInvoked();
        } catch (RemoteException e) {
            Log.e(TAG, "dispatchOnBackInvoked error: ", e);
        }
    }

    private void tryDispatchOnBackCancelled(IOnBackInvokedCallback callback) {
        if (!mOnBackStartDispatched) {
            Log.d(TAG, "Skipping dispatching onBackCancelled. Start was never dispatched.");
            return;
        }
        if (callback == null) {
            return;
        }
        try {
            callback.onBackCancelled();
        } catch (RemoteException e) {
            Log.e(TAG, "dispatchOnBackCancelled error: ", e);
        }
    }

    private void dispatchOnBackProgressed(IOnBackInvokedCallback callback,
            BackMotionEvent backEvent) {
        if (callback == null || (!shouldDispatchToAnimator() && mBackNavigationInfo != null
                && isAppProgressGenerationAllowed())) {
            return;
        }
        try {
            callback.onBackProgressed(backEvent);
        } catch (RemoteException e) {
            Log.e(TAG, "dispatchOnBackProgressed error: ", e);
        }
    }

    /**
     * @return Latest task id which back gesture has occurred on it.
     */
    public int getLatestTriggerBackTask() {
        return mBackTransitionObserver.mFocusedTaskId;
    }

    /**
     * Sets to true when the back gesture has passed the triggering threshold, false otherwise.
     */
    public void setTriggerBack(boolean triggerBack) {
        if (mActiveCallback != null) {
            try {
                mActiveCallback.setTriggerBack(triggerBack);
            } catch (RemoteException e) {
                Log.e(TAG, "remote setTriggerBack error: ", e);
            }
        }
        BackTouchTracker activeBackGestureInfo = getActiveTracker();
        if (activeBackGestureInfo != null) {
            activeBackGestureInfo.setTriggerBack(triggerBack);
        }
    }

    private void setSwipeThresholds(
            float linearDistance,
            float maxDistance,
            float nonLinearFactor) {
        mCurrentTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor);
        mQueuedTracker.setProgressThresholds(linearDistance, maxDistance, nonLinearFactor);
    }

    private void invokeOrCancelBack(@NonNull BackTouchTracker touchTracker) {
        // Make a synchronized call to core before dispatch back event to client side.
        // If the close transition happens before the core receives onAnimationFinished, there will
        // play a second close animation for that transition.
        if (mBackAnimationFinishedCallback != null) {
            try {
                mBackAnimationFinishedCallback.onAnimationFinished(touchTracker.getTriggerBack());
            } catch (RemoteException e) {
                Log.e(TAG, "Failed call IBackAnimationFinishedCallback", e);
            }
            mBackAnimationFinishedCallback = null;
        }

        if (mBackNavigationInfo != null && !mRealCallbackInvoked) {
            final IOnBackInvokedCallback callback = mBackNavigationInfo.getOnBackInvokedCallback();
            if (touchTracker.getTriggerBack()) {
                dispatchOnBackInvoked(callback);
            } else {
                tryDispatchOnBackCancelled(callback);
            }
        }
        mRealCallbackInvoked = false;
        finishBackNavigation(touchTracker.getTriggerBack());
    }

    /**
     * Called when the gesture is released, then it could start the post commit animation.
     */
    private void onGestureFinished() {
        BackTouchTracker activeTouchTracker = getActiveTracker();
        if (!mBackGestureStarted || activeTouchTracker == null) {
            // This can happen when an unfinished gesture has been reset in resetTouchTracker
            ProtoLog.d(WM_SHELL_BACK_PREVIEW,
                    "onGestureFinished called while no gesture is started");
            return;
        }
        boolean triggerBack = activeTouchTracker.getTriggerBack();
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", triggerBack);

        if (triggerBack) {
            mBackTransitionObserver.update(mBackNavigationInfo != null
                            ? mBackNavigationInfo.getFocusedTaskId()
                            : INVALID_TASK_ID);
        }
        final boolean hasRequestAnimation = mThresholdCrossed;
        // Reset gesture states.
        mThresholdCrossed = false;
        mPointersPilfered = false;
        mBackAnimationTriggered = false;
        mBackGestureStarted = false;
        activeTouchTracker.setState(BackTouchTracker.TouchTrackerState.FINISHED);
        mTransitionIdleRunner.mRequestCount = 0;

        if (mPostCommitAnimationInProgress) {
            ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Animation is still running");
            return;
        }

        if (mBackNavigationInfo == null) {
            // No focus window found or core are running recents animation, inject back key as
            // legacy behavior, or new back gesture was started while previous has not finished yet
            if (!mQueuedTracker.isInInitialState()) {
                ProtoLog.e(WM_SHELL_BACK_PREVIEW, "mBackNavigationInfo is null AND there is "
                        + "another back animation in progress");
            }
            mCurrentTracker.reset();
            if (triggerBack) {
                injectBackKey(mBackAnimationAdapter.mOriginDisplayId);
            }
            finishBackNavigation(triggerBack);
            return;
        }

        final int backType = mBackNavigationInfo.getType();
        // Simply trigger and finish back navigation when no animator defined.
        if (!shouldDispatchToAnimator()
                || (!hasRequestAnimation && predictiveBackDelayWmTransition())
                || mShellBackAnimationRegistry.isAnimationCancelledOrNull(backType)) {
            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Trigger back without dispatching to animator.");
            invokeOrCancelBack(mCurrentTracker);
            mCurrentTracker.reset();
            return;
        } else if (mShellBackAnimationRegistry.isWaitingAnimation(backType)) {
            ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Gesture released, but animation didn't ready.");
            // Supposed it is in post commit animation state, and start the timeout to watch
            // if the animation is ready.
            mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, mMaxAnimationDuration);
            return;
        }
        startPostCommitAnimation();
    }

    /**
     * Start the phase 2 animation when gesture is released.
     * Callback to {@link #onBackAnimationFinished} when it is finished or timeout.
     */
    private void startPostCommitAnimation() {
        if (mPostCommitAnimationInProgress) {
            return;
        }

        mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startPostCommitAnimation()");
        mPostCommitAnimationInProgress = true;
        mShellExecutor.executeDelayed(mAnimationTimeoutRunnable, mMaxAnimationDuration);

        // The next callback should be {@link #onBackAnimationFinished}.
        if (mCurrentTracker.getTriggerBack()) {
            // notify core gesture is commit
            if (shouldTriggerCloseTransition()) {
                mBackTransitionHandler.mCloseTransitionRequested = true;
                final IOnBackInvokedCallback callback =
                        mBackNavigationInfo.getOnBackInvokedCallback();
                // invoked client side onBackInvoked
                dispatchOnBackInvoked(callback);
                mRealCallbackInvoked = true;
            }
            // start post animation
            dispatchOnBackInvoked(mActiveCallback);
        } else {
            tryDispatchOnBackCancelled(mActiveCallback);
        }
    }

    // Close window won't create any transition
    private boolean shouldTriggerCloseTransition() {
        if (mBackNavigationInfo == null) {
            return false;
        }
        int type = mBackNavigationInfo.getType();
        return type == BackNavigationInfo.TYPE_RETURN_TO_HOME
                || type == BackNavigationInfo.TYPE_CROSS_TASK
                || type == BackNavigationInfo.TYPE_CROSS_ACTIVITY;
    }
    /**
     * Called when the post commit animation is completed or timeout.
     * This will trigger the real {@link IOnBackInvokedCallback} behavior.
     */
    @VisibleForTesting
    void onBackAnimationFinished() {
        if (!mPostCommitAnimationInProgress) {
            // This can happen when a post-commit cancel animation was interrupted by a new back
            // gesture but the timing of interruption was bad such that the back-callback
            // implementation finished in between the time of the new gesture having started and
            // the time of the back-callback receiving the new onBackStarted event. Due to the
            // asynchronous APIs this isn't an unlikely case. To handle this, let's return early.
            // The back-callback implementation will call onBackAnimationFinished again when it is
            // done with animating the second gesture.
            return;
        }
        finishBackAnimation();
    }

    private void finishBackAnimation() {
        // Stop timeout runner.
        mShellExecutor.removeCallbacks(mAnimationTimeoutRunnable);
        mPostCommitAnimationInProgress = false;

        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: onBackAnimationFinished()");

        if (mCurrentTracker.isActive() || mCurrentTracker.isFinished()) {
            // Trigger the real back.
            invokeOrCancelBack(mCurrentTracker);
        } else {
            ProtoLog.d(WM_SHELL_BACK_PREVIEW,
                    "mCurrentBackGestureInfo was null when back animation finished");
        }
        resetTouchTracker();
        mBackTransitionHandler.onAnimationFinished();
    }

    /**
     * Resets the BackTouchTracker and potentially starts a new back navigation in case one
     * is queued.
     */
    private void resetTouchTracker() {
        BackTouchTracker temp = mCurrentTracker;
        mCurrentTracker = mQueuedTracker;
        temp.reset();
        mQueuedTracker = temp;

        if (mCurrentTracker.isInInitialState()) {
            if (mBackGestureStarted) {
                mBackGestureStarted = false;
                tryDispatchOnBackCancelled(mActiveCallback);
                finishBackNavigation(false);
                ProtoLog.d(WM_SHELL_BACK_PREVIEW,
                        "resetTouchTracker -> reset an unfinished gesture");
            } else {
                ProtoLog.d(WM_SHELL_BACK_PREVIEW, "resetTouchTracker -> no queued gesture");
            }
            return;
        }

        if (mCurrentTracker.isFinished() && mCurrentTracker.getTriggerBack()) {
            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "resetTouchTracker -> start queued back navigation "
                    + "AND post commit animation");
            injectBackKey(mBackAnimationAdapter.mOriginDisplayId);
            finishBackNavigation(true);
            mCurrentTracker.reset();
        } else if (!mCurrentTracker.isFinished()) {
            ProtoLog.d(WM_SHELL_BACK_PREVIEW,
                    "resetTouchTracker -> queued gesture not finished; do nothing");
        } else {
            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "resetTouchTracker -> reset queued gesture");
            mCurrentTracker.reset();
        }
    }

    /**
     * This should be called after the whole back navigation is completed.
     */
    @VisibleForTesting
    void finishBackNavigation(boolean triggerBack) {
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishBackNavigation()");
        mActiveCallback = null;
        mApps = null;
        mOnBackStartDispatched = false;
        mThresholdCrossed = false;
        mPointersPilfered = false;
        mShellBackAnimationRegistry.resetDefaultCrossActivity();
        cancelLatencyTracking();
        mReceivedNullNavigationInfo = false;
        if (mBackNavigationInfo != null) {
            mPreviousNavigationType = mBackNavigationInfo.getType();
            mBackNavigationInfo.onBackNavigationFinished(triggerBack);
            mBackNavigationInfo = null;
            requestTopUi(false, mPreviousNavigationType);
        }
    }

    private void startLatencyTracking() {
        if (mTrackingLatency) {
            cancelLatencyTracking();
        }
        mLatencyTracker.onActionStart(LatencyTracker.ACTION_BACK_SYSTEM_ANIMATION);
        mTrackingLatency = true;
    }

    private void cancelLatencyTracking() {
        if (!mTrackingLatency) {
            return;
        }
        mLatencyTracker.onActionCancel(LatencyTracker.ACTION_BACK_SYSTEM_ANIMATION);
        mTrackingLatency = false;
    }

    private void endLatencyTracking() {
        if (!mTrackingLatency) {
            return;
        }
        mLatencyTracker.onActionEnd(LatencyTracker.ACTION_BACK_SYSTEM_ANIMATION);
        mTrackingLatency = false;
    }

    private void startSystemAnimation() {
        if (mBackNavigationInfo == null) {
            ProtoLog.e(WM_SHELL_BACK_PREVIEW, "Lack of navigation info to start animation.");
            return;
        }
        if (!validateAnimationTargets(mApps)) {
            ProtoLog.w(WM_SHELL_BACK_PREVIEW, "Not starting animation due to mApps being null.");
            return;
        }

        final BackAnimationRunner runner =
                mShellBackAnimationRegistry.getAnimationRunnerAndInit(mBackNavigationInfo);
        if (runner == null) {
            if (mBackAnimationFinishedCallback != null) {
                try {
                    mBackAnimationFinishedCallback.onAnimationFinished(false);
                } catch (RemoteException e) {
                    Log.w(TAG, "Failed call IBackNaviAnimationController", e);
                }
            }
            return;
        }
        mActiveCallback = runner.getCallback();

        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: startAnimation()");

        runner.startAnimation(mApps, /*wallpapers*/ null, /*nonApps*/ null,
                () -> mShellExecutor.execute(this::onBackAnimationFinished));

        if (mApps.length >= 1) {
            BackMotionEvent startEvent = mCurrentTracker.createStartEvent();
            dispatchOnBackStarted(mActiveCallback, startEvent);
            if (predictiveBackStopKeycodeBackForwarding()
                    || startEvent.getSwipeEdge() == EDGE_NONE) {
                // onBackStarted is dispatched here so that WindowOnBackInvokedDispatcher knows
                // about the back navigation and can intercept touch events while it's active. This
                // is used for 3-button-nav predictive back cases. This is also needed, so that any
                // observer callbacks can be invoked
                dispatchOnBackStarted(mBackNavigationInfo.getOnBackInvokedCallback(), startEvent);
            }
        }
    }

    private void requestTopUi(boolean hasTopUi, int backType) {
        if (mRequestTopUiCallback != null && (backType == BackNavigationInfo.TYPE_CROSS_TASK
                || backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY)) {
            mRequestTopUiCallback.requestTopUi(hasTopUi, TAG);
        }
    }

    /**
     * Validate animation targets.
     */
    static boolean validateAnimationTargets(RemoteAnimationTarget[] apps) {
        if (apps == null || apps.length == 0) {
            return false;
        }
        for (int i = apps.length - 1; i >= 0; --i) {
            if (!apps[i].leash.isValid()) {
                return false;
            }
        }
        return true;
    }

    private void kickStartAnimation() {
        startSystemAnimation();

        // Dispatch the first progress after animation start for
        // smoothing the initial animation, instead of waiting for next
        // onMove.
        final BackMotionEvent backFinish = mCurrentTracker
                .createProgressEvent();
        dispatchOnBackProgressed(mActiveCallback, backFinish);
        if (mCurrentTracker.isFinished()) {
            // if the down -> up gesture happened before animation
            // start, we have to trigger the uninterruptible transition
            // to finish the back animation.
            startPostCommitAnimation();
        }
    }

    private void createAdapter() {
        IBackAnimationRunner runner =
                new IBackAnimationRunner.Stub() {
                    @Override
                    public void onAnimationStart(
                            RemoteAnimationTarget[] apps,
                            IBinder token,
                            IBackAnimationFinishedCallback finishedCallback) {
                        mShellExecutor.execute(
                                () -> {
                                    endLatencyTracking();
                                    if (!validateAnimationTargets(apps)) {
                                        Log.e(TAG, "Invalid animation targets!");
                                        return;
                                    }
                                    mBackAnimationFinishedCallback = finishedCallback;
                                    mApps = apps;
                                    // app only visible after transition ready, break for now.
                                    if (token != null) {
                                        return;
                                    }
                                    kickStartAnimation();
                                });
                    }

                    @Override
                    public void onAnimationCancelled() {
                        mShellExecutor.execute(
                                () -> {
                                    if (!mShellBackAnimationRegistry.cancel(
                                            mBackNavigationInfo != null
                                                    ? mBackNavigationInfo.getType()
                                                    : mPreviousNavigationType)) {
                                        return;
                                    }
                                    if (!mBackGestureStarted) {
                                        invokeOrCancelBack(mCurrentTracker);
                                    }
                                });
                    }
                };
        mBackAnimationAdapter = new BackAnimationAdapter(runner);
    }

    private void registerBackGestureDelegate() {
        mInputManager.registerKeyGestureEventHandler(List.of(KeyGestureEvent.KEY_GESTURE_TYPE_BACK),
                (event, focussedToken) -> {
                    if (event.getKeyGestureType() == KeyGestureEvent.KEY_GESTURE_TYPE_BACK) {
                        mShellExecutor.execute(() -> {
                            if (mBackGestureStarted) {
                                Log.w(TAG, "Back gesture is running, ignore request");
                                return;
                            }
                            onMotionEvent(0, 0, KeyEvent.ACTION_DOWN, EDGE_NONE, INVALID_DISPLAY);
                            setTriggerBack(true);
                            onMotionEvent(0, 0, KeyEvent.ACTION_UP, EDGE_NONE, INVALID_DISPLAY);
                        });
                    } else {
                        Log.w(TAG, "Unsupported gesture " + event + " received!");
                    }
                });
    }

    /**
     * Description of current BackAnimationController state.
     */
    private void dump(PrintWriter pw, String prefix) {
        pw.println(prefix + "BackAnimationController state:");
        pw.println(prefix + "  mBackGestureStarted=" + mBackGestureStarted);
        pw.println(prefix + "  mPostCommitAnimationInProgress=" + mPostCommitAnimationInProgress);
        pw.println(prefix + "  mShouldStartOnNextMoveEvent=" + mShouldStartOnNextMoveEvent);
        pw.println(prefix + "  mPointerPilfered=" + mThresholdCrossed);
        pw.println(prefix + "  mRequirePointerPilfer=" + mRequirePointerPilfer);
        pw.println(prefix + "  mCurrentTracker state:");
        mCurrentTracker.dump(pw, prefix + "    ");
        pw.println(prefix + "  mQueuedTracker state:");
        mQueuedTracker.dump(pw, prefix + "    ");
    }

    class BackTransitionHandler implements Transitions.TransitionHandler {
        private final Transitions mTransitions;

        Runnable mOnAnimationFinishCallback;
        boolean mCloseTransitionRequested;
        SurfaceControl.Transaction mFinishOpenTransaction;
        Transitions.TransitionFinishCallback mFinishOpenTransitionCallback;
        // The Transition to make behindActivity become visible
        IBinder mPrepareOpenTransition;
        // The Transition to make behindActivity become invisible, if prepare open exist and
        // animation is canceled, start a close prepare transition to finish the whole transition.
        IBinder mClosePrepareTransition;
        TransitionInfo mOpenTransitionInfo;
        Transitions.TransitionHandler mTakeoverHandler;

        BackTransitionHandler(Transitions transitions) {
            mTransitions = transitions;
        }

        void onAnimationFinished() {
            if (!mCloseTransitionRequested && mPrepareOpenTransition != null) {
                createClosePrepareTransition();
            }
            if (mOnAnimationFinishCallback != null) {
                mOnAnimationFinishCallback.run();
                mOnAnimationFinishCallback = null;
            }
        }

        private void applyFinishOpenTransition() {
            if (mFinishOpenTransaction != null) {
                final SurfaceControl.Transaction t = mFinishOpenTransaction;
                t.apply();
            }
            if (mFinishOpenTransitionCallback != null) {
                final Transitions.TransitionFinishCallback callback = mFinishOpenTransitionCallback;
                callback.onTransitionFinished(null);
            }
            cleanUpInternalState();
        }

        private void cleanUpInternalState() {
            mOpenTransitionInfo = null;
            mPrepareOpenTransition = null;
            mFinishOpenTransaction = null;
            mFinishOpenTransitionCallback = null;
            mTakeoverHandler = null;
        }

        private void applyAndFinish(@NonNull SurfaceControl.Transaction st,
                @NonNull SurfaceControl.Transaction ft,
                @NonNull Transitions.TransitionFinishCallback finishCallback) {
            applyFinishOpenTransition();
            st.apply();
            ft.apply();
            finishCallback.onTransitionFinished(null);
            mCloseTransitionRequested = false;
        }

        @Override
        public boolean startAnimation(@NonNull IBinder transition,
                @NonNull TransitionInfo info,
                @NonNull SurfaceControl.Transaction st,
                @NonNull SurfaceControl.Transaction ft,
                @NonNull Transitions.TransitionFinishCallback finishCallback) {
            // Both mShellExecutor and Transitions#mMainExecutor are ShellMainThread, so we don't
            // need to post to ShellExecutor when called.
            if (info.getType() == WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) {
                // only consume it if this transition hasn't being processed.
                if (mClosePrepareTransition != null) {
                    mClosePrepareTransition = null;
                    applyAndFinish(st, ft, finishCallback);
                    return true;
                }
                return false;
            }

            if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION
                    && isNotGestureBackTransition(info)) {
                return false;
            }

            if (shouldCancelAnimation(info)) {
                mPrepareOpenTransition = null;
                return false;
            }

            if (mApps == null || mApps.length == 0) {
                if (mCloseTransitionRequested) {
                    // animation never start, consume directly
                    applyAndFinish(st, ft, finishCallback);
                    return true;
                } else if (mClosePrepareTransition == null
                        && info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) {
                    // Gesture animation was cancelled before prepare transition ready, create
                    // the close prepare transition
                    createClosePrepareTransition();
                }
            }

            if (handlePrepareTransition(transition, info, st, ft, finishCallback)) {
                mTakeoverHandler = mTransitions.getHandlerForTakeover(transition, info);
                kickStartAnimation();
                return true;
            }
            return handleCloseTransition(info, st, ft, finishCallback);
        }

        private boolean canHandOffAnimation() {
            return mTakeoverHandler != null;
        }

        private void handOffAnimation(
                RemoteAnimationTarget[] targets, WindowAnimationState[] states) {
            if (mTakeoverHandler == null) {
                ProtoLog.e(WM_SHELL_BACK_PREVIEW,
                        "Missing takeover handler when trying to hand off animation.");
                return;
            } else if (targets.length != states.length) {
                ProtoLog.e(WM_SHELL_BACK_PREVIEW,
                        "Targets passed for takeover don't match the window states.");
                return;
            }

            // The states passed to this method are paired with the targets, but they need to be
            // paired with the changes inside the TransitionInfo. So for each change we find its
            // matching target, and leave the state for any change missing a matching target blank.
            WindowAnimationState[] updatedStates =
                    new WindowAnimationState[mOpenTransitionInfo.getChanges().size()];
            for (int i = 0; i < mOpenTransitionInfo.getChanges().size(); i++) {
                ActivityManager.RunningTaskInfo taskInfo =
                        mOpenTransitionInfo.getChanges().get(i).getTaskInfo();
                if (taskInfo == null) {
                    continue;
                }

                for (int j = 0; j < targets.length; j++) {
                    if (taskInfo.taskId == targets[j].taskId) {
                        updatedStates[i] = states[j];
                        break;
                    }
                }
            }

            mTakeoverHandler.takeOverAnimation(
                    mPrepareOpenTransition, mOpenTransitionInfo, new SurfaceControl.Transaction(),
                    mFinishOpenTransitionCallback, updatedStates);

            cleanUpInternalState();
        }

        @Override
        public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
                @Nullable SurfaceControl.Transaction finishTransaction) {
            if (transition == mClosePrepareTransition && aborted) {
                mClosePrepareTransition = null;
                applyFinishOpenTransition();
            } else if (!aborted) {
                // Since the closing target participates in the predictive back transition, the
                // merged transition must be applied with the first transition to ensure a seamless
                // animation.
                if (mFinishOpenTransaction != null && finishTransaction != null) {
                    mFinishOpenTransaction.merge(finishTransaction);
                }
            }
        }

        void createClosePrepareTransition() {
            if (mClosePrepareTransition != null) {
                Log.e(TAG, "Re-create close prepare transition");
                return;
            }
            final WindowContainerTransaction wct = new WindowContainerTransaction();
            wct.restoreBackNavi();
            mClosePrepareTransition = mTransitions.startTransition(
                    TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION, wct, mBackTransitionHandler);
        }
        private void mergePendingTransitions(TransitionInfo info) {
            if (mOpenTransitionInfo == null) {
                return;
            }
            // Copy initial changes to final transition
            final TransitionInfo init = mOpenTransitionInfo;
            // Find prepare open target
            boolean openShowWallpaper = false;
            final ArrayList<SurfaceControl> openSurfaces = new ArrayList<>();
            int tmpSize;
            for (int j = init.getChanges().size() - 1; j >= 0; --j) {
                final TransitionInfo.Change change = init.getChanges().get(j);
                if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)
                        && TransitionUtil.isOpeningMode(change.getMode())) {
                    final ComponentName openComponent = findComponentName(change);
                    final int openTaskId = findTaskId(change);
                    final WindowContainerToken openToken = findToken(change);
                    if (openComponent == null && openTaskId == INVALID_TASK_ID
                            && openToken == null) {
                        continue;
                    }
                    openSurfaces.add(change.getLeash());
                    if (change.hasFlags(FLAG_SHOW_WALLPAPER)) {
                        openShowWallpaper = true;
                    }
                }
            }
            if (openSurfaces.isEmpty()) {
                // This shouldn't happen, but if that happen, consume the initial transition anyway.
                Log.e(TAG, "Unable to merge following transition, cannot find the gesture "
                        + "animated target from the open transition=" + mOpenTransitionInfo);
                mOpenTransitionInfo = null;
                return;
            }
            // Find first non-prepare open target
            boolean isOpen = false;
            tmpSize = info.getChanges().size();
            for (int j = 0; j < tmpSize; ++j) {
                final TransitionInfo.Change change = info.getChanges().get(j);
                if (isOpenSurfaceMatched(openSurfaces, change)) {
                    // This is original close target, potential be close, but cannot determine
                    // from it.
                    if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) {
                        isOpen = !TransitionUtil.isClosingMode(change.getMode());
                    } else {
                        isOpen = TransitionUtil.isOpeningMode(change.getMode());
                        break;
                    }
                }
            }
            if (!isOpen) {
                // Close transition, the transition info should be:
                // init info(open A & wallpaper) => init info(open A & change B & wallpaper)
                // current info(close B target) => current info(change A & close B)
                // remove init info(open/change A target & wallpaper)
                boolean moveToTop = false;
                boolean excludeOpenTarget = false;
                boolean mergePredictive = false;
                for (int j = info.getChanges().size() - 1; j >= 0; --j) {
                    final TransitionInfo.Change change = info.getChanges().get(j);
                    if (isOpenSurfaceMatched(openSurfaces, change)) {
                        if (TransitionUtil.isClosingMode(change.getMode())) {
                            excludeOpenTarget = true;
                        }
                        moveToTop = change.hasFlags(FLAG_MOVED_TO_TOP);
                        info.getChanges().remove(j);
                    } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER))) {
                        info.getChanges().remove(j);
                    } else if (!mergePredictive && TransitionUtil.isClosingMode(change.getMode())) {
                        mergePredictive = true;
                    }
                }
                // Ignore merge if there is no close target
                if (!info.getChanges().isEmpty() && mergePredictive) {
                    tmpSize = init.getChanges().size();
                    for (int i = 0; i < tmpSize; ++i) {
                        final TransitionInfo.Change change = init.getChanges().get(i);
                        if (change.hasFlags(FLAG_IS_WALLPAPER)) {
                            continue;
                        }
                        if (isOpenSurfaceMatched(openSurfaces, change)) {
                            if (excludeOpenTarget) {
                                // App has triggered another change during predictive back
                                // transition, filter out predictive back target.
                                continue;
                            }
                            if (moveToTop) {
                                change.setFlags(change.getFlags() | FLAG_MOVED_TO_TOP);
                            }
                        } else if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)
                                && change.getMode() == TRANSIT_CHANGE
                                && isCloseChangeExist(info, change)) {
                            // This is the original top target, don't add it into current transition
                            // if it is closing.
                            continue;
                        }
                        info.getChanges().add(i, change);
                    }
                }
            } else {
                // Open transition, the transition info should be:
                // init info(open A & wallpaper)
                // current info(open C target + close B target + close A & wallpaper)

                // If close target isn't back navigated, filter out close A & wallpaper because the
                // (open C + close B) pair didn't participant prepare close
                boolean nonBackOpen = false;
                boolean nonBackClose = false;
                tmpSize = info.getChanges().size();
                for (int j = 0; j < tmpSize; ++j) {
                    final TransitionInfo.Change change = info.getChanges().get(j);
                    if (!change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)
                            && canBeTransitionTarget(change)) {
                        final int mode = change.getMode();
                        nonBackOpen |= TransitionUtil.isOpeningMode(mode);
                        nonBackClose |= TransitionUtil.isClosingMode(mode);
                    }
                }
                if (nonBackClose && nonBackOpen) {
                    for (int j = info.getChanges().size() - 1; j >= 0; --j) {
                        final TransitionInfo.Change change = info.getChanges().get(j);
                        if (isOpenSurfaceMatched(openSurfaces, change)) {
                            info.getChanges().remove(j);
                        } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER))) {
                            info.getChanges().remove(j);
                        }
                    }
                }
            }
            ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation transition, merge pending "
                    + "transitions result=%s", info);
            // Only handle one merge transition request.
            mOpenTransitionInfo = null;
        }

        @Override
        public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
                @NonNull SurfaceControl.Transaction startT,
                @NonNull SurfaceControl.Transaction finishT,
                @NonNull IBinder mergeTarget,
                @NonNull Transitions.TransitionFinishCallback finishCallback) {
            if (mClosePrepareTransition == transition) {
                mClosePrepareTransition = null;
            }
            // try to handle unexpected transition
            if (mOpenTransitionInfo != null) {
                mergePendingTransitions(info);
            }

            if (info.getType() == TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION
                    && !mCloseTransitionRequested && info.getChanges().isEmpty() && mApps == null) {
                finishCallback.onTransitionFinished(null);
                startT.apply();
                applyFinishOpenTransition();
                return;
            }
            if (isNotGestureBackTransition(info) || shouldCancelAnimation(info)
                    || !mCloseTransitionRequested) {
                if (mPrepareOpenTransition != null) {
                    applyFinishOpenTransition();
                }
                return;
            }
            // Handle the commit transition if this handler is running the open transition.
            finishCallback.onTransitionFinished(null);
            startT.apply();
            if (mCloseTransitionRequested) {
                if (mApps == null || mApps.length == 0) {
                    // animation was done
                    applyFinishOpenTransition();
                    mCloseTransitionRequested = false;
                } else {
                    // we are animating, wait until animation finish
                    mOnAnimationFinishCallback = () -> {
                        applyFinishOpenTransition();
                        mCloseTransitionRequested = false;
                    };
                }
            }
        }

        // Cancel close animation if something happen unexpected, let another handler to handle
        private boolean shouldCancelAnimation(@NonNull TransitionInfo info) {
            final boolean noCloseAllowed = !mCloseTransitionRequested
                    && info.getType() == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION;
            boolean unableToHandle = false;
            boolean filterTargets = false;
            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                final TransitionInfo.Change c = info.getChanges().get(i);
                final boolean backGestureAnimated = c.hasFlags(FLAG_BACK_GESTURE_ANIMATED);
                if (!backGestureAnimated && !c.hasFlags(FLAG_IS_WALLPAPER)) {
                    // something we cannot handle?
                    unableToHandle = true;
                    filterTargets = true;
                } else if (noCloseAllowed && backGestureAnimated
                        && TransitionUtil.isClosingMode(c.getMode())) {
                    // Prepare back navigation shouldn't contain close change, unless top app
                    // request close.
                    unableToHandle = true;
                }
            }
            if (!unableToHandle) {
                return false;
            }
            if (!filterTargets) {
                return true;
            }
            if (TransitionUtil.isOpeningType(info.getType())
                    || TransitionUtil.isClosingType(info.getType())) {
                boolean removeWallpaper = false;
                for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                    final TransitionInfo.Change c = info.getChanges().get(i);
                    // filter out opening target, keep original closing target in this transition
                    if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED)
                            && TransitionUtil.isOpeningMode(c.getMode())) {
                        info.getChanges().remove(i);
                        removeWallpaper |= c.hasFlags(FLAG_SHOW_WALLPAPER);
                    }
                }
                if (removeWallpaper) {
                    for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                        final TransitionInfo.Change c = info.getChanges().get(i);
                        if (c.hasFlags(FLAG_IS_WALLPAPER)) {
                            info.getChanges().remove(i);
                        }
                    }
                }
            }
            return true;
        }

        /**
         * Check whether this transition is prepare for predictive back animation, which could
         * happen when core make an activity become visible.
         */
        @VisibleForTesting
        boolean handlePrepareTransition(@NonNull IBinder transition,
                @NonNull TransitionInfo info,
                @NonNull SurfaceControl.Transaction st,
                @NonNull SurfaceControl.Transaction ft,
                @NonNull Transitions.TransitionFinishCallback finishCallback) {
            if (info.getType() != WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) {
                return false;
            }
            // Must have open target, must not have close target.
            if (hasAnimationInMode(info, TransitionUtil::isClosingMode)
                    || !hasAnimationInMode(info, TransitionUtil::isOpeningMode)) {
                return false;
            }
            SurfaceControl openingLeash = null;
            SurfaceControl closingLeash = null;
            if (mApps != null) {
                for (int i = mApps.length - 1; i >= 0; --i) {
                    if (mApps[i].mode == MODE_OPENING) {
                        openingLeash = mApps[i].leash;
                    } else if (mApps[i].mode == MODE_CLOSING) {
                        closingLeash = mApps[i].leash;
                    }
                }
            }
            if (openingLeash != null && closingLeash != null) {
                int rootIdx = -1;
                for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                    final TransitionInfo.Change c = info.getChanges().get(i);
                    if (TransitionUtil.isOpeningMode(c.getMode())) {
                        final Point offset = c.getEndRelOffset();
                        st.setPosition(c.getLeash(), offset.x, offset.y);
                        st.reparent(c.getLeash(), openingLeash);
                        st.setAlpha(c.getLeash(), 1.0f);
                        rootIdx = TransitionUtil.rootIndexFor(c, info);
                    } else if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED)
                            && c.getMode() == TRANSIT_CHANGE) {
                        st.reparent(c.getLeash(), closingLeash);
                    }
                }
                // The root leash and the leash of opening target should actually in the same level,
                // but since the root leash is created after opening target, it will have higher
                // layer in surface flinger. Move the root leash to lower level, so it won't affect
                // the playing animation.
                if (rootIdx >= 0 && info.getRootCount() > 0) {
                    st.setLayer(info.getRoot(rootIdx).getLeash(), -1);
                }
            }
            st.apply();
            // In case other transition handler took the handleRequest before this class.
            mPrepareOpenTransition = transition;
            mFinishOpenTransaction = ft;
            mFinishOpenTransitionCallback = finishCallback;
            mOpenTransitionInfo = info;
            return true;
        }

        /**
         * Check whether this transition is triggered from back gesture commitment.
         * Reparent the transition targets to animation leashes, so the animation won't be broken.
         */
        @VisibleForTesting
        boolean handleCloseTransition(@NonNull TransitionInfo info,
                @NonNull SurfaceControl.Transaction st,
                @NonNull SurfaceControl.Transaction ft,
                @NonNull Transitions.TransitionFinishCallback finishCallback) {
            if (!mCloseTransitionRequested) {
                return false;
            }
            // must have close target
            if (!hasAnimationInMode(info, TransitionUtil::isClosingMode)) {
                return false;
            }
            if (mApps == null) {
                // animation is done
                applyAndFinish(st, ft, finishCallback);
                return true;
            }
            SurfaceControl openingLeash = null;
            SurfaceControl closingLeash = null;
            for (int i = mApps.length - 1; i >= 0; --i) {
                if (mApps[i].mode == MODE_OPENING) {
                    openingLeash = mApps[i].leash;
                }
                if (mApps[i].mode == MODE_CLOSING) {
                    closingLeash = mApps[i].leash;
                }
            }
            if (openingLeash != null && closingLeash != null) {
                for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                    final TransitionInfo.Change c = info.getChanges().get(i);
                    if (c.hasFlags(FLAG_IS_WALLPAPER)) {
                        st.setAlpha(c.getLeash(), 1.0f);
                        continue;
                    }
                    if (TransitionUtil.isOpeningMode(c.getMode())) {
                        final Point offset = c.getEndRelOffset();
                        st.setPosition(c.getLeash(), offset.x, offset.y);
                        st.reparent(c.getLeash(), openingLeash);
                        st.setAlpha(c.getLeash(), 1.0f);
                    } else if (TransitionUtil.isClosingMode(c.getMode())) {
                        st.reparent(c.getLeash(), closingLeash);
                    }
                }
            }
            st.apply();
            // mApps must exists
            mOnAnimationFinishCallback = () -> {
                ft.apply();
                finishCallback.onTransitionFinished(null);
                mCloseTransitionRequested = false;
            };
            return true;
        }

        @Nullable
        @Override
        public WindowContainerTransaction handleRequest(
                @NonNull IBinder transition,
                @NonNull TransitionRequestInfo request) {
            final int type = request.getType();
            if (type == WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION) {
                mPrepareOpenTransition = transition;
                return new WindowContainerTransaction();
            }
            if (type == WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION) {
                return new WindowContainerTransaction();
            }
            if (TransitionUtil.isClosingType(request.getType()) && mCloseTransitionRequested) {
                return new WindowContainerTransaction();
            }
            return null;
        }
    }

    private static boolean isNotGestureBackTransition(@NonNull TransitionInfo info) {
        return !hasAnimationInMode(info, TransitionUtil::isOpenOrCloseMode);
    }

    private static boolean hasAnimationInMode(@NonNull TransitionInfo info,
            Predicate<Integer> mode) {
        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
            final TransitionInfo.Change c = info.getChanges().get(i);
            if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) && mode.test(c.getMode())) {
                return true;
            }
        }
        return false;
    }

    private static WindowContainerToken findToken(TransitionInfo.Change change) {
        return change.getContainer();
    }

    private static ComponentName findComponentName(TransitionInfo.Change change) {
        final ComponentName componentName = change.getActivityComponent();
        if (componentName != null) {
            return componentName;
        }
        final TaskInfo taskInfo = change.getTaskInfo();
        if (taskInfo != null) {
            return taskInfo.topActivity;
        }
        return null;
    }

    private static int findTaskId(TransitionInfo.Change change) {
        final TaskInfo taskInfo = change.getTaskInfo();
        if (taskInfo != null) {
            return taskInfo.taskId;
        }
        return INVALID_TASK_ID;
    }

    static boolean isOpenSurfaceMatched(@NonNull ArrayList<SurfaceControl> openSurfaces,
            TransitionInfo.Change change) {
        for (int i = openSurfaces.size() - 1; i >= 0; --i) {
            if (openSurfaces.get(i).isSameSurface(change.getLeash())) {
                return true;
            }
        }
        return false;
    }

    private static boolean canBeTransitionTarget(TransitionInfo.Change change) {
        return findComponentName(change) != null || findTaskId(change) != INVALID_TASK_ID;
    }

    private static boolean isCloseChangeExist(TransitionInfo info, TransitionInfo.Change change) {
        for (int j = info.getChanges().size() - 1; j >= 0; --j) {
            final TransitionInfo.Change current = info.getChanges().get(j);
            if (TransitionUtil.isClosingMode(current.getMode())
                    && change.getLeash().isSameSurface(current.getLeash())) {
                return true;
            }
        }
        return false;
    }

    // Record the latest back gesture happen on which task.
    static class BackTransitionObserver implements Transitions.TransitionObserver {
        int mFocusedTaskId = INVALID_TASK_ID;
        IBinder mFocusTaskMonitorToken;
        private BackTransitionHandler mBackTransitionHandler;
        void setBackTransitionHandler(BackTransitionHandler handler) {
            mBackTransitionHandler = handler;
        }

        void update(int focusedTaskId) {
            mFocusedTaskId = focusedTaskId;
        }

        @Override
        public void onTransitionReady(@NonNull IBinder transition, @NonNull TransitionInfo info,
                @NonNull SurfaceControl.Transaction startTransaction,
                @NonNull SurfaceControl.Transaction finishTransaction) {
            if (mFocusedTaskId == INVALID_TASK_ID) {
                return;
            }
            for (int i = info.getChanges().size() - 1; i >= 0; --i) {
                final TransitionInfo.Change c = info.getChanges().get(i);
                if (c.getTaskInfo() != null && c.getTaskInfo().taskId == mFocusedTaskId) {
                    mFocusTaskMonitorToken = transition;
                    break;
                }
            }
            // Transition happen but the task isn't involved, reset.
            if (mFocusTaskMonitorToken == null) {
                mFocusedTaskId = INVALID_TASK_ID;
            }
        }

        @Override
        public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
            if (mFocusTaskMonitorToken == merged) {
                mFocusTaskMonitorToken = playing;
            }
            if (mBackTransitionHandler.mClosePrepareTransition == merged) {
                mBackTransitionHandler.mClosePrepareTransition = null;
            }
        }

        @Override
        public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {
            if (mFocusTaskMonitorToken == transition) {
                mFocusedTaskId = INVALID_TASK_ID;
            }
            if (mBackTransitionHandler.mClosePrepareTransition == transition) {
                mBackTransitionHandler.mClosePrepareTransition = null;
            }
        }
    }
}
