/*
 * Copyright (C) 2015 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.systemui.stackdivider;

import static android.view.PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
import static android.view.PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
import static android.view.WindowManager.DOCKED_RIGHT;

import android.animation.AnimationHandler;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.Region;
import android.graphics.Region.Op;
import android.hardware.display.DisplayManager;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.util.AttributeSet;
import android.util.Slog;
import android.view.Display;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
import android.view.VelocityTracker;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewConfiguration;
import android.view.ViewRootImpl;
import android.view.ViewTreeObserver.InternalInsetsInfo;
import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
import android.widget.FrameLayout;

import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.policy.DividerSnapAlgorithm;
import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget;
import com.android.internal.policy.DockedDividerUtils;
import com.android.systemui.Dependency;
import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.recents.OverviewProxyService;
import com.android.systemui.statusbar.FlingAnimationUtils;

import java.util.function.Consumer;

/**
 * Docked stack divider.
 */
public class DividerView extends FrameLayout implements OnTouchListener,
        OnComputeInternalInsetsListener {
    private static final String TAG = "DividerView";
    private static final boolean DEBUG = Divider.DEBUG;

    public interface DividerCallbacks {
        void onDraggingStart();
        void onDraggingEnd();
        void growRecents();
    }

    static final long TOUCH_ANIMATION_DURATION = 150;
    static final long TOUCH_RELEASE_ANIMATION_DURATION = 200;

    public static final int INVALID_RECENTS_GROW_TARGET = -1;

    private static final int LOG_VALUE_RESIZE_50_50 = 0;
    private static final int LOG_VALUE_RESIZE_DOCKED_SMALLER = 1;
    private static final int LOG_VALUE_RESIZE_DOCKED_LARGER = 2;

    private static final int LOG_VALUE_UNDOCK_MAX_DOCKED = 0;
    private static final int LOG_VALUE_UNDOCK_MAX_OTHER = 1;

    private static final int TASK_POSITION_SAME = Integer.MAX_VALUE;

    /**
     * How much the background gets scaled when we are in the minimized dock state.
     */
    private static final float MINIMIZE_DOCK_SCALE = 0f;
    private static final float ADJUSTED_FOR_IME_SCALE = 0.5f;

    private static final PathInterpolator SLOWDOWN_INTERPOLATOR =
            new PathInterpolator(0.5f, 1f, 0.5f, 1f);
    private static final PathInterpolator DIM_INTERPOLATOR =
            new PathInterpolator(.23f, .87f, .52f, -0.11f);
    private static final Interpolator IME_ADJUST_INTERPOLATOR =
            new PathInterpolator(0.2f, 0f, 0.1f, 1f);

    private DividerHandleView mHandle;
    private View mBackground;
    private MinimizedDockShadow mMinimizedShadow;
    private int mStartX;
    private int mStartY;
    private int mStartPosition;
    private int mDockSide;
    private boolean mMoving;
    private int mTouchSlop;
    private boolean mBackgroundLifted;
    private boolean mIsInMinimizeInteraction;
    SnapTarget mSnapTargetBeforeMinimized;

    private int mDividerInsets;
    private final Display mDefaultDisplay;

    private int mDividerSize;
    private int mTouchElevation;
    private int mLongPressEntraceAnimDuration;

    private final Rect mDockedRect = new Rect();
    private final Rect mDockedTaskRect = new Rect();
    private final Rect mOtherTaskRect = new Rect();
    private final Rect mOtherRect = new Rect();
    private final Rect mDockedInsetRect = new Rect();
    private final Rect mOtherInsetRect = new Rect();
    private final Rect mLastResizeRect = new Rect();
    private final Rect mTmpRect = new Rect();
    private WindowManagerProxy mWindowManagerProxy;
    private DividerWindowManager mWindowManager;
    private VelocityTracker mVelocityTracker;
    private FlingAnimationUtils mFlingAnimationUtils;
    private SplitDisplayLayout mSplitLayout;
    private DividerImeController mImeController;
    private DividerCallbacks mCallback;
    private final AnimationHandler mAnimationHandler = new AnimationHandler();

    private boolean mGrowRecents;
    private ValueAnimator mCurrentAnimator;
    private boolean mEntranceAnimationRunning;
    private boolean mExitAnimationRunning;
    private int mExitStartPosition;
    private boolean mDockedStackMinimized;
    private boolean mHomeStackResizable;
    private boolean mAdjustedForIme;
    private DividerState mState;

    private SplitScreenTaskOrganizer mTiles;
    boolean mFirstLayout = true;
    int mDividerPositionX;
    int mDividerPositionY;

    private final Matrix mTmpMatrix = new Matrix();
    private final float[] mTmpValues = new float[9];

    // The view is removed or in the process of been removed from the system.
    private boolean mRemoved;

    // Whether the surface for this view has been hidden regardless of actual visibility. This is
    // used interact with keyguard.
    private boolean mSurfaceHidden = false;

    private final Handler mHandler = new Handler();

    private final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() {
        @Override
        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
            super.onInitializeAccessibilityNodeInfo(host, info);
            final DividerSnapAlgorithm snapAlgorithm = getSnapAlgorithm();
            if (isHorizontalDivision()) {
                info.addAction(new AccessibilityAction(R.id.action_move_tl_full,
                        mContext.getString(R.string.accessibility_action_divider_top_full)));
                if (snapAlgorithm.isFirstSplitTargetAvailable()) {
                    info.addAction(new AccessibilityAction(R.id.action_move_tl_70,
                            mContext.getString(R.string.accessibility_action_divider_top_70)));
                }
                if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) {
                    // Only show the middle target if there are more than 1 split target
                    info.addAction(new AccessibilityAction(R.id.action_move_tl_50,
                        mContext.getString(R.string.accessibility_action_divider_top_50)));
                }
                if (snapAlgorithm.isLastSplitTargetAvailable()) {
                    info.addAction(new AccessibilityAction(R.id.action_move_tl_30,
                            mContext.getString(R.string.accessibility_action_divider_top_30)));
                }
                info.addAction(new AccessibilityAction(R.id.action_move_rb_full,
                        mContext.getString(R.string.accessibility_action_divider_bottom_full)));
            } else {
                info.addAction(new AccessibilityAction(R.id.action_move_tl_full,
                        mContext.getString(R.string.accessibility_action_divider_left_full)));
                if (snapAlgorithm.isFirstSplitTargetAvailable()) {
                    info.addAction(new AccessibilityAction(R.id.action_move_tl_70,
                            mContext.getString(R.string.accessibility_action_divider_left_70)));
                }
                if (snapAlgorithm.showMiddleSplitTargetForAccessibility()) {
                    // Only show the middle target if there are more than 1 split target
                    info.addAction(new AccessibilityAction(R.id.action_move_tl_50,
                        mContext.getString(R.string.accessibility_action_divider_left_50)));
                }
                if (snapAlgorithm.isLastSplitTargetAvailable()) {
                    info.addAction(new AccessibilityAction(R.id.action_move_tl_30,
                            mContext.getString(R.string.accessibility_action_divider_left_30)));
                }
                info.addAction(new AccessibilityAction(R.id.action_move_rb_full,
                        mContext.getString(R.string.accessibility_action_divider_right_full)));
            }
        }

        @Override
        public boolean performAccessibilityAction(View host, int action, Bundle args) {
            int currentPosition = getCurrentPosition();
            SnapTarget nextTarget = null;
            DividerSnapAlgorithm snapAlgorithm = mSplitLayout.getSnapAlgorithm();
            if (action == R.id.action_move_tl_full) {
                nextTarget = snapAlgorithm.getDismissEndTarget();
            } else if (action == R.id.action_move_tl_70) {
                nextTarget = snapAlgorithm.getLastSplitTarget();
            } else if (action == R.id.action_move_tl_50) {
                nextTarget = snapAlgorithm.getMiddleTarget();
            } else if (action == R.id.action_move_tl_30) {
                nextTarget = snapAlgorithm.getFirstSplitTarget();
            } else if (action == R.id.action_move_rb_full) {
                nextTarget = snapAlgorithm.getDismissStartTarget();
            }
            if (nextTarget != null) {
                startDragging(true /* animate */, false /* touching */);
                stopDragging(currentPosition, nextTarget, 250, Interpolators.FAST_OUT_SLOW_IN);
                return true;
            }
            return super.performAccessibilityAction(host, action, args);
        }
    };

    private final Runnable mResetBackgroundRunnable = new Runnable() {
        @Override
        public void run() {
            resetBackground();
        }
    };

    private Runnable mUpdateEmbeddedMatrix = () -> {
        if (getViewRootImpl() == null) {
            return;
        }
        if (isHorizontalDivision()) {
            mTmpMatrix.setTranslate(0, mDividerPositionY - mDividerInsets);
        } else {
            mTmpMatrix.setTranslate(mDividerPositionX - mDividerInsets, 0);
        }
        mTmpMatrix.getValues(mTmpValues);
        try {
            getViewRootImpl().getAccessibilityEmbeddedConnection().setScreenMatrix(mTmpValues);
        } catch (RemoteException e) {
        }
    };

    public DividerView(Context context) {
        this(context, null);
    }

    public DividerView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public DividerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public DividerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        final DisplayManager displayManager =
                (DisplayManager) mContext.getSystemService(Context.DISPLAY_SERVICE);
        mDefaultDisplay = displayManager.getDisplay(Display.DEFAULT_DISPLAY);
        mAnimationHandler.setProvider(new SfVsyncFrameCallbackProvider());
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mHandle = findViewById(R.id.docked_divider_handle);
        mBackground = findViewById(R.id.docked_divider_background);
        mMinimizedShadow = findViewById(R.id.minimized_dock_shadow);
        mHandle.setOnTouchListener(this);
        final int dividerWindowWidth = getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.docked_stack_divider_thickness);
        mDividerInsets = getResources().getDimensionPixelSize(
                com.android.internal.R.dimen.docked_stack_divider_insets);
        mDividerSize = dividerWindowWidth - 2 * mDividerInsets;
        mTouchElevation = getResources().getDimensionPixelSize(
                R.dimen.docked_stack_divider_lift_elevation);
        mLongPressEntraceAnimDuration = getResources().getInteger(
                R.integer.long_press_dock_anim_duration);
        mGrowRecents = getResources().getBoolean(R.bool.recents_grow_in_multiwindow);
        mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop();
        mFlingAnimationUtils = new FlingAnimationUtils(getResources().getDisplayMetrics(), 0.3f);
        boolean landscape = getResources().getConfiguration().orientation
                == Configuration.ORIENTATION_LANDSCAPE;
        mHandle.setPointerIcon(PointerIcon.getSystemIcon(getContext(),
                landscape ? TYPE_HORIZONTAL_DOUBLE_ARROW : TYPE_VERTICAL_DOUBLE_ARROW));
        getViewTreeObserver().addOnComputeInternalInsetsListener(this);
        mHandle.setAccessibilityDelegate(mHandleDelegate);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        // Save the current target if not minimized once attached to window
        if (mDockSide != WindowManager.DOCKED_INVALID && !mIsInMinimizeInteraction) {
            saveSnapTargetBeforeMinimized(mSnapTargetBeforeMinimized);
        }
        mFirstLayout = true;
    }

    void onDividerRemoved() {
        mRemoved = true;
        mCallback = null;
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (mFirstLayout) {
            // Wait for first layout so that the ViewRootImpl surface has been created.
            initializeSurfaceState();
            mFirstLayout = false;
        }
        int minimizeLeft = 0;
        int minimizeTop = 0;
        if (mDockSide == WindowManager.DOCKED_TOP) {
            minimizeTop = mBackground.getTop();
        } else if (mDockSide == WindowManager.DOCKED_LEFT) {
            minimizeLeft = mBackground.getLeft();
        } else if (mDockSide == WindowManager.DOCKED_RIGHT) {
            minimizeLeft = mBackground.getRight() - mMinimizedShadow.getWidth();
        }
        mMinimizedShadow.layout(minimizeLeft, minimizeTop,
                minimizeLeft + mMinimizedShadow.getMeasuredWidth(),
                minimizeTop + mMinimizedShadow.getMeasuredHeight());
        if (changed) {
            notifySplitScreenBoundsChanged();
        }
    }

    public void injectDependencies(DividerWindowManager windowManager, DividerState dividerState,
            DividerCallbacks callback, SplitScreenTaskOrganizer tiles, SplitDisplayLayout sdl,
            DividerImeController imeController, WindowManagerProxy wmProxy) {
        mWindowManager = windowManager;
        mState = dividerState;
        mCallback = callback;
        mTiles = tiles;
        mSplitLayout = sdl;
        mImeController = imeController;
        mWindowManagerProxy = wmProxy;

        if (mState.mRatioPositionBeforeMinimized == 0) {
            // Set the middle target as the initial state
            mSnapTargetBeforeMinimized = mSplitLayout.getSnapAlgorithm().getMiddleTarget();
        } else {
            repositionSnapTargetBeforeMinimized();
        }
    }

    public Rect getNonMinimizedSplitScreenSecondaryBounds() {
        mOtherTaskRect.set(mSplitLayout.mSecondary);
        return mOtherTaskRect;
    }

    private boolean inSplitMode() {
        return getVisibility() == VISIBLE;
    }

    /** Unlike setVisible, this directly hides the surface without changing view visibility. */
    void setHidden(boolean hidden) {
        if (mSurfaceHidden == hidden) {
            return;
        }
        mSurfaceHidden = hidden;
        post(() -> {
            final SurfaceControl sc = getWindowSurfaceControl();
            if (sc == null) {
                return;
            }
            Transaction t = mTiles.getTransaction();
            if (hidden) {
                t.hide(sc);
            } else {
                t.show(sc);
            }
            mImeController.setDimsHidden(t, hidden);
            t.apply();
            mTiles.releaseTransaction(t);
        });
    }

    boolean isHidden() {
        return mSurfaceHidden;
    }

    public boolean startDragging(boolean animate, boolean touching) {
        cancelFlingAnimation();
        if (touching) {
            mHandle.setTouching(true, animate);
        }
        mDockSide = mSplitLayout.getPrimarySplitSide();

        mWindowManagerProxy.setResizing(true);
        if (touching) {
            mWindowManager.setSlippery(false);
            liftBackground();
        }
        if (mCallback != null) {
            mCallback.onDraggingStart();
        }
        return inSplitMode();
    }

    public void stopDragging(int position, float velocity, boolean avoidDismissStart,
            boolean logMetrics) {
        mHandle.setTouching(false, true /* animate */);
        fling(position, velocity, avoidDismissStart, logMetrics);
        mWindowManager.setSlippery(true);
        releaseBackground();
    }

    public void stopDragging(int position, SnapTarget target, long duration,
            Interpolator interpolator) {
        stopDragging(position, target, duration, 0 /* startDelay*/, 0 /* endDelay */, interpolator);
    }

    public void stopDragging(int position, SnapTarget target, long duration,
            Interpolator interpolator, long endDelay) {
        stopDragging(position, target, duration, 0 /* startDelay*/, endDelay, interpolator);
    }

    public void stopDragging(int position, SnapTarget target, long duration, long startDelay,
            long endDelay, Interpolator interpolator) {
        mHandle.setTouching(false, true /* animate */);
        flingTo(position, target, duration, startDelay, endDelay, interpolator);
        mWindowManager.setSlippery(true);
        releaseBackground();
    }

    private void stopDragging() {
        mHandle.setTouching(false, true /* animate */);
        mWindowManager.setSlippery(true);
        releaseBackground();
    }

    private void updateDockSide() {
        mDockSide = mSplitLayout.getPrimarySplitSide();
        mMinimizedShadow.setDockSide(mDockSide);
    }

    public DividerSnapAlgorithm getSnapAlgorithm() {
        return mDockedStackMinimized ? mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable)
                        : mSplitLayout.getSnapAlgorithm();
    }

    public int getCurrentPosition() {
        return isHorizontalDivision() ? mDividerPositionY : mDividerPositionX;
    }

    public boolean isMinimized() {
        return mDockedStackMinimized;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        convertToScreenCoordinates(event);
        final int action = event.getAction() & MotionEvent.ACTION_MASK;
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mVelocityTracker = VelocityTracker.obtain();
                mVelocityTracker.addMovement(event);
                mStartX = (int) event.getX();
                mStartY = (int) event.getY();
                boolean result = startDragging(true /* animate */, true /* touching */);
                if (!result) {

                    // Weren't able to start dragging successfully, so cancel it again.
                    stopDragging();
                }
                mStartPosition = getCurrentPosition();
                mMoving = false;
                return result;
            case MotionEvent.ACTION_MOVE:
                mVelocityTracker.addMovement(event);
                int x = (int) event.getX();
                int y = (int) event.getY();
                boolean exceededTouchSlop =
                        isHorizontalDivision() && Math.abs(y - mStartY) > mTouchSlop
                                || (!isHorizontalDivision() && Math.abs(x - mStartX) > mTouchSlop);
                if (!mMoving && exceededTouchSlop) {
                    mStartX = x;
                    mStartY = y;
                    mMoving = true;
                }
                if (mMoving && mDockSide != WindowManager.DOCKED_INVALID) {
                    SnapTarget snapTarget = getSnapAlgorithm().calculateSnapTarget(
                            mStartPosition, 0 /* velocity */, false /* hardDismiss */);
                    resizeStackSurfaces(calculatePosition(x, y), mStartPosition, snapTarget,
                            null /* transaction */);
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mVelocityTracker.addMovement(event);

                x = (int) event.getRawX();
                y = (int) event.getRawY();

                mVelocityTracker.computeCurrentVelocity(1000);
                int position = calculatePosition(x, y);
                stopDragging(position, isHorizontalDivision() ? mVelocityTracker.getYVelocity()
                        : mVelocityTracker.getXVelocity(), false /* avoidDismissStart */,
                        true /* log */);
                mMoving = false;
                break;
        }
        return true;
    }

    private void logResizeEvent(SnapTarget snapTarget) {
        if (snapTarget == mSplitLayout.getSnapAlgorithm().getDismissStartTarget()) {
            MetricsLogger.action(
                    mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_MAX, dockSideTopLeft(mDockSide)
                            ? LOG_VALUE_UNDOCK_MAX_OTHER
                            : LOG_VALUE_UNDOCK_MAX_DOCKED);
        } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getDismissEndTarget()) {
            MetricsLogger.action(
                    mContext, MetricsEvent.ACTION_WINDOW_UNDOCK_MAX, dockSideBottomRight(mDockSide)
                            ? LOG_VALUE_UNDOCK_MAX_OTHER
                            : LOG_VALUE_UNDOCK_MAX_DOCKED);
        } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getMiddleTarget()) {
            MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE,
                    LOG_VALUE_RESIZE_50_50);
        } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getFirstSplitTarget()) {
            MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE,
                    dockSideTopLeft(mDockSide)
                            ? LOG_VALUE_RESIZE_DOCKED_SMALLER
                            : LOG_VALUE_RESIZE_DOCKED_LARGER);
        } else if (snapTarget == mSplitLayout.getSnapAlgorithm().getLastSplitTarget()) {
            MetricsLogger.action(mContext, MetricsEvent.ACTION_WINDOW_DOCK_RESIZE,
                    dockSideTopLeft(mDockSide)
                            ? LOG_VALUE_RESIZE_DOCKED_LARGER
                            : LOG_VALUE_RESIZE_DOCKED_SMALLER);
        }
    }

    private void convertToScreenCoordinates(MotionEvent event) {
        event.setLocation(event.getRawX(), event.getRawY());
    }

    private void fling(int position, float velocity, boolean avoidDismissStart,
            boolean logMetrics) {
        DividerSnapAlgorithm currentSnapAlgorithm = getSnapAlgorithm();
        SnapTarget snapTarget = currentSnapAlgorithm.calculateSnapTarget(position, velocity);
        if (avoidDismissStart && snapTarget == currentSnapAlgorithm.getDismissStartTarget()) {
            snapTarget = currentSnapAlgorithm.getFirstSplitTarget();
        }
        if (logMetrics) {
            logResizeEvent(snapTarget);
        }
        ValueAnimator anim = getFlingAnimator(position, snapTarget, 0 /* endDelay */);
        mFlingAnimationUtils.apply(anim, position, snapTarget.position, velocity);
        anim.start();
    }

    private void flingTo(int position, SnapTarget target, long duration, long startDelay,
            long endDelay, Interpolator interpolator) {
        ValueAnimator anim = getFlingAnimator(position, target, endDelay);
        anim.setDuration(duration);
        anim.setStartDelay(startDelay);
        anim.setInterpolator(interpolator);
        anim.start();
    }

    private ValueAnimator getFlingAnimator(int position, final SnapTarget snapTarget,
            final long endDelay) {
        if (mCurrentAnimator != null) {
            cancelFlingAnimation();
            updateDockSide();
        }
        if (DEBUG) Slog.d(TAG, "Getting fling " + position + "->" + snapTarget.position);
        final boolean taskPositionSameAtEnd = snapTarget.flag == SnapTarget.FLAG_NONE;
        ValueAnimator anim = ValueAnimator.ofInt(position, snapTarget.position);
        anim.addUpdateListener(animation -> resizeStackSurfaces((int) animation.getAnimatedValue(),
                taskPositionSameAtEnd && animation.getAnimatedFraction() == 1f
                        ? TASK_POSITION_SAME
                        : snapTarget.taskPosition,
                snapTarget, null /* transaction */));
        Consumer<Boolean> endAction = cancelled -> {
            if (DEBUG) Slog.d(TAG, "End Fling " + cancelled + " min:" + mIsInMinimizeInteraction);
            final boolean wasMinimizeInteraction = mIsInMinimizeInteraction;
            // Reset minimized divider position after unminimized state animation finishes.
            if (!cancelled && !mDockedStackMinimized && mIsInMinimizeInteraction) {
                mIsInMinimizeInteraction = false;
            }
            boolean dismissed = commitSnapFlags(snapTarget);
            mWindowManagerProxy.setResizing(false);
            updateDockSide();
            mCurrentAnimator = null;
            mEntranceAnimationRunning = false;
            mExitAnimationRunning = false;
            if (!dismissed && !wasMinimizeInteraction) {
                WindowManagerProxy.applyResizeSplits(snapTarget.position, mSplitLayout);
            }
            if (mCallback != null) {
                mCallback.onDraggingEnd();
            }

            // Record last snap target the divider moved to
            if (!mIsInMinimizeInteraction) {
                // The last snapTarget position can be negative when the last divider position was
                // offscreen. In that case, save the middle (default) SnapTarget so calculating next
                // position isn't negative.
                final SnapTarget saveTarget;
                if (snapTarget.position < 0) {
                    saveTarget = mSplitLayout.getSnapAlgorithm().getMiddleTarget();
                } else {
                    saveTarget = snapTarget;
                }
                final DividerSnapAlgorithm snapAlgo = mSplitLayout.getSnapAlgorithm();
                if (saveTarget.position != snapAlgo.getDismissEndTarget().position
                        && saveTarget.position != snapAlgo.getDismissStartTarget().position) {
                    saveSnapTargetBeforeMinimized(saveTarget);
                }
            }
            notifySplitScreenBoundsChanged();
        };
        anim.addListener(new AnimatorListenerAdapter() {

            private boolean mCancelled;

            @Override
            public void onAnimationCancel(Animator animation) {
                mCancelled = true;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                long delay = 0;
                if (endDelay != 0) {
                    delay = endDelay;
                } else if (mCancelled) {
                    delay = 0;
                }
                if (delay == 0) {
                    endAction.accept(mCancelled);
                } else {
                    final Boolean cancelled = mCancelled;
                    if (DEBUG) Slog.d(TAG, "Posting endFling " + cancelled + " d:" + delay + "ms");
                    mHandler.postDelayed(() -> endAction.accept(cancelled), delay);
                }
            }
        });
        anim.setAnimationHandler(mAnimationHandler);
        mCurrentAnimator = anim;
        return anim;
    }

    private void notifySplitScreenBoundsChanged() {
        if (mSplitLayout.mPrimary == null || mSplitLayout.mSecondary == null) {
            return;
        }
        mOtherTaskRect.set(mSplitLayout.mSecondary);

        mTmpRect.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(), mHandle.getBottom());
        if (isHorizontalDivision()) {
            mTmpRect.offsetTo(0, mDividerPositionY);
        } else {
            mTmpRect.offsetTo(mDividerPositionX, 0);
        }
        mWindowManagerProxy.setTouchRegion(mTmpRect);

        mTmpRect.set(mSplitLayout.mDisplayLayout.stableInsets());
        switch (mSplitLayout.getPrimarySplitSide()) {
            case WindowManager.DOCKED_LEFT:
                mTmpRect.left = 0;
                break;
            case WindowManager.DOCKED_RIGHT:
                mTmpRect.right = 0;
                break;
            case WindowManager.DOCKED_TOP:
                mTmpRect.top = 0;
                break;
        }
        Dependency.get(OverviewProxyService.class)
                .notifySplitScreenBoundsChanged(mOtherTaskRect, mTmpRect);
    }

    private void cancelFlingAnimation() {
        if (mCurrentAnimator != null) {
            mCurrentAnimator.cancel();
        }
    }

    private boolean commitSnapFlags(SnapTarget target) {
        if (target.flag == SnapTarget.FLAG_NONE) {
            return false;
        }
        final boolean dismissOrMaximize;
        if (target.flag == SnapTarget.FLAG_DISMISS_START) {
            dismissOrMaximize = mDockSide == WindowManager.DOCKED_LEFT
                    || mDockSide == WindowManager.DOCKED_TOP;
        } else {
            dismissOrMaximize = mDockSide == WindowManager.DOCKED_RIGHT
                    || mDockSide == WindowManager.DOCKED_BOTTOM;
        }
        mWindowManagerProxy.dismissOrMaximizeDocked(mTiles, mSplitLayout, dismissOrMaximize);
        Transaction t = mTiles.getTransaction();
        setResizeDimLayer(t, true /* primary */, 0f);
        setResizeDimLayer(t, false /* primary */, 0f);
        t.apply();
        mTiles.releaseTransaction(t);
        return true;
    }

    private void liftBackground() {
        if (mBackgroundLifted) {
            return;
        }
        if (isHorizontalDivision()) {
            mBackground.animate().scaleY(1.4f);
        } else {
            mBackground.animate().scaleX(1.4f);
        }
        mBackground.animate()
                .setInterpolator(Interpolators.TOUCH_RESPONSE)
                .setDuration(TOUCH_ANIMATION_DURATION)
                .translationZ(mTouchElevation)
                .start();

        // Lift handle as well so it doesn't get behind the background, even though it doesn't
        // cast shadow.
        mHandle.animate()
                .setInterpolator(Interpolators.TOUCH_RESPONSE)
                .setDuration(TOUCH_ANIMATION_DURATION)
                .translationZ(mTouchElevation)
                .start();
        mBackgroundLifted = true;
    }

    private void releaseBackground() {
        if (!mBackgroundLifted) {
            return;
        }
        mBackground.animate()
                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
                .translationZ(0)
                .scaleX(1f)
                .scaleY(1f)
                .start();
        mHandle.animate()
                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                .setDuration(TOUCH_RELEASE_ANIMATION_DURATION)
                .translationZ(0)
                .start();
        mBackgroundLifted = false;
    }

    private void initializeSurfaceState() {
        int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
        // Recalculate the split-layout's internal tile bounds
        mSplitLayout.resizeSplits(midPos);
        Transaction t = mTiles.getTransaction();
        if (mDockedStackMinimized) {
            int position = mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable)
                    .getMiddleTarget().position;
            calculateBoundsForPosition(position, mDockSide, mDockedRect);
            calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide),
                    mOtherRect);
            mDividerPositionX = mDividerPositionY = position;
            resizeSplitSurfaces(t, mDockedRect, mSplitLayout.mPrimary,
                    mOtherRect, mSplitLayout.mSecondary);
        } else {
            resizeSplitSurfaces(t, mSplitLayout.mPrimary, null,
                    mSplitLayout.mSecondary, null);
        }
        setResizeDimLayer(t, true /* primary */, 0.f /* alpha */);
        setResizeDimLayer(t, false /* secondary */, 0.f /* alpha */);
        t.apply();
        mTiles.releaseTransaction(t);

        // Get the actually-visible bar dimensions (relative to full window). This is a thin
        // bar going through the center.
        final Rect dividerBar = isHorizontalDivision()
                ? new Rect(0, mDividerInsets, mSplitLayout.mDisplayLayout.width(),
                mDividerInsets + mDividerSize)
                : new Rect(mDividerInsets, 0, mDividerInsets + mDividerSize,
                mSplitLayout.mDisplayLayout.height());
        final Region touchRegion = new Region(dividerBar);
        // Add in the "draggable" portion. While not visible, this is an expanded area that the
        // user can interact with.
        touchRegion.union(new Rect(mHandle.getLeft(), mHandle.getTop(),
                mHandle.getRight(), mHandle.getBottom()));
        mWindowManager.setTouchRegion(touchRegion);
    }

    void setMinimizedDockStack(boolean minimized, boolean isHomeStackResizable,
            Transaction t) {
        mHomeStackResizable = isHomeStackResizable;
        updateDockSide();
        if (!minimized) {
            resetBackground();
        }
        mMinimizedShadow.setAlpha(minimized ? 1f : 0f);
        if (mDockedStackMinimized != minimized) {
            mDockedStackMinimized = minimized;
            if (mSplitLayout.mDisplayLayout.rotation() != mDefaultDisplay.getRotation()) {
                // Splitscreen to minimize is about to starts after rotating landscape to seascape,
                // update display info and snap algorithm targets
                repositionSnapTargetBeforeMinimized();
            }
            if (mIsInMinimizeInteraction != minimized || mCurrentAnimator != null) {
                cancelFlingAnimation();
                if (minimized) {
                    // Relayout to recalculate the divider shadow when minimizing
                    requestLayout();
                    mIsInMinimizeInteraction = true;
                    resizeStackSurfaces(mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable)
                            .getMiddleTarget(), t);
                } else {
                    resizeStackSurfaces(mSnapTargetBeforeMinimized, t);
                    mIsInMinimizeInteraction = false;
                }
            }
        }
    }

    void enterSplitMode(boolean isHomeStackResizable) {
        post(() -> {
            final SurfaceControl sc = getWindowSurfaceControl();
            if (sc == null) {
                return;
            }
            Transaction t = mTiles.getTransaction();
            t.show(sc).apply();
            mTiles.releaseTransaction(t);
        });

        SnapTarget miniMid =
                mSplitLayout.getMinimizedSnapAlgorithm(isHomeStackResizable).getMiddleTarget();
        if (mDockedStackMinimized) {
            mDividerPositionY = mDividerPositionX = miniMid.position;
        }
    }

    /**
     * Tries to grab a surface control from ViewRootImpl. If this isn't available for some reason
     * (ie. the window isn't ready yet), it will get the surfacecontrol that the WindowlessWM has
     * assigned to it.
     */
    private SurfaceControl getWindowSurfaceControl() {
        final ViewRootImpl root = getViewRootImpl();
        if (root == null) {
            return null;
        }
        SurfaceControl out = root.getSurfaceControl();
        if (out != null && out.isValid()) {
            return out;
        }
        return mWindowManager.mSystemWindows.getViewSurface(this);
    }

    void exitSplitMode() {
        // Reset tile bounds
        final SurfaceControl sc = getWindowSurfaceControl();
        if (sc == null) {
            return;
        }
        Transaction t = mTiles.getTransaction();
        t.hide(sc).apply();
        mTiles.releaseTransaction(t);
        int midPos = mSplitLayout.getSnapAlgorithm().getMiddleTarget().position;
        WindowManagerProxy.applyResizeSplits(midPos, mSplitLayout);
    }

    public void setMinimizedDockStack(boolean minimized, long animDuration,
            boolean isHomeStackResizable) {
        if (DEBUG) Slog.d(TAG, "setMinDock: " + mDockedStackMinimized + "->" + minimized);
        mHomeStackResizable = isHomeStackResizable;
        updateDockSide();
        if (mDockedStackMinimized != minimized) {
            mIsInMinimizeInteraction = true;
            mDockedStackMinimized = minimized;
            stopDragging(minimized
                            ? mSnapTargetBeforeMinimized.position
                            : getCurrentPosition(),
                    minimized
                            ? mSplitLayout.getMinimizedSnapAlgorithm(mHomeStackResizable)
                                    .getMiddleTarget()
                            : mSnapTargetBeforeMinimized,
                    animDuration, Interpolators.FAST_OUT_SLOW_IN, 0);
            setAdjustedForIme(false, animDuration);
        }
        if (!minimized) {
            mBackground.animate().withEndAction(mResetBackgroundRunnable);
        }
        mBackground.animate()
                .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
                .setDuration(animDuration)
                .start();
    }

    // Needed to end any currently playing animations when they might compete with other anims
    // (specifically, IME adjust animation immediately after leaving minimized). Someday maybe
    // these can be unified, but not today.
    void finishAnimations() {
        if (mCurrentAnimator != null) {
            mCurrentAnimator.end();
        }
    }

    public void setAdjustedForIme(boolean adjustedForIme, long animDuration) {
        if (mAdjustedForIme == adjustedForIme) {
            return;
        }
        updateDockSide();
        mHandle.animate()
                .setInterpolator(IME_ADJUST_INTERPOLATOR)
                .setDuration(animDuration)
                .alpha(adjustedForIme ? 0f : 1f)
                .start();
        if (mDockSide == WindowManager.DOCKED_TOP) {
            mBackground.setPivotY(0);
            mBackground.animate()
                    .scaleY(adjustedForIme ? ADJUSTED_FOR_IME_SCALE : 1f);
        }
        if (!adjustedForIme) {
            mBackground.animate().withEndAction(mResetBackgroundRunnable);
        }
        mBackground.animate()
                .setInterpolator(IME_ADJUST_INTERPOLATOR)
                .setDuration(animDuration)
                .start();
        mAdjustedForIme = adjustedForIme;
    }

    private void saveSnapTargetBeforeMinimized(SnapTarget target) {
        mSnapTargetBeforeMinimized = target;
        mState.mRatioPositionBeforeMinimized = (float) target.position /
                (isHorizontalDivision() ? mSplitLayout.mDisplayLayout.height()
                        : mSplitLayout.mDisplayLayout.width());
    }

    private void resetBackground() {
        mBackground.setPivotX(mBackground.getWidth() / 2);
        mBackground.setPivotY(mBackground.getHeight() / 2);
        mBackground.setScaleX(1f);
        mBackground.setScaleY(1f);
        mMinimizedShadow.setAlpha(0f);
    }

    @Override
    protected void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
    }

    private void repositionSnapTargetBeforeMinimized() {
        int position = (int) (mState.mRatioPositionBeforeMinimized *
                (isHorizontalDivision() ? mSplitLayout.mDisplayLayout.height()
                        : mSplitLayout.mDisplayLayout.width()));

        // Set the snap target before minimized but do not save until divider is attached and not
        // minimized because it does not know its minimized state yet.
        mSnapTargetBeforeMinimized =
                mSplitLayout.getSnapAlgorithm().calculateNonDismissingSnapTarget(position);
    }

    private int calculatePosition(int touchX, int touchY) {
        return isHorizontalDivision() ? calculateYPosition(touchY) : calculateXPosition(touchX);
    }

    public boolean isHorizontalDivision() {
        return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
    }

    private int calculateXPosition(int touchX) {
        return mStartPosition + touchX - mStartX;
    }

    private int calculateYPosition(int touchY) {
        return mStartPosition + touchY - mStartY;
    }

    private void alignTopLeft(Rect containingRect, Rect rect) {
        int width = rect.width();
        int height = rect.height();
        rect.set(containingRect.left, containingRect.top,
                containingRect.left + width, containingRect.top + height);
    }

    private void alignBottomRight(Rect containingRect, Rect rect) {
        int width = rect.width();
        int height = rect.height();
        rect.set(containingRect.right - width, containingRect.bottom - height,
                containingRect.right, containingRect.bottom);
    }

    public void calculateBoundsForPosition(int position, int dockSide, Rect outRect) {
        DockedDividerUtils.calculateBoundsForPosition(position, dockSide, outRect,
                mSplitLayout.mDisplayLayout.width(), mSplitLayout.mDisplayLayout.height(),
                mDividerSize);
    }

    private void resizeStackSurfaces(SnapTarget taskSnapTarget, Transaction t) {
        resizeStackSurfaces(taskSnapTarget.position, taskSnapTarget.position, taskSnapTarget, t);
    }

    void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect otherRect) {
        resizeSplitSurfaces(t, dockedRect, null, otherRect, null);
    }

    private void resizeSplitSurfaces(Transaction t, Rect dockedRect, Rect dockedTaskRect,
            Rect otherRect, Rect otherTaskRect) {
        dockedTaskRect = dockedTaskRect == null ? dockedRect : dockedTaskRect;
        otherTaskRect = otherTaskRect == null ? otherRect : otherTaskRect;

        mDividerPositionX = mSplitLayout.getPrimarySplitSide() == DOCKED_RIGHT
                ? otherRect.right : dockedRect.right;
        mDividerPositionY = dockedRect.bottom;

        if (DEBUG) {
            Slog.d(TAG, "Resizing split surfaces: " + dockedRect + " " + dockedTaskRect
                    + " " + otherRect + " " + otherTaskRect);
        }

        t.setPosition(mTiles.mPrimarySurface, dockedTaskRect.left, dockedTaskRect.top);
        Rect crop = new Rect(dockedRect);
        crop.offsetTo(-Math.min(dockedTaskRect.left - dockedRect.left, 0),
                -Math.min(dockedTaskRect.top - dockedRect.top, 0));
        t.setWindowCrop(mTiles.mPrimarySurface, crop);
        t.setPosition(mTiles.mSecondarySurface, otherTaskRect.left, otherTaskRect.top);
        crop.set(otherRect);
        crop.offsetTo(-(otherTaskRect.left - otherRect.left),
                -(otherTaskRect.top - otherRect.top));
        t.setWindowCrop(mTiles.mSecondarySurface, crop);
        final SurfaceControl dividerCtrl = getWindowSurfaceControl();
        if (dividerCtrl != null) {
            if (isHorizontalDivision()) {
                t.setPosition(dividerCtrl, 0, mDividerPositionY - mDividerInsets);
            } else {
                t.setPosition(dividerCtrl, mDividerPositionX - mDividerInsets, 0);
            }
        }
        if (getViewRootImpl() != null) {
            mHandler.removeCallbacks(mUpdateEmbeddedMatrix);
            mHandler.post(mUpdateEmbeddedMatrix);
        }
    }

    void setResizeDimLayer(Transaction t, boolean primary, float alpha) {
        SurfaceControl dim = primary ? mTiles.mPrimaryDim : mTiles.mSecondaryDim;
        if (alpha <= 0.001f) {
            t.hide(dim);
        } else {
            t.setAlpha(dim, alpha);
            t.show(dim);
        }
    }

    void resizeStackSurfaces(int position, int taskPosition, SnapTarget taskSnapTarget,
            Transaction transaction) {
        if (mRemoved) {
            // This divider view has been removed so shouldn't have any additional influence.
            return;
        }
        calculateBoundsForPosition(position, mDockSide, mDockedRect);
        calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide),
                mOtherRect);

        if (mDockedRect.equals(mLastResizeRect) && !mEntranceAnimationRunning) {
            return;
        }

        // Make sure shadows are updated
        if (mBackground.getZ() > 0f) {
            mBackground.invalidate();
        }

        final boolean ownTransaction = transaction == null;
        final Transaction t = ownTransaction ? mTiles.getTransaction() : transaction;
        mLastResizeRect.set(mDockedRect);
        if (mIsInMinimizeInteraction) {
            calculateBoundsForPosition(mSnapTargetBeforeMinimized.position, mDockSide,
                    mDockedTaskRect);
            calculateBoundsForPosition(mSnapTargetBeforeMinimized.position,
                    DockedDividerUtils.invertDockSide(mDockSide), mOtherTaskRect);

            // Move a right-docked-app to line up with the divider while dragging it
            if (mDockSide == DOCKED_RIGHT) {
                mDockedTaskRect.offset(Math.max(position, -mDividerSize)
                        - mDockedTaskRect.left + mDividerSize, 0);
            }
            resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
            if (ownTransaction) {
                t.apply();
                mTiles.releaseTransaction(t);
            }
            return;
        }

        if (mEntranceAnimationRunning && taskPosition != TASK_POSITION_SAME) {
            calculateBoundsForPosition(taskPosition, mDockSide, mDockedTaskRect);

            // Move a docked app if from the right in position with the divider up to insets
            if (mDockSide == DOCKED_RIGHT) {
                mDockedTaskRect.offset(Math.max(position, -mDividerSize)
                        - mDockedTaskRect.left + mDividerSize, 0);
            }
            calculateBoundsForPosition(taskPosition, DockedDividerUtils.invertDockSide(mDockSide),
                    mOtherTaskRect);
            resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
        } else if (mExitAnimationRunning && taskPosition != TASK_POSITION_SAME) {
            calculateBoundsForPosition(taskPosition, mDockSide, mDockedTaskRect);
            mDockedInsetRect.set(mDockedTaskRect);
            calculateBoundsForPosition(mExitStartPosition,
                    DockedDividerUtils.invertDockSide(mDockSide), mOtherTaskRect);
            mOtherInsetRect.set(mOtherTaskRect);
            applyExitAnimationParallax(mOtherTaskRect, position);

            // Move a right-docked-app to line up with the divider while dragging it
            if (mDockSide == DOCKED_RIGHT) {
                mDockedTaskRect.offset(position + mDividerSize, 0);
            }
            resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
        } else if (taskPosition != TASK_POSITION_SAME) {
            calculateBoundsForPosition(position, DockedDividerUtils.invertDockSide(mDockSide),
                    mOtherRect);
            int dockSideInverted = DockedDividerUtils.invertDockSide(mDockSide);
            int taskPositionDocked =
                    restrictDismissingTaskPosition(taskPosition, mDockSide, taskSnapTarget);
            int taskPositionOther =
                    restrictDismissingTaskPosition(taskPosition, dockSideInverted, taskSnapTarget);
            calculateBoundsForPosition(taskPositionDocked, mDockSide, mDockedTaskRect);
            calculateBoundsForPosition(taskPositionOther, dockSideInverted, mOtherTaskRect);
            mTmpRect.set(0, 0, mSplitLayout.mDisplayLayout.width(),
                    mSplitLayout.mDisplayLayout.height());
            alignTopLeft(mDockedRect, mDockedTaskRect);
            alignTopLeft(mOtherRect, mOtherTaskRect);
            mDockedInsetRect.set(mDockedTaskRect);
            mOtherInsetRect.set(mOtherTaskRect);
            if (dockSideTopLeft(mDockSide)) {
                alignTopLeft(mTmpRect, mDockedInsetRect);
                alignBottomRight(mTmpRect, mOtherInsetRect);
            } else {
                alignBottomRight(mTmpRect, mDockedInsetRect);
                alignTopLeft(mTmpRect, mOtherInsetRect);
            }
            applyDismissingParallax(mDockedTaskRect, mDockSide, taskSnapTarget, position,
                    taskPositionDocked);
            applyDismissingParallax(mOtherTaskRect, dockSideInverted, taskSnapTarget, position,
                    taskPositionOther);
            resizeSplitSurfaces(t, mDockedRect, mDockedTaskRect, mOtherRect, mOtherTaskRect);
        } else {
            resizeSplitSurfaces(t, mDockedRect, null, mOtherRect, null);
        }
        SnapTarget closestDismissTarget = getSnapAlgorithm().getClosestDismissTarget(position);
        float dimFraction = getDimFraction(position, closestDismissTarget);
        setResizeDimLayer(t, isDismissTargetPrimary(closestDismissTarget), dimFraction);
        if (ownTransaction) {
            t.apply();
            mTiles.releaseTransaction(t);
        }
    }

    private void applyExitAnimationParallax(Rect taskRect, int position) {
        if (mDockSide == WindowManager.DOCKED_TOP) {
            taskRect.offset(0, (int) ((position - mExitStartPosition) * 0.25f));
        } else if (mDockSide == WindowManager.DOCKED_LEFT) {
            taskRect.offset((int) ((position - mExitStartPosition) * 0.25f), 0);
        } else if (mDockSide == WindowManager.DOCKED_RIGHT) {
            taskRect.offset((int) ((mExitStartPosition - position) * 0.25f), 0);
        }
    }

    private float getDimFraction(int position, SnapTarget dismissTarget) {
        if (mEntranceAnimationRunning) {
            return 0f;
        }
        float fraction = getSnapAlgorithm().calculateDismissingFraction(position);
        fraction = Math.max(0, Math.min(fraction, 1f));
        fraction = DIM_INTERPOLATOR.getInterpolation(fraction);
        return fraction;
    }

    /**
     * When the snap target is dismissing one side, make sure that the dismissing side doesn't get
     * 0 size.
     */
    private int restrictDismissingTaskPosition(int taskPosition, int dockSide,
            SnapTarget snapTarget) {
        if (snapTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(dockSide)) {
            return Math.max(mSplitLayout.getSnapAlgorithm().getFirstSplitTarget().position,
                    mStartPosition);
        } else if (snapTarget.flag == SnapTarget.FLAG_DISMISS_END
                && dockSideBottomRight(dockSide)) {
            return Math.min(mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position,
                    mStartPosition);
        } else {
            return taskPosition;
        }
    }

    /**
     * Applies a parallax to the task when dismissing.
     */
    private void applyDismissingParallax(Rect taskRect, int dockSide, SnapTarget snapTarget,
            int position, int taskPosition) {
        float fraction = Math.min(1, Math.max(0,
                mSplitLayout.getSnapAlgorithm().calculateDismissingFraction(position)));
        SnapTarget dismissTarget = null;
        SnapTarget splitTarget = null;
        int start = 0;
        if (position <= mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position
                && dockSideTopLeft(dockSide)) {
            dismissTarget = mSplitLayout.getSnapAlgorithm().getDismissStartTarget();
            splitTarget = mSplitLayout.getSnapAlgorithm().getFirstSplitTarget();
            start = taskPosition;
        } else if (position >= mSplitLayout.getSnapAlgorithm().getLastSplitTarget().position
                && dockSideBottomRight(dockSide)) {
            dismissTarget = mSplitLayout.getSnapAlgorithm().getDismissEndTarget();
            splitTarget = mSplitLayout.getSnapAlgorithm().getLastSplitTarget();
            start = splitTarget.position;
        }
        if (dismissTarget != null && fraction > 0f
                && isDismissing(splitTarget, position, dockSide)) {
            fraction = calculateParallaxDismissingFraction(fraction, dockSide);
            int offsetPosition = (int) (start +
                    fraction * (dismissTarget.position - splitTarget.position));
            int width = taskRect.width();
            int height = taskRect.height();
            switch (dockSide) {
                case WindowManager.DOCKED_LEFT:
                    taskRect.left = offsetPosition - width;
                    taskRect.right = offsetPosition;
                    break;
                case WindowManager.DOCKED_RIGHT:
                    taskRect.left = offsetPosition + mDividerSize;
                    taskRect.right = offsetPosition + width + mDividerSize;
                    break;
                case WindowManager.DOCKED_TOP:
                    taskRect.top = offsetPosition - height;
                    taskRect.bottom = offsetPosition;
                    break;
                case WindowManager.DOCKED_BOTTOM:
                    taskRect.top = offsetPosition + mDividerSize;
                    taskRect.bottom = offsetPosition + height + mDividerSize;
                    break;
            }
        }
    }

    /**
     * @return for a specified {@code fraction}, this returns an adjusted value that simulates a
     *         slowing down parallax effect
     */
    private static float calculateParallaxDismissingFraction(float fraction, int dockSide) {
        float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f;

        // Less parallax at the top, just because.
        if (dockSide == WindowManager.DOCKED_TOP) {
            result /= 2f;
        }
        return result;
    }

    private static boolean isDismissing(SnapTarget snapTarget, int position, int dockSide) {
        if (dockSide == WindowManager.DOCKED_TOP || dockSide == WindowManager.DOCKED_LEFT) {
            return position < snapTarget.position;
        } else {
            return position > snapTarget.position;
        }
    }

    private boolean isDismissTargetPrimary(SnapTarget dismissTarget) {
        return (dismissTarget.flag == SnapTarget.FLAG_DISMISS_START && dockSideTopLeft(mDockSide))
                || (dismissTarget.flag == SnapTarget.FLAG_DISMISS_END
                        && dockSideBottomRight(mDockSide));
    }

    /**
     * @return true if and only if {@code dockSide} is top or left
     */
    private static boolean dockSideTopLeft(int dockSide) {
        return dockSide == WindowManager.DOCKED_TOP || dockSide == WindowManager.DOCKED_LEFT;
    }

    /**
     * @return true if and only if {@code dockSide} is bottom or right
     */
    private static boolean dockSideBottomRight(int dockSide) {
        return dockSide == WindowManager.DOCKED_BOTTOM || dockSide == WindowManager.DOCKED_RIGHT;
    }

    @Override
    public void onComputeInternalInsets(InternalInsetsInfo inoutInfo) {
        inoutInfo.setTouchableInsets(InternalInsetsInfo.TOUCHABLE_INSETS_REGION);
        inoutInfo.touchableRegion.set(mHandle.getLeft(), mHandle.getTop(), mHandle.getRight(),
                mHandle.getBottom());
        inoutInfo.touchableRegion.op(mBackground.getLeft(), mBackground.getTop(),
                mBackground.getRight(), mBackground.getBottom(), Op.UNION);
    }

    /**
     * Checks whether recents will grow when invoked. This happens in multi-window when recents is
     * very small. When invoking recents, we shrink the docked stack so recents has more space.
     *
     * @return the position of the divider when recents grows, or
     *         {@link #INVALID_RECENTS_GROW_TARGET} if recents won't grow
     */
    public int growsRecents() {
        boolean result = mGrowRecents
                && mDockSide == WindowManager.DOCKED_TOP
                && getCurrentPosition() == getSnapAlgorithm().getLastSplitTarget().position;
        if (result) {
            return getSnapAlgorithm().getMiddleTarget().position;
        } else {
            return INVALID_RECENTS_GROW_TARGET;
        }
    }

    void onRecentsActivityStarting() {
        if (mGrowRecents && mDockSide == WindowManager.DOCKED_TOP
                && getSnapAlgorithm().getMiddleTarget() != getSnapAlgorithm().getLastSplitTarget()
                && getCurrentPosition() == getSnapAlgorithm().getLastSplitTarget().position) {
            mState.growAfterRecentsDrawn = true;
            startDragging(false /* animate */, false /* touching */);
        }
    }

    void onDockedFirstAnimationFrame() {
        saveSnapTargetBeforeMinimized(mSplitLayout.getSnapAlgorithm().getMiddleTarget());
    }

    void onDockedTopTask() {
        mState.growAfterRecentsDrawn = false;
        mState.animateAfterRecentsDrawn = true;
        startDragging(false /* animate */, false /* touching */);
        updateDockSide();
        mEntranceAnimationRunning = true;

        resizeStackSurfaces(calculatePositionForInsetBounds(),
                mSplitLayout.getSnapAlgorithm().getMiddleTarget().position,
                mSplitLayout.getSnapAlgorithm().getMiddleTarget(),
                null /* transaction */);
    }

    void onRecentsDrawn() {
        updateDockSide();
        final int position = calculatePositionForInsetBounds();
        if (mState.animateAfterRecentsDrawn) {
            mState.animateAfterRecentsDrawn = false;

            mHandler.post(() -> {
                // Delay switching resizing mode because this might cause jank in recents animation
                // that's longer than this animation.
                stopDragging(position, getSnapAlgorithm().getMiddleTarget(),
                        mLongPressEntraceAnimDuration, Interpolators.FAST_OUT_SLOW_IN,
                        200 /* endDelay */);
            });
        }
        if (mState.growAfterRecentsDrawn) {
            mState.growAfterRecentsDrawn = false;
            updateDockSide();
            if (mCallback != null) {
                mCallback.growRecents();
            }
            stopDragging(position, getSnapAlgorithm().getMiddleTarget(), 336,
                    Interpolators.FAST_OUT_SLOW_IN);
        }
    }

    void onUndockingTask() {
        int dockSide = mSplitLayout.getPrimarySplitSide();
        if (inSplitMode()) {
            startDragging(false /* animate */, false /* touching */);
            SnapTarget target = dockSideTopLeft(dockSide)
                    ? mSplitLayout.getSnapAlgorithm().getDismissEndTarget()
                    : mSplitLayout.getSnapAlgorithm().getDismissStartTarget();

            // Don't start immediately - give a little bit time to settle the drag resize change.
            mExitAnimationRunning = true;
            mExitStartPosition = getCurrentPosition();
            stopDragging(mExitStartPosition, target, 336 /* duration */, 100 /* startDelay */,
                    0 /* endDelay */, Interpolators.FAST_OUT_SLOW_IN);
        }
    }

    private int calculatePositionForInsetBounds() {
        mSplitLayout.mDisplayLayout.getStableBounds(mTmpRect);
        return DockedDividerUtils.calculatePositionForBounds(mTmpRect, mDockSide, mDividerSize);
    }
}
