Change drag to edge to requestSplit when not in freeform.

When in fullscreen or split, dragging a task using the handle would move
the task to freeform in split snap bounds. This change makes it so the
task instead requests splitscreen windowing mode.

Test: Manual
Test: atest DragToDesktopTransitionHandlerTest
Bug: 336310725
Change-Id: Idf232146e625a193960a8d9497f116eb3e4ac0e4
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
index 6e45397..831c6cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt
@@ -459,7 +459,9 @@
             "DesktopTasksController: cancelDragToDesktop taskId=%d",
             task.taskId
         )
-        dragToDesktopTransitionHandler.cancelDragToDesktopTransition()
+        dragToDesktopTransitionHandler.cancelDragToDesktopTransition(
+            DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
+        )
     }
 
     private fun moveToFullscreenWithAnimation(task: RunningTaskInfo, position: Point) {
@@ -1078,20 +1080,31 @@
     @JvmOverloads
     fun requestSplit(
         taskInfo: RunningTaskInfo,
-        leftOrTop: Boolean = false,
+        leftOrTop: Boolean = false
     ) {
-        val windowingMode = taskInfo.windowingMode
-        if (
-            windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_FREEFORM
-        ) {
-            val wct = WindowContainerTransaction()
-            addMoveToSplitChanges(wct, taskInfo)
-            splitScreenController.requestEnterSplitSelect(
-                taskInfo,
-                wct,
-                if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT,
-                taskInfo.configuration.windowConfiguration.bounds
-            )
+        // If a drag to desktop is in progress, we want to enter split select
+        // even if the requesting task is already in split.
+        val isDragging = dragToDesktopTransitionHandler.inProgress
+        val shouldRequestSplit = taskInfo.isFullscreen || taskInfo.isFreeform || isDragging
+        if (shouldRequestSplit) {
+            if (isDragging) {
+                releaseVisualIndicator()
+                val cancelState = if (leftOrTop) {
+                    DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT
+                } else {
+                    DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT
+                }
+                dragToDesktopTransitionHandler.cancelDragToDesktopTransition(cancelState)
+            } else {
+                val wct = WindowContainerTransaction()
+                addMoveToSplitChanges(wct, taskInfo)
+                splitScreenController.requestEnterSplitSelect(
+                    taskInfo,
+                    wct,
+                    if (leftOrTop) SPLIT_POSITION_TOP_OR_LEFT else SPLIT_POSITION_BOTTOM_OR_RIGHT,
+                    taskInfo.configuration.windowConfiguration.bounds
+                )
+            }
         }
     }
 
@@ -1220,7 +1233,10 @@
      * @param taskInfo the task being dragged.
      * @param y height of drag, to be checked against status bar height.
      */
-    fun onDragPositioningEndThroughStatusBar(inputCoordinates: PointF, taskInfo: RunningTaskInfo) {
+    fun onDragPositioningEndThroughStatusBar(
+        inputCoordinates: PointF,
+        taskInfo: RunningTaskInfo,
+    ) {
         val indicator = getVisualIndicator() ?: return
         val indicatorType = indicator.updateIndicatorType(inputCoordinates, taskInfo.windowingMode)
         when (indicatorType) {
@@ -1237,10 +1253,10 @@
                 cancelDragToDesktop(taskInfo)
             }
             DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
-                finalizeDragToDesktop(taskInfo, getSnapBounds(taskInfo, SnapPosition.LEFT))
+                requestSplit(taskInfo, leftOrTop = true)
             }
             DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
-                finalizeDragToDesktop(taskInfo, getSnapBounds(taskInfo, SnapPosition.RIGHT))
+                requestSplit(taskInfo, leftOrTop = false)
             }
         }
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
index 98c79d7..d99b724 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt
@@ -4,6 +4,7 @@
 import android.animation.AnimatorListenerAdapter
 import android.animation.RectEvaluator
 import android.animation.ValueAnimator
+import android.app.ActivityManager.RunningTaskInfo
 import android.app.ActivityOptions
 import android.app.ActivityOptions.SourceInfo
 import android.app.ActivityTaskManager.INVALID_TASK_ID
@@ -12,9 +13,11 @@
 import android.app.PendingIntent.FLAG_MUTABLE
 import android.app.WindowConfiguration.ACTIVITY_TYPE_HOME
 import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
+import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW
 import android.content.Context
 import android.content.Intent
 import android.content.Intent.FILL_IN_COMPONENT
+import android.graphics.PointF
 import android.graphics.Rect
 import android.os.Bundle
 import android.os.IBinder
@@ -30,6 +33,7 @@
 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
 import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED
+import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition
 import com.android.wm.shell.protolog.ShellProtoLogGroup
 import com.android.wm.shell.shared.TransitionUtil
 import com.android.wm.shell.splitscreen.SplitScreenController
@@ -186,7 +190,7 @@
      * outside the desktop drop zone and is instead dropped back into the status bar region that
      * means the user wants to remain in their current windowing mode.
      */
-    fun cancelDragToDesktopTransition() {
+    fun cancelDragToDesktopTransition(cancelState: CancelState) {
         if (!inProgress) {
             // Don't attempt to cancel a drag to desktop transition since there is no transition in
             // progress which means that the drag to desktop transition was never successfully
@@ -200,13 +204,32 @@
             clearState()
             return
         }
-        state.cancelled = true
-        if (state.draggedTaskChange != null) {
+        state.cancelState = cancelState
+
+        if (state.draggedTaskChange != null && cancelState == CancelState.STANDARD_CANCEL) {
             // Regular case, transient launch of Home happened as is waiting for the cancel
             // transient to start and merge. Animate the cancellation (scale back to original
             // bounds) first before actually starting the cancel transition so that the wallpaper
             // is visible behind the animating task.
             startCancelAnimation()
+        } else if (
+            state.draggedTaskChange != null &&
+            (cancelState == CancelState.CANCEL_SPLIT_LEFT ||
+                    cancelState == CancelState.CANCEL_SPLIT_RIGHT)
+            ) {
+            // We have a valid dragged task, but the animation will be handled by
+            // SplitScreenController; request the transition here.
+            @SplitPosition val splitPosition = if (cancelState == CancelState.CANCEL_SPLIT_LEFT) {
+                SPLIT_POSITION_TOP_OR_LEFT
+            } else {
+                SPLIT_POSITION_BOTTOM_OR_RIGHT
+            }
+            val wct = WindowContainerTransaction()
+            restoreWindowOrder(wct, state)
+            state.startTransitionFinishTransaction?.apply()
+            state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+            requestSplitFromScaledTask(splitPosition, wct)
+            clearState()
         } else {
             // There's no dragged task, this can happen when the "cancel" happened too quickly
             // before the "start" transition is even ready (like on a fling gesture). The
@@ -217,6 +240,54 @@
         }
     }
 
+    /** Calculate the bounds of a scaled task, then use those bounds to request split select. */
+    private fun requestSplitFromScaledTask(
+        @SplitPosition splitPosition: Int,
+        wct: WindowContainerTransaction
+    ) {
+        val state = requireTransitionState()
+        val taskInfo = state.draggedTaskChange?.taskInfo
+            ?: error("Expected non-null taskInfo")
+        val taskBounds = Rect(taskInfo.configuration.windowConfiguration.bounds)
+        val taskScale = state.dragAnimator.scale
+        val scaledWidth = taskBounds.width() * taskScale
+        val scaledHeight = taskBounds.height() * taskScale
+        val dragPosition = PointF(state.dragAnimator.position)
+        state.dragAnimator.cancelAnimator()
+        val animatedTaskBounds = Rect(
+            dragPosition.x.toInt(),
+            dragPosition.y.toInt(),
+            (dragPosition.x + scaledWidth).toInt(),
+            (dragPosition.y + scaledHeight).toInt()
+        )
+        requestSplitSelect(wct, taskInfo, splitPosition, animatedTaskBounds)
+    }
+
+    private fun requestSplitSelect(
+        wct: WindowContainerTransaction,
+        taskInfo: RunningTaskInfo,
+        @SplitPosition splitPosition: Int,
+        taskBounds: Rect = Rect(taskInfo.configuration.windowConfiguration.bounds)
+    ) {
+        // Prepare to exit split in order to enter split select.
+        if (taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW) {
+            splitScreenController.prepareExitSplitScreen(
+                wct,
+                splitScreenController.getStageOfTask(taskInfo.taskId),
+                SplitScreenController.EXIT_REASON_DESKTOP_MODE
+            )
+            splitScreenController.transitionHandler.onSplitToDesktop()
+        }
+        wct.setWindowingMode(taskInfo.token, WINDOWING_MODE_MULTI_WINDOW)
+        wct.setDensityDpi(taskInfo.token, context.resources.displayMetrics.densityDpi)
+        splitScreenController.requestEnterSplitSelect(
+            taskInfo,
+            wct,
+            splitPosition,
+            taskBounds
+        )
+    }
+
     override fun startAnimation(
         transition: IBinder,
         info: TransitionInfo,
@@ -261,7 +332,7 @@
                     is TransitionState.FromSplit -> {
                         state.splitRootChange = change
                         val layer =
-                            if (!state.cancelled) {
+                            if (state.cancelState == CancelState.NO_CANCEL) {
                                 // Normal case, split root goes to the bottom behind everything
                                 // else.
                                 appLayers - i
@@ -311,8 +382,18 @@
                 // Do not do this in the cancel-early case though, since in that case nothing should
                 // happen on screen so the layering will remain the same as if no transition
                 // occurred.
-                if (change.taskInfo?.taskId == state.draggedTaskId && !state.cancelled) {
+                if (
+                    change.taskInfo?.taskId == state.draggedTaskId &&
+                    state.cancelState != CancelState.STANDARD_CANCEL
+                ) {
+                    // We need access to the dragged task's change in both non-cancel and split
+                    // cancel cases.
                     state.draggedTaskChange = change
+                }
+                if (
+                    change.taskInfo?.taskId == state.draggedTaskId &&
+                    state.cancelState == CancelState.NO_CANCEL
+                    ) {
                     taskDisplayAreaOrganizer.reparentToDisplayArea(
                         change.endDisplayId,
                         change.leash,
@@ -331,11 +412,11 @@
         state.startTransitionFinishTransaction = finishTransaction
         startTransaction.apply()
 
-        if (!state.cancelled) {
+        if (state.cancelState == CancelState.NO_CANCEL) {
             // Normal case, start animation to scale down the dragged task. It'll also be moved to
             // follow the finger and when released we'll start the next phase/transition.
             state.dragAnimator.startAnimation()
-        } else {
+        } else if (state.cancelState == CancelState.STANDARD_CANCEL) {
             // Cancel-early case, the state was flagged was cancelled already, which means the
             // gesture ended in the cancel region. This can happen even before the start transition
             // is ready/animate here when cancelling quickly like with a fling. There's no point
@@ -343,6 +424,26 @@
             // directly into starting the cancel transition to restore WM order. Surfaces should
             // not move as if no transition happened.
             startCancelDragToDesktopTransition()
+        } else if (
+            state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
+            state.cancelState == CancelState.CANCEL_SPLIT_RIGHT
+            ){
+            // Cancel-early case for split-cancel. The state was flagged already as a cancel for
+            // requesting split select. Similar to the above, this can happen due to quick fling
+            // gestures. We can simply request split here without needing to calculate animated
+            // task bounds as the task has not shrunk at all.
+            val splitPosition = if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT) {
+                SPLIT_POSITION_TOP_OR_LEFT
+            } else {
+                SPLIT_POSITION_BOTTOM_OR_RIGHT
+            }
+            val taskInfo = state.draggedTaskChange?.taskInfo
+                ?: error("Expected non-null task info.")
+            val wct = WindowContainerTransaction()
+            restoreWindowOrder(wct)
+            state.startTransitionFinishTransaction?.apply()
+            state.startTransitionFinishCb?.onTransitionFinished(null /* wct */)
+            requestSplitSelect(wct, taskInfo, splitPosition)
         }
         return true
     }
@@ -355,6 +456,12 @@
         finishCallback: Transitions.TransitionFinishCallback
     ) {
         val state = requireTransitionState()
+        // We don't want to merge the split select animation if that's what we requested.
+        if (state.cancelState == CancelState.CANCEL_SPLIT_LEFT ||
+            state.cancelState == CancelState.CANCEL_SPLIT_RIGHT) {
+            clearState()
+            return
+        }
         val isCancelTransition =
             info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP &&
                 transition == state.cancelTransitionToken &&
@@ -552,6 +659,17 @@
     private fun startCancelDragToDesktopTransition() {
         val state = requireTransitionState()
         val wct = WindowContainerTransaction()
+        restoreWindowOrder(wct, state)
+        state.cancelTransitionToken =
+            transitions.startTransition(
+                TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this
+            )
+    }
+
+    private fun restoreWindowOrder(
+        wct: WindowContainerTransaction,
+        state: TransitionState = requireTransitionState()
+    ) {
         when (state) {
             is TransitionState.FromFullscreen -> {
                 // There may have been tasks sent behind home that are not the dragged task (like
@@ -580,9 +698,6 @@
         }
         val homeWc = state.homeToken ?: error("Home task should be non-null before cancelling")
         wct.restoreTransientOrder(homeWc)
-
-        state.cancelTransitionToken =
-            transitions.startTransition(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, wct, this)
     }
 
     private fun clearState() {
@@ -624,7 +739,7 @@
         abstract var cancelTransitionToken: IBinder?
         abstract var homeToken: WindowContainerToken?
         abstract var draggedTaskChange: Change?
-        abstract var cancelled: Boolean
+        abstract var cancelState: CancelState
         abstract var startAborted: Boolean
 
         data class FromFullscreen(
@@ -636,7 +751,7 @@
             override var cancelTransitionToken: IBinder? = null,
             override var homeToken: WindowContainerToken? = null,
             override var draggedTaskChange: Change? = null,
-            override var cancelled: Boolean = false,
+            override var cancelState: CancelState = CancelState.NO_CANCEL,
             override var startAborted: Boolean = false,
             var otherRootChanges: MutableList<Change> = mutableListOf()
         ) : TransitionState()
@@ -650,13 +765,25 @@
             override var cancelTransitionToken: IBinder? = null,
             override var homeToken: WindowContainerToken? = null,
             override var draggedTaskChange: Change? = null,
-            override var cancelled: Boolean = false,
+            override var cancelState: CancelState = CancelState.NO_CANCEL,
             override var startAborted: Boolean = false,
             var splitRootChange: Change? = null,
             var otherSplitTask: Int
         ) : TransitionState()
     }
 
+    /** Enum to provide context on cancelling a drag to desktop event. */
+    enum class CancelState {
+        /** No cancel case; this drag is not flagged for a cancel event. */
+        NO_CANCEL,
+        /** A standard cancel event; should restore task to previous windowing mode. */
+        STANDARD_CANCEL,
+        /** A cancel event where the task will request to enter split on the left side. */
+        CANCEL_SPLIT_LEFT,
+        /** A cancel event where the task will request to enter split on the right side. */
+        CANCEL_SPLIT_RIGHT
+    }
+
     companion object {
         /** The duration of the animation to commit or cancel the drag-to-desktop gesture. */
         private const val DRAG_TO_DESKTOP_FINISH_ANIM_DURATION_MS = 336L
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
index 2ade3fb..bbf523b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt
@@ -18,6 +18,8 @@
 import com.android.wm.shell.RootTaskDisplayAreaOrganizer
 import com.android.wm.shell.ShellTestCase
 import com.android.wm.shell.TestRunningTaskInfoBuilder
+import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT
+import com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT
 import com.android.wm.shell.splitscreen.SplitScreenController
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP
@@ -48,6 +50,7 @@
     @Mock private lateinit var transitions: Transitions
     @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
     @Mock private lateinit var splitScreenController: SplitScreenController
+    @Mock private lateinit var dragAnimator: MoveToDesktopAnimator
 
     private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() }
 
@@ -68,7 +71,6 @@
     @Test
     fun startDragToDesktop_animateDragWhenReady() {
         val task = createTask()
-        val dragAnimator = mock<MoveToDesktopAnimator>()
         // Simulate transition is started.
         val transition = startDragToDesktopTransition(task, dragAnimator)
 
@@ -90,36 +92,36 @@
 
     @Test
     fun startDragToDesktop_cancelledBeforeReady_startCancelTransition() {
-        val task = createTask()
-        val dragAnimator = mock<MoveToDesktopAnimator>()
-        // Simulate transition is started and is ready to animate.
-        val transition = startDragToDesktopTransition(task, dragAnimator)
-
-        handler.cancelDragToDesktopTransition()
-
-        handler.startAnimation(
-            transition = transition,
-            info =
-                createTransitionInfo(
-                    type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
-                    draggedTask = task
-                ),
-            startTransaction = mock(),
-            finishTransaction = mock(),
-            finishCallback = {}
-        )
-
-        // Don't even animate the "drag" since it was already cancelled.
-        verify(dragAnimator, never()).startAnimation()
-        // Instead, start the cancel transition.
+        performEarlyCancel(DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL)
         verify(transitions)
             .startTransition(eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), eq(handler))
     }
 
     @Test
+    fun startDragToDesktop_cancelledBeforeReady_verifySplitLeftCancel() {
+        performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT)
+        verify(splitScreenController).requestEnterSplitSelect(
+            any(),
+            any(),
+            eq(SPLIT_POSITION_TOP_OR_LEFT),
+            any()
+        )
+    }
+
+    @Test
+    fun startDragToDesktop_cancelledBeforeReady_verifySplitRightCancel() {
+        performEarlyCancel(DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT)
+        verify(splitScreenController).requestEnterSplitSelect(
+            any(),
+            any(),
+            eq(SPLIT_POSITION_BOTTOM_OR_RIGHT),
+            any()
+        )
+    }
+
+    @Test
     fun startDragToDesktop_aborted_finishDropped() {
         val task = createTask()
-        val dragAnimator = mock<MoveToDesktopAnimator>()
         // Simulate transition is started.
         val transition = startDragToDesktopTransition(task, dragAnimator)
         // But the transition was aborted.
@@ -137,14 +139,15 @@
     @Test
     fun startDragToDesktop_aborted_cancelDropped() {
         val task = createTask()
-        val dragAnimator = mock<MoveToDesktopAnimator>()
         // Simulate transition is started.
         val transition = startDragToDesktopTransition(task, dragAnimator)
         // But the transition was aborted.
         handler.onTransitionConsumed(transition, aborted = true, mock())
 
         // Attempt to finish the failed drag start.
-        handler.cancelDragToDesktopTransition()
+        handler.cancelDragToDesktopTransition(
+            DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
+        )
 
         // Should not be attempted and state should be reset.
         assertFalse(handler.inProgress)
@@ -153,7 +156,6 @@
     @Test
     fun startDragToDesktop_anotherTransitionInProgress_startDropped() {
         val task = createTask()
-        val dragAnimator = mock<MoveToDesktopAnimator>()
 
         // Simulate attempt to start two drag to desktop transitions.
         startDragToDesktopTransition(task, dragAnimator)
@@ -169,39 +171,63 @@
 
     @Test
     fun cancelDragToDesktop_startWasReady_cancel() {
-        val task = createTask()
-        val dragAnimator = mock<MoveToDesktopAnimator>()
-        whenever(dragAnimator.position).thenReturn(PointF())
-        // Simulate transition is started and is ready to animate.
-        val transition = startDragToDesktopTransition(task, dragAnimator)
-        handler.startAnimation(
-            transition = transition,
-            info =
-                createTransitionInfo(
-                    type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
-                    draggedTask = task
-                ),
-            startTransaction = mock(),
-            finishTransaction = mock(),
-            finishCallback = {}
-        )
+        startDrag()
 
         // Then user cancelled after it had already started.
-        handler.cancelDragToDesktopTransition()
+        handler.cancelDragToDesktopTransition(
+            DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
+        )
 
         // Cancel animation should run since it had already started.
         verify(dragAnimator).cancelAnimator()
     }
 
     @Test
+    fun cancelDragToDesktop_splitLeftCancelType_splitRequested() {
+        startDrag()
+
+        // Then user cancelled it, requesting split.
+        handler.cancelDragToDesktopTransition(
+            DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT
+        )
+
+        // Verify the request went through split controller.
+        verify(splitScreenController).requestEnterSplitSelect(
+            any(),
+            any(),
+            eq(SPLIT_POSITION_TOP_OR_LEFT),
+            any()
+        )
+    }
+
+    @Test
+    fun cancelDragToDesktop_splitRightCancelType_splitRequested() {
+        startDrag()
+
+        // Then user cancelled it, requesting split.
+        handler.cancelDragToDesktopTransition(
+            DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT
+        )
+
+        // Verify the request went through split controller.
+        verify(splitScreenController).requestEnterSplitSelect(
+            any(),
+            any(),
+            eq(SPLIT_POSITION_BOTTOM_OR_RIGHT),
+            any()
+        )
+    }
+
+    @Test
     fun cancelDragToDesktop_startWasNotReady_animateCancel() {
         val task = createTask()
-        val dragAnimator = mock<MoveToDesktopAnimator>()
         // Simulate transition is started and is ready to animate.
         startDragToDesktopTransition(task, dragAnimator)
 
         // Then user cancelled before the transition was ready and animated.
-        handler.cancelDragToDesktopTransition()
+        handler.cancelDragToDesktopTransition(
+            DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
+        )
 
         // No need to animate the cancel since the start animation couldn't even start.
         verifyZeroInteractions(dragAnimator)
@@ -210,7 +236,9 @@
     @Test
     fun cancelDragToDesktop_transitionNotInProgress_dropCancel() {
         // Then cancel is called before the transition was started.
-        handler.cancelDragToDesktopTransition()
+        handler.cancelDragToDesktopTransition(
+            DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL
+        )
 
         // Verify cancel is dropped.
         verify(transitions, never()).startTransition(
@@ -233,6 +261,24 @@
         )
     }
 
+    private fun startDrag() {
+        val task = createTask()
+        whenever(dragAnimator.position).thenReturn(PointF())
+        // Simulate transition is started and is ready to animate.
+        val transition = startDragToDesktopTransition(task, dragAnimator)
+        handler.startAnimation(
+            transition = transition,
+            info =
+            createTransitionInfo(
+                type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
+                draggedTask = task
+            ),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+            finishCallback = {}
+        )
+    }
+
     private fun startDragToDesktopTransition(
         task: RunningTaskInfo,
         dragAnimator: MoveToDesktopAnimator
@@ -250,6 +296,29 @@
         return token
     }
 
+    private fun performEarlyCancel(cancelState: DragToDesktopTransitionHandler.CancelState) {
+        val task = createTask()
+        // Simulate transition is started and is ready to animate.
+        val transition = startDragToDesktopTransition(task, dragAnimator)
+
+        handler.cancelDragToDesktopTransition(cancelState)
+
+        handler.startAnimation(
+            transition = transition,
+            info =
+            createTransitionInfo(
+                type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP,
+                draggedTask = task
+            ),
+            startTransaction = mock(),
+            finishTransaction = mock(),
+            finishCallback = {}
+        )
+
+        // Don't even animate the "drag" since it was already cancelled.
+        verify(dragAnimator, never()).startAnimation()
+    }
+
     private fun createTask(
         @WindowingMode windowingMode: Int = WINDOWING_MODE_FULLSCREEN,
         isHome: Boolean = false,