/*
 * 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.view.RemoteAnimationTarget.MODE_CLOSING;
import static android.view.RemoteAnimationTarget.MODE_OPENING;

import static com.android.wm.shell.common.ExecutorUtils.executeRemoteCallWithTaskPermission;
import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
import static com.android.wm.shell.sysui.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityTaskManager;
import android.app.IActivityTaskManager;
import android.app.WindowConfiguration;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.hardware.HardwareBuffer;
import android.hardware.input.InputManager;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings.Global;
import android.util.Log;
import android.view.IWindowFocusObserver;
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.window.BackAnimationAdaptor;
import android.window.BackEvent;
import android.window.BackMotionEvent;
import android.window.BackNavigationInfo;
import android.window.IBackAnimationRunner;
import android.window.IBackNaviAnimationController;
import android.window.IOnBackInvokedCallback;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.protolog.common.ProtoLog;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.annotations.ShellBackgroundThread;
import com.android.wm.shell.common.annotations.ShellMainThread;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Controls the window animation run when a user initiates a back gesture.
 */
public class BackAnimationController implements RemoteCallable<BackAnimationController> {
    private static final String TAG = "BackAnimationController";
    private static final int SETTING_VALUE_OFF = 0;
    private static final int SETTING_VALUE_ON = 1;
    public static final boolean IS_ENABLED =
            SystemProperties.getInt("persist.wm.debug.predictive_back",
                    SETTING_VALUE_ON) == SETTING_VALUE_ON;
    /** Flag for U animation features */
    public static boolean IS_U_ANIMATION_ENABLED =
            SystemProperties.getInt("persist.wm.debug.predictive_back_anim",
                    SETTING_VALUE_ON) == SETTING_VALUE_ON;
    /** Predictive back animation developer option */
    private final AtomicBoolean mEnableAnimations = new AtomicBoolean(false);
    // TODO (b/241808055) Find a appropriate time to remove during refactor
    private static final boolean USE_TRANSITION =
            SystemProperties.getInt("persist.wm.debug.predictive_back_ani_trans", 1) != 0;
    /**
     * Max duration to wait for a transition to finish before accepting another gesture start
     * request.
     */
    private static final long MAX_TRANSITION_DURATION = 2000;

    /** True when a back gesture is ongoing */
    private boolean mBackGestureStarted = false;

    /** Tracks if an uninterruptible transition is in progress */
    private boolean mTransitionInProgress = false;
    /** Tracks if we should start the back gesture on the next motion move event */
    private boolean mShouldStartOnNextMoveEvent = false;
    /** @see #setTriggerBack(boolean) */
    private boolean mTriggerBack;

    @Nullable
    private BackNavigationInfo mBackNavigationInfo;
    private final SurfaceControl.Transaction mTransaction;
    private final IActivityTaskManager mActivityTaskManager;
    private final Context mContext;
    private final ContentResolver mContentResolver;
    private final ShellController mShellController;
    private final ShellExecutor mShellExecutor;
    private final Handler mBgHandler;
    @Nullable
    private IOnBackInvokedCallback mBackToLauncherCallback;
    private float mTriggerThreshold;
    private final Runnable mResetTransitionRunnable = () -> {
        finishAnimation();
        mTransitionInProgress = false;
    };

    private RemoteAnimationTarget mAnimationTarget;
    IBackAnimationRunner mIBackAnimationRunner;
    private IBackNaviAnimationController mBackAnimationController;
    private BackAnimationAdaptor mBackAnimationAdaptor;

    private final TouchTracker mTouchTracker = new TouchTracker();
    private final CachingBackDispatcher mCachingBackDispatcher = new CachingBackDispatcher();

    @VisibleForTesting
    final IWindowFocusObserver mFocusObserver = new IWindowFocusObserver.Stub() {
        @Override
        public void focusGained(IBinder inputToken) { }
        @Override
        public void focusLost(IBinder inputToken) {
            mShellExecutor.execute(() -> {
                if (!mBackGestureStarted || mTransitionInProgress) {
                    // If an uninterruptible transition is already in progress, we should ignore
                    // this due to the transition may cause focus lost. (alpha = 0)
                    return;
                }
                setTriggerBack(false);
                onGestureFinished(false);
            });
        }
    };

    /**
     * Cache the temporary callback and trigger result if gesture was finish before received
     * BackAnimationRunner#onAnimationStart/cancel, so there can continue play the animation.
     */
    private class CachingBackDispatcher {
        private IOnBackInvokedCallback mOnBackCallback;
        private boolean mTriggerBack;
        // Whether we are waiting to receive onAnimationStart
        private boolean mWaitingAnimation;

        void startWaitingAnimation() {
            mWaitingAnimation = true;
        }

        boolean set(IOnBackInvokedCallback callback, boolean triggerBack) {
            if (mWaitingAnimation) {
                mOnBackCallback = callback;
                mTriggerBack = triggerBack;
                return true;
            }
            return false;
        }

        boolean consume() {
            boolean consumed = false;
            if (mWaitingAnimation && mOnBackCallback != null) {
                if (mTriggerBack) {
                    final BackMotionEvent backFinish = mTouchTracker.createProgressEvent(1);
                    dispatchOnBackProgressed(mBackToLauncherCallback, backFinish);
                    dispatchOnBackInvoked(mOnBackCallback);
                } else {
                    final BackMotionEvent backFinish = mTouchTracker.createProgressEvent(0);
                    dispatchOnBackProgressed(mBackToLauncherCallback, backFinish);
                    dispatchOnBackCancelled(mOnBackCallback);
                }
                startTransition();
                consumed = true;
            }
            mOnBackCallback = null;
            mWaitingAnimation = false;
            return consumed;
        }
    }

    public BackAnimationController(
            @NonNull ShellInit shellInit,
            @NonNull ShellController shellController,
            @NonNull @ShellMainThread ShellExecutor shellExecutor,
            @NonNull @ShellBackgroundThread Handler backgroundHandler,
            Context context) {
        this(shellInit, shellController, shellExecutor, backgroundHandler,
                new SurfaceControl.Transaction(), ActivityTaskManager.getService(),
                context, context.getContentResolver());
    }

    @VisibleForTesting
    BackAnimationController(
            @NonNull ShellInit shellInit,
            @NonNull ShellController shellController,
            @NonNull @ShellMainThread ShellExecutor shellExecutor,
            @NonNull @ShellBackgroundThread Handler bgHandler,
            @NonNull SurfaceControl.Transaction transaction,
            @NonNull IActivityTaskManager activityTaskManager,
            Context context, ContentResolver contentResolver) {
        mShellController = shellController;
        mShellExecutor = shellExecutor;
        mTransaction = transaction;
        mActivityTaskManager = activityTaskManager;
        mContext = context;
        mContentResolver = contentResolver;
        mBgHandler = bgHandler;
        shellInit.addInitCallback(this::onInit, this);
    }

    @VisibleForTesting
    void setEnableUAnimation(boolean enable) {
        IS_U_ANIMATION_ENABLED = enable;
    }

    private void onInit() {
        setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler);
        mShellController.addExternalInterface(KEY_EXTRA_SHELL_BACK_ANIMATION,
                this::createExternalInterface, this);
    }

    private void setupAnimationDeveloperSettingsObserver(
            @NonNull ContentResolver contentResolver,
            @NonNull @ShellBackgroundThread final Handler backgroundHandler) {
        ContentObserver settingsObserver = new ContentObserver(backgroundHandler) {
            @Override
            public void onChange(boolean selfChange, Uri uri) {
                updateEnableAnimationFromSetting();
            }
        };
        contentResolver.registerContentObserver(
                Global.getUriFor(Global.ENABLE_BACK_ANIMATION),
                false, settingsObserver, UserHandle.USER_SYSTEM
        );
        updateEnableAnimationFromSetting();
    }

    @ShellBackgroundThread
    private void updateEnableAnimationFromSetting() {
        int settingValue = Global.getInt(mContext.getContentResolver(),
                Global.ENABLE_BACK_ANIMATION, SETTING_VALUE_OFF);
        boolean isEnabled = settingValue == SETTING_VALUE_ON;
        mEnableAnimations.set(isEnabled);
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Back animation enabled=%s",
                isEnabled);
    }

    public BackAnimation getBackAnimationImpl() {
        return mBackAnimation;
    }

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

    private final BackAnimationImpl mBackAnimation = new BackAnimationImpl();

    @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) {
            mShellExecutor.execute(() -> onMotionEvent(touchX, touchY, keyAction, swipeEdge));
        }

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

        @Override
        public void setSwipeThresholds(float triggerThreshold, float progressThreshold) {
            mShellExecutor.execute(() -> BackAnimationController.this.setSwipeThresholds(
                    triggerThreshold, progressThreshold));
        }
    }

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

        IBackAnimationImpl(BackAnimationController controller) {
            mController = controller;
        }

        @Override
        public void setBackToLauncherCallback(IOnBackInvokedCallback callback) {
            executeRemoteCallWithTaskPermission(mController, "setBackToLauncherCallback",
                    (controller) -> controller.setBackToLauncherCallback(callback));
        }

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

        @Override
        public void onBackToLauncherAnimationFinished() {
            executeRemoteCallWithTaskPermission(mController, "onBackToLauncherAnimationFinished",
                    (controller) -> controller.onBackToLauncherAnimationFinished());
        }

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

    @VisibleForTesting
    void setBackToLauncherCallback(IOnBackInvokedCallback callback) {
        mBackToLauncherCallback = callback;
        if (USE_TRANSITION) {
            createAdaptor();
        }
    }

    private void clearBackToLauncherCallback() {
        mBackToLauncherCallback = null;
    }

    @VisibleForTesting
    void onBackToLauncherAnimationFinished() {
        final boolean triggerBack = mTriggerBack;
        IOnBackInvokedCallback callback = mBackNavigationInfo != null
                ? mBackNavigationInfo.getOnBackInvokedCallback() : null;
        // Make sure the notification sequence should be controller > client.
        finishAnimation();
        if (callback != null) {
            if (triggerBack) {
                dispatchOnBackInvoked(callback);
            } else {
                dispatchOnBackCancelled(callback);
            }
        }
    }

    /**
     * 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) {
        if (mTransitionInProgress) {
            return;
        }

        mTouchTracker.update(touchX, touchY);
        if (keyAction == MotionEvent.ACTION_DOWN) {
            if (!mBackGestureStarted) {
                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(touchX, touchY, 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) {
                mTriggerBack = false;
            }
            onGestureFinished(true);
        }
    }

    private void onGestureStarted(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "initAnimation mMotionStarted=%b", mBackGestureStarted);
        if (mBackGestureStarted || mBackNavigationInfo != null) {
            Log.e(TAG, "Animation is being initialized but is already started.");
            finishAnimation();
        }

        mTouchTracker.setGestureStartLocation(touchX, touchY, swipeEdge);
        mBackGestureStarted = true;

        try {
            boolean requestAnimation = mEnableAnimations.get();
            mBackNavigationInfo = mActivityTaskManager.startBackNavigation(requestAnimation,
                    mFocusObserver, mBackAnimationAdaptor);
            onBackNavigationInfoReceived(mBackNavigationInfo);
        } catch (RemoteException remoteException) {
            Log.e(TAG, "Failed to initAnimation", remoteException);
            finishAnimation();
        }
    }

    private void onBackNavigationInfoReceived(@Nullable BackNavigationInfo backNavigationInfo) {
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo);
        if (backNavigationInfo == null) {
            Log.e(TAG, "Received BackNavigationInfo is null.");
            return;
        }
        int backType = backNavigationInfo.getType();
        IOnBackInvokedCallback targetCallback = null;
        final boolean dispatchToLauncher = shouldDispatchToLauncher(backType);
        if (backType == BackNavigationInfo.TYPE_CROSS_ACTIVITY) {
            HardwareBuffer hardwareBuffer = backNavigationInfo.getScreenshotHardwareBuffer();
            if (hardwareBuffer != null) {
                displayTargetScreenshot(hardwareBuffer,
                        backNavigationInfo.getTaskWindowConfiguration());
            }
            targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
            mTransaction.apply();
        } else if (dispatchToLauncher) {
            targetCallback = mBackToLauncherCallback;
            if (USE_TRANSITION) {
                mCachingBackDispatcher.startWaitingAnimation();
            }
        } else if (backType == BackNavigationInfo.TYPE_CALLBACK) {
            targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
        }
        if (!USE_TRANSITION || !dispatchToLauncher) {
            dispatchOnBackStarted(
                    targetCallback,
                    mTouchTracker.createStartEvent(
                            mBackNavigationInfo.getDepartingAnimationTarget()));
        }
    }

    /**
     * Display the screenshot of the activity beneath.
     *
     * @param hardwareBuffer The buffer containing the screenshot.
     */
    private void displayTargetScreenshot(@NonNull HardwareBuffer hardwareBuffer,
            WindowConfiguration taskWindowConfiguration) {
        SurfaceControl screenshotSurface =
                mBackNavigationInfo == null ? null : mBackNavigationInfo.getScreenshotSurface();
        if (screenshotSurface == null) {
            Log.e(TAG, "BackNavigationInfo doesn't contain a surface for the screenshot. ");
            return;
        }

        // Scale the buffer to fill the whole Task
        float sx = 1;
        float sy = 1;
        float w = taskWindowConfiguration.getBounds().width();
        float h = taskWindowConfiguration.getBounds().height();

        if (w != hardwareBuffer.getWidth()) {
            sx = w / hardwareBuffer.getWidth();
        }

        if (h != hardwareBuffer.getHeight()) {
            sy = h / hardwareBuffer.getHeight();
        }
        mTransaction.setScale(screenshotSurface, sx, sy);
        mTransaction.setBuffer(screenshotSurface, hardwareBuffer);
        mTransaction.setVisibility(screenshotSurface, true);
    }

    private void onMove(float touchX, float touchY, @BackEvent.SwipeEdge int swipeEdge) {
        if (!mBackGestureStarted || mBackNavigationInfo == null) {
            return;
        }
        final BackMotionEvent backEvent = mTouchTracker.createProgressEvent();
        if (USE_TRANSITION && mBackAnimationController != null && mAnimationTarget != null) {
                dispatchOnBackProgressed(mBackToLauncherCallback, backEvent);
        } else if (mEnableAnimations.get()) {
            int backType = mBackNavigationInfo.getType();
            IOnBackInvokedCallback targetCallback;
            if (shouldDispatchToLauncher(backType)) {
                targetCallback = mBackToLauncherCallback;
            } else {
                targetCallback = mBackNavigationInfo.getOnBackInvokedCallback();
            }
            dispatchOnBackProgressed(targetCallback, backEvent);
        }
    }

    private void injectBackKey() {
        sendBackEvent(KeyEvent.ACTION_DOWN);
        sendBackEvent(KeyEvent.ACTION_UP);
    }

    private void sendBackEvent(int action) {
        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);

        ev.setDisplayId(mContext.getDisplay().getDisplayId());
        if (!InputManager.getInstance()
                .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
            Log.e(TAG, "Inject input event fail");
        }
    }

    private void onGestureFinished(boolean fromTouch) {
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
        if (!mBackGestureStarted) {
            finishAnimation();
            return;
        }

        if (fromTouch) {
            // Let touch reset the flag otherwise it will start a new back navigation and refresh
            // the info when received a new move event.
            mBackGestureStarted = false;
        }

        if (mTransitionInProgress) {
            return;
        }

        if (mBackNavigationInfo == null) {
            // No focus window found or core are running recents animation, inject back key as
            // legacy behavior.
            if (mTriggerBack) {
                injectBackKey();
            }
            finishAnimation();
            return;
        }

        int backType = mBackNavigationInfo.getType();
        boolean shouldDispatchToLauncher = shouldDispatchToLauncher(backType);
        IOnBackInvokedCallback targetCallback = shouldDispatchToLauncher
                ? mBackToLauncherCallback
                : mBackNavigationInfo.getOnBackInvokedCallback();
        if (mCachingBackDispatcher.set(targetCallback, mTriggerBack)) {
            return;
        }
        if (shouldDispatchToLauncher) {
            startTransition();
        }
        if (mTriggerBack) {
            dispatchOnBackInvoked(targetCallback);
        } else {
            dispatchOnBackCancelled(targetCallback);
        }
        if (backType != BackNavigationInfo.TYPE_RETURN_TO_HOME || !shouldDispatchToLauncher) {
            // Launcher callback missing. Simply finish animation.
            finishAnimation();
        }
    }

    private boolean shouldDispatchToLauncher(int backType) {
        return backType == BackNavigationInfo.TYPE_RETURN_TO_HOME
                && mBackToLauncherCallback != null
                && mEnableAnimations.get()
                && mBackNavigationInfo != null
                && ((USE_TRANSITION && mBackNavigationInfo.isPrepareRemoteAnimation())
                || mBackNavigationInfo.getDepartingAnimationTarget() != null);
    }

    private void dispatchOnBackStarted(IOnBackInvokedCallback callback,
            BackMotionEvent backEvent) {
        if (callback == null) {
            return;
        }
        try {
            if (shouldDispatchAnimation(callback)) {
                callback.onBackStarted(backEvent);
            }
        } 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 dispatchOnBackCancelled(IOnBackInvokedCallback callback) {
        if (callback == null) {
            return;
        }
        try {
            if (shouldDispatchAnimation(callback)) {
                callback.onBackCancelled();
            }
        } catch (RemoteException e) {
            Log.e(TAG, "dispatchOnBackCancelled error: ", e);
        }
    }

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

    private boolean shouldDispatchAnimation(IOnBackInvokedCallback callback) {
        return (IS_U_ANIMATION_ENABLED || callback == mBackToLauncherCallback)
                && mEnableAnimations.get();
    }

    /**
     * Sets to true when the back gesture has passed the triggering threshold, false otherwise.
     */
    public void setTriggerBack(boolean triggerBack) {
        if (mTransitionInProgress) {
            return;
        }
        mTriggerBack = triggerBack;
        mTouchTracker.setTriggerBack(triggerBack);
    }

    private void setSwipeThresholds(float triggerThreshold, float progressThreshold) {
        mTouchTracker.setProgressThreshold(progressThreshold);
        mTriggerThreshold = triggerThreshold;
    }

    private void finishAnimation() {
        ProtoLog.d(WM_SHELL_BACK_PREVIEW, "BackAnimationController: finishAnimation()");
        mTouchTracker.reset();
        BackNavigationInfo backNavigationInfo = mBackNavigationInfo;
        boolean triggerBack = mTriggerBack;
        mBackNavigationInfo = null;
        mAnimationTarget = null;
        mTriggerBack = false;
        mShouldStartOnNextMoveEvent = false;
        if (backNavigationInfo == null) {
            return;
        }

        if (!USE_TRANSITION) {
            RemoteAnimationTarget animationTarget = backNavigationInfo
                    .getDepartingAnimationTarget();
            if (animationTarget != null) {
                if (animationTarget.leash != null && animationTarget.leash.isValid()) {
                    mTransaction.remove(animationTarget.leash);
                }
            }
            SurfaceControl screenshotSurface = backNavigationInfo.getScreenshotSurface();
            if (screenshotSurface != null && screenshotSurface.isValid()) {
                mTransaction.remove(screenshotSurface);
            }
            mTransaction.apply();
        }
        stopTransition();
        backNavigationInfo.onBackNavigationFinished(triggerBack);
        if (USE_TRANSITION) {
            final IBackNaviAnimationController controller = mBackAnimationController;
            if (controller != null) {
                try {
                    controller.finish(triggerBack);
                } catch (RemoteException r) {
                    // Oh no!
                }
            }
            mBackAnimationController = null;
        }
    }

    private void startTransition() {
        if (mTransitionInProgress) {
            return;
        }
        mTransitionInProgress = true;
        mShellExecutor.executeDelayed(mResetTransitionRunnable, MAX_TRANSITION_DURATION);
    }

    private void stopTransition() {
        if (!mTransitionInProgress) {
            return;
        }
        mShellExecutor.removeCallbacks(mResetTransitionRunnable);
        mTransitionInProgress = false;
    }

    private void createAdaptor() {
        mIBackAnimationRunner = new IBackAnimationRunner.Stub() {
            @Override
            public void onAnimationCancelled() {
                // no op for now
            }
            @Override // Binder interface
            public void onAnimationStart(IBackNaviAnimationController controller, int type,
                    RemoteAnimationTarget[] apps, RemoteAnimationTarget[] wallpapers,
                    RemoteAnimationTarget[] nonApps) {
                mShellExecutor.execute(() -> {
                    mBackAnimationController = controller;
                    for (int i = 0; i < apps.length; i++) {
                        final RemoteAnimationTarget target = apps[i];
                        if (MODE_CLOSING == target.mode) {
                            mAnimationTarget = target;
                        } else if (MODE_OPENING == target.mode) {
                            // TODO Home activity should handle the visibility for itself
                            //  once it finish relayout for orientation change
                            SurfaceControl.Transaction tx =
                                    new SurfaceControl.Transaction();
                            tx.setAlpha(target.leash, 1);
                            tx.apply();
                        }
                    }
                    dispatchOnBackStarted(mBackToLauncherCallback,
                            mTouchTracker.createStartEvent(mAnimationTarget));
                    final BackMotionEvent backInit = mTouchTracker.createProgressEvent();
                    if (!mCachingBackDispatcher.consume()) {
                        dispatchOnBackProgressed(mBackToLauncherCallback, backInit);
                    }
                });
            }
        };
        mBackAnimationAdaptor = new BackAnimationAdaptor(mIBackAnimationRunner,
                BackNavigationInfo.TYPE_RETURN_TO_HOME);
    }
}
