Add mixed transition handling for desktop changes

Identifies desktop changes (immersive exits for now) within transitions
that should be mainly handled by existing handlers and splits it so that
the desktop change is animated by its own handler.

Specifically, deskop immersive exits during an open/front transition
should be split so that the immersive task that is being resized is
animated by the immersive handler, while the opening task remains
animated by the DefaultTransitionHandler.
Similarly, for intent launches with a remote handler, the immersive
change is split and animated by the immersive handler and the rest is
sent to the remote handler to animate.

Flag: com.android.window.flags.enable_fully_immersive_in_desktop
Bug: 372319492
Test: while in desktop immersive, launch an app from the taskbar and see
both of them animate
Test: while in desktop immersive, launch an app from the notification
shade and see both of them animate

Change-Id: Ib63601322df20505dd7c012d85b5705d31dec5a8
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 4c25889..b700a54 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -67,7 +67,7 @@
 import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
 import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler;
-import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
+import com.android.wm.shell.desktopmode.DesktopImmersiveController;
 import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler;
 import com.android.wm.shell.desktopmode.DesktopModeEventLogger;
@@ -397,12 +397,12 @@
             Context context,
             ShellInit shellInit,
             Transitions transitions,
-            Optional<DesktopFullImmersiveTransitionHandler> desktopImmersiveTransitionHandler,
+            Optional<DesktopImmersiveController> desktopImmersiveController,
             WindowDecorViewModel windowDecorViewModel,
             Optional<TaskChangeListener> taskChangeListener,
             FocusTransitionObserver focusTransitionObserver) {
         return new FreeformTaskTransitionObserver(
-                context, shellInit, transitions, desktopImmersiveTransitionHandler,
+                context, shellInit, transitions, desktopImmersiveController,
                 windowDecorViewModel, taskChangeListener, focusTransitionObserver);
     }
 
@@ -638,7 +638,7 @@
             ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler,
             DragToDesktopTransitionHandler dragToDesktopTransitionHandler,
             @DynamicOverride DesktopRepository desktopRepository,
-            Optional<DesktopFullImmersiveTransitionHandler> desktopFullImmersiveTransitionHandler,
+            Optional<DesktopImmersiveController> desktopImmersiveController,
             DesktopModeLoggerTransitionObserver desktopModeLoggerTransitionObserver,
             LaunchAdjacentController launchAdjacentController,
             RecentsTransitionHandler recentsTransitionHandler,
@@ -657,7 +657,7 @@
                 returnToDragStartAnimator, enterDesktopTransitionHandler,
                 exitDesktopTransitionHandler, desktopModeDragAndDropTransitionHandler,
                 toggleResizeDesktopTaskTransitionHandler,
-                dragToDesktopTransitionHandler, desktopFullImmersiveTransitionHandler.get(),
+                dragToDesktopTransitionHandler, desktopImmersiveController.get(),
                 desktopRepository,
                 desktopModeLoggerTransitionObserver, launchAdjacentController,
                 recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter,
@@ -705,7 +705,7 @@
 
     @WMSingleton
     @Provides
-    static Optional<DesktopFullImmersiveTransitionHandler> provideDesktopImmersiveHandler(
+    static Optional<DesktopImmersiveController> provideDesktopImmersiveController(
             Context context,
             Transitions transitions,
             @DynamicOverride DesktopRepository desktopRepository,
@@ -713,7 +713,7 @@
             ShellTaskOrganizer shellTaskOrganizer) {
         if (DesktopModeStatus.canEnterDesktopMode(context)) {
             return Optional.of(
-                    new DesktopFullImmersiveTransitionHandler(
+                    new DesktopImmersiveController(
                             transitions,
                             desktopRepository,
                             displayController,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
similarity index 81%
rename from libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
rename to libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
index 19ffd96..d0bc5f0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt
@@ -36,20 +36,21 @@
 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
 import com.android.wm.shell.transition.Transitions
 import com.android.wm.shell.transition.Transitions.TransitionHandler
+import com.android.wm.shell.transition.Transitions.TransitionObserver
 import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener
 
 /**
- * A [TransitionHandler] to move a task in/out of desktop's full immersive state where the task
+ * A controller to move tasks in/out of desktop's full immersive state where the task
  * remains freeform while being able to take fullscreen bounds and have its App Header visibility
  * be transient below the status bar like in fullscreen immersive mode.
  */
-class DesktopFullImmersiveTransitionHandler(
+class DesktopImmersiveController(
     private val transitions: Transitions,
     private val desktopRepository: DesktopRepository,
     private val displayController: DisplayController,
     private val shellTaskOrganizer: ShellTaskOrganizer,
     private val transactionSupplier: () -> SurfaceControl.Transaction,
-) : TransitionHandler {
+) : TransitionHandler, TransitionObserver {
 
     constructor(
         transitions: Transitions,
@@ -67,7 +68,7 @@
     private var state: TransitionState? = null
 
     @VisibleForTesting
-    val pendingExternalExitTransitions = mutableSetOf<ExternalPendingExit>()
+    val pendingExternalExitTransitions = mutableListOf<ExternalPendingExit>()
 
     /** Whether there is an immersive transition that hasn't completed yet. */
     private val inProgress: Boolean
@@ -184,6 +185,17 @@
         return null
     }
 
+
+    /** Whether the [change] in the [transition] is a known immersive change. */
+    fun isImmersiveChange(
+        transition: IBinder,
+        change: TransitionInfo.Change,
+    ): Boolean {
+        return pendingExternalExitTransitions.any {
+            it.transition == transition && it.taskId == change.taskInfo?.taskId
+        }
+    }
+
     private fun addPendingImmersiveExit(taskId: Int, displayId: Int, transition: IBinder) {
         pendingExternalExitTransitions.add(
             ExternalPendingExit(
@@ -201,10 +213,11 @@
         finishTransaction: SurfaceControl.Transaction,
         finishCallback: Transitions.TransitionFinishCallback
     ): Boolean {
+        logD("startAnimation transition=%s", transition)
         val state = requireState()
         if (transition != state.transition) return false
         animateResize(
-            transitionState = state,
+            targetTaskId = state.taskId,
             info = info,
             startTransaction = startTransaction,
             finishTransaction = finishTransaction,
@@ -214,40 +227,55 @@
     }
 
     private fun animateResize(
-        transitionState: TransitionState,
+        targetTaskId: Int,
         info: TransitionInfo,
         startTransaction: SurfaceControl.Transaction,
         finishTransaction: SurfaceControl.Transaction,
         finishCallback: Transitions.TransitionFinishCallback
     ) {
+        logD("animateResize for task#%d", targetTaskId)
         val change = info.changes.first { c ->
             val taskInfo = c.taskInfo
-            return@first taskInfo != null && taskInfo.taskId == transitionState.taskId
+            return@first taskInfo != null && taskInfo.taskId == targetTaskId
         }
+        animateResizeChange(change, startTransaction, finishTransaction, finishCallback)
+    }
+
+    /**
+     *  Animate an immersive change.
+     *
+     *  As of now, both enter and exit transitions have the same animation, a veiled resize.
+     */
+    fun animateResizeChange(
+        change: TransitionInfo.Change,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+        finishCallback: Transitions.TransitionFinishCallback,
+    ) {
+        val taskId = change.taskInfo!!.taskId
         val leash = change.leash
         val startBounds = change.startAbsBounds
         val endBounds = change.endAbsBounds
+        logD("Animating resize change for task#%d from %s to %s", taskId, startBounds, endBounds)
 
+        startTransaction
+            .setPosition(leash, startBounds.left.toFloat(), startBounds.top.toFloat())
+            .setWindowCrop(leash, startBounds.width(), startBounds.height())
+            .show(leash)
+        onTaskResizeAnimationListener
+            ?.onAnimationStart(taskId, startTransaction, startBounds)
+            ?: startTransaction.apply()
         val updateTransaction = transactionSupplier()
         ValueAnimator.ofObject(rectEvaluator, startBounds, endBounds).apply {
             duration = FULL_IMMERSIVE_ANIM_DURATION_MS
             interpolator = DecelerateInterpolator()
             addListener(
-                onStart = {
-                    startTransaction
-                        .setPosition(leash, startBounds.left.toFloat(), startBounds.top.toFloat())
-                        .setWindowCrop(leash, startBounds.width(), startBounds.height())
-                        .show(leash)
-                    onTaskResizeAnimationListener
-                        ?.onAnimationStart(transitionState.taskId, startTransaction, startBounds)
-                        ?: startTransaction.apply()
-                },
                 onEnd = {
                     finishTransaction
                         .setPosition(leash, endBounds.left.toFloat(), endBounds.top.toFloat())
                         .setWindowCrop(leash, endBounds.width(), endBounds.height())
                         .apply()
-                    onTaskResizeAnimationListener?.onAnimationEnd(transitionState.taskId)
+                    onTaskResizeAnimationListener?.onAnimationEnd(taskId)
                     finishCallback.onTransitionFinished(null /* wct */)
                     clearState()
                 }
@@ -259,7 +287,7 @@
                     .setWindowCrop(leash, rect.width(), rect.height())
                     .apply()
                 onTaskResizeAnimationListener
-                    ?.onBoundsChange(transitionState.taskId, updateTransaction, rect)
+                    ?.onBoundsChange(taskId, updateTransaction, rect)
                     ?: updateTransaction.apply()
             }
             start()
@@ -289,15 +317,20 @@
      * |onTransitionReady|, before this transition actually animates) because drawing decorations
      * depends on whether the task is in full immersive state or not.
      */
-    fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
+    override fun onTransitionReady(
+        transition: IBinder,
+        info: TransitionInfo,
+        startTransaction: SurfaceControl.Transaction,
+        finishTransaction: SurfaceControl.Transaction,
+    ) {
+        logD("onTransitionReady transition=%s", transition)
         // Check if this is a pending external exit transition.
         val pendingExit = pendingExternalExitTransitions
             .firstOrNull { pendingExit -> pendingExit.transition == transition }
         if (pendingExit != null) {
-            pendingExternalExitTransitions.remove(pendingExit)
             if (info.hasTaskChange(taskId = pendingExit.taskId)) {
                 if (desktopRepository.isTaskInFullImmersiveState(pendingExit.taskId)) {
-                    logV("Pending external exit for task ${pendingExit.taskId} verified")
+                    logV("Pending external exit for task#%d verified", pendingExit.taskId)
                     desktopRepository.setTaskInFullImmersiveState(
                         displayId = pendingExit.displayId,
                         taskId = pendingExit.taskId,
@@ -316,7 +349,7 @@
             val state = requireState()
             val startBounds = info.changes.first { c -> c.taskInfo?.taskId == state.taskId }
                 .startAbsBounds
-            logV("Direct move for task ${state.taskId} in ${state.direction} direction verified")
+            logV("Direct move for task#%d in %s direction verified", state.taskId, state.direction)
             when (state.direction) {
                 Direction.ENTER -> {
                     desktopRepository.setTaskInFullImmersiveState(
@@ -348,7 +381,7 @@
             .filter { c -> desktopRepository.isTaskInFullImmersiveState(c.taskInfo!!.taskId) }
             .filter { c -> c.startRotation != c.endRotation }
             .forEach { c ->
-                logV("Detected immersive exit due to rotation for task: ${c.taskInfo!!.taskId}")
+                logV("Detected immersive exit due to rotation for task#%d", c.taskInfo!!.taskId)
                 desktopRepository.setTaskInFullImmersiveState(
                     displayId = c.taskInfo!!.displayId,
                     taskId = c.taskInfo!!.taskId,
@@ -357,6 +390,32 @@
             }
     }
 
+    override fun onTransitionMerged(merged: IBinder, playing: IBinder) {
+        logD("onTransitionMerged merged=%s playing=%s", merged, playing)
+        val pendingExit = pendingExternalExitTransitions
+            .firstOrNull { pendingExit -> pendingExit.transition == merged }
+        if (pendingExit != null) {
+            logV(
+                "Pending exit transition %s for task#%s merged into %s",
+                merged, pendingExit.taskId, playing
+            )
+            pendingExit.transition = playing
+        }
+    }
+
+    override fun onTransitionFinished(transition: IBinder, aborted: Boolean) {
+        logD("onTransitionFinished transition=%s aborted=%b", transition, aborted)
+        val pendingExit = pendingExternalExitTransitions
+            .firstOrNull { pendingExit -> pendingExit.transition == transition }
+        if (pendingExit != null) {
+            logV(
+                "Pending exit transition %s for task#%s finished",
+                transition, pendingExit
+            )
+            pendingExternalExitTransitions.remove(pendingExit)
+        }
+    }
+
     private fun clearState() {
         state = null
     }
@@ -399,7 +458,7 @@
     data class ExternalPendingExit(
         val taskId: Int,
         val displayId: Int,
-        val transition: IBinder,
+        var transition: IBinder,
     )
 
     private enum class Direction {
@@ -410,6 +469,10 @@
         ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
     }
 
+    private fun logD(msg: String, vararg arguments: Any?) {
+        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
+    }
+
     private companion object {
         private const val TAG = "DesktopImmersive"
 
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 18cf1f2..781aee0 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
@@ -48,6 +48,7 @@
 import android.view.KeyEvent
 import android.view.MotionEvent
 import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
 import android.view.WindowManager.TRANSIT_CHANGE
 import android.view.WindowManager.TRANSIT_CLOSE
 import android.view.WindowManager.TRANSIT_NONE
@@ -59,6 +60,7 @@
 import android.window.DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS
 import android.window.RemoteTransition
 import android.window.TransitionInfo
+import android.window.TransitionInfo.Change
 import android.window.TransitionRequestInfo
 import android.window.WindowContainerTransaction
 import androidx.annotation.BinderThread
@@ -115,6 +117,7 @@
 import com.android.wm.shell.transition.FocusTransitionObserver
 import com.android.wm.shell.transition.OneShotRemoteHandler
 import com.android.wm.shell.transition.Transitions
+import com.android.wm.shell.transition.Transitions.TransitionFinishCallback
 import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility
 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator
 import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener
@@ -146,7 +149,7 @@
     private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler,
     private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler,
     private val dragToDesktopTransitionHandler: DragToDesktopTransitionHandler,
-    private val immersiveTransitionHandler: DesktopFullImmersiveTransitionHandler,
+    private val desktopImmersiveController: DesktopImmersiveController,
     private val taskRepository: DesktopRepository,
     private val desktopModeLoggerTransitionObserver: DesktopModeLoggerTransitionObserver,
     private val launchAdjacentController: LaunchAdjacentController,
@@ -252,7 +255,7 @@
         toggleResizeDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
         enterDesktopTaskTransitionHandler.setOnTaskResizeAnimationListener(listener)
         dragToDesktopTransitionHandler.onTaskResizeAnimationListener = listener
-        immersiveTransitionHandler.onTaskResizeAnimationListener = listener
+        desktopImmersiveController.onTaskResizeAnimationListener = listener
     }
 
     fun setOnTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) {
@@ -372,7 +375,7 @@
         // TODO(342378842): Instead of using default display, support multiple displays
         val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(
             DEFAULT_DISPLAY, wct, taskId)
-        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
             wct = wct,
             displayId = DEFAULT_DISPLAY,
             excludeTaskId = taskId,
@@ -403,7 +406,7 @@
         }
         logV("moveRunningTaskToDesktop taskId=%d", task.taskId)
         exitSplitIfApplicable(wct, task)
-        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
             wct = wct,
             displayId = task.displayId,
             excludeTaskId = task.taskId,
@@ -452,7 +455,7 @@
         val taskToMinimize =
             bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId)
         addMoveToDesktopChanges(wct, taskInfo)
-        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
             wct, taskInfo.displayId)
         val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct)
         transition?.let {
@@ -499,7 +502,7 @@
                 taskId
             )
         )
-        return immersiveTransitionHandler.exitImmersiveIfApplicable(wct, taskInfo)
+        return desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo)
     }
 
     fun minimizeTask(taskInfo: RunningTaskInfo) {
@@ -512,7 +515,7 @@
             removeWallpaperActivity(wct)
         }
         // Notify immersive handler as it might need to exit immersive state.
-        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(wct, taskInfo)
+        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo)
 
         wct.reorder(taskInfo.token, false)
         val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct)
@@ -616,7 +619,7 @@
         logV("moveBackgroundTaskToFront taskId=%s", taskId)
         val wct = WindowContainerTransaction()
         // TODO: b/342378842 - Instead of using default display, support multiple displays
-        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
             wct = wct,
             displayId = DEFAULT_DISPLAY,
             excludeTaskId = taskId,
@@ -642,7 +645,7 @@
         logV("moveTaskToFront taskId=%s", taskInfo.taskId)
         val wct = WindowContainerTransaction()
         wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */)
-        val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+        val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
             wct = wct,
             displayId = taskInfo.displayId,
             excludeTaskId = taskInfo.taskId,
@@ -749,12 +752,12 @@
 
     private fun moveDesktopTaskToFullImmersive(taskInfo: RunningTaskInfo) {
         check(taskInfo.isFreeform) { "Task must already be in freeform" }
-        immersiveTransitionHandler.moveTaskToImmersive(taskInfo)
+        desktopImmersiveController.moveTaskToImmersive(taskInfo)
     }
 
     private fun exitDesktopTaskFromFullImmersive(taskInfo: RunningTaskInfo) {
         check(taskInfo.isFreeform) { "Task must already be in freeform" }
-        immersiveTransitionHandler.moveTaskToNonImmersive(taskInfo)
+        desktopImmersiveController.moveTaskToNonImmersive(taskInfo)
     }
 
     /**
@@ -1233,6 +1236,67 @@
         return result
     }
 
+    /** Whether the given [change] in the [transition] is a known desktop change. */
+    fun isDesktopChange(
+        transition: IBinder,
+        change: TransitionInfo.Change,
+    ): Boolean {
+        // Only the immersive controller is currently involved in mixed transitions.
+        return Flags.enableFullyImmersiveInDesktop()
+                && desktopImmersiveController.isImmersiveChange(transition, change)
+    }
+
+    /**
+     * Whether the given transition [info] will potentially include a desktop change, in which
+     * case the transition should be treated as mixed so that the change is in part animated by
+     * one of the desktop transition handlers.
+     */
+    fun shouldPlayDesktopAnimation(info: TransitionRequestInfo): Boolean {
+        // Only immersive mixed transition are currently supported.
+        if (!Flags.enableFullyImmersiveInDesktop()) return false
+        val triggerTask = info.triggerTask ?: return false
+        if (!isDesktopModeShowing(triggerTask.displayId)) {
+            return false
+        }
+        if (!TransitionUtil.isOpeningType(info.type)) {
+            return false
+        }
+        taskRepository.getTaskInFullImmersiveState(displayId = triggerTask.displayId)
+            ?: return false
+        return when {
+            triggerTask.isFullscreen -> {
+                // Trigger fullscreen task will enter desktop, so any existing immersive task
+                // should exit.
+                shouldFullscreenTaskLaunchSwitchToDesktop(triggerTask)
+            }
+            triggerTask.isFreeform -> {
+                // Trigger freeform task will enter desktop, so any existing immersive task should
+                // exit.
+                !shouldFreeformTaskLaunchSwitchToFullscreen(triggerTask)
+            }
+            else -> false
+        }
+    }
+
+    /** Animate a desktop change found in a mixed transitions. */
+    fun animateDesktopChange(
+        transition: IBinder,
+        change: Change,
+        startTransaction: Transaction,
+        finishTransaction: Transaction,
+        finishCallback: TransitionFinishCallback,
+    ) {
+        if (!desktopImmersiveController.isImmersiveChange(transition, change)) {
+            throw IllegalStateException("Only immersive changes support desktop mixed transitions")
+        }
+        desktopImmersiveController.animateResizeChange(
+            change,
+            startTransaction,
+            finishTransaction,
+            finishCallback
+        )
+    }
+
     private fun taskContainsDragAndDropCookie(taskInfo: RunningTaskInfo?) =
         taskInfo?.launchCookies?.any { it == dragAndDropFullscreenCookie } ?: false
 
@@ -1279,7 +1343,7 @@
             wct.startTask(requestedTaskId, options.toBundle())
             val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(
                 callingTask.displayId, wct, requestedTaskId)
-            val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable(
+            val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(
                 wct = wct,
                 displayId = callingTask.displayId,
                 excludeTaskId = requestedTaskId,
@@ -1392,7 +1456,7 @@
             return null
         }
         val wct = WindowContainerTransaction()
-        if (!isDesktopModeShowing(task.displayId)) {
+        if (shouldFreeformTaskLaunchSwitchToFullscreen(task)) {
             logD("Bring desktop tasks to front on transition=taskId=%d", task.taskId)
             if (taskRepository.isActiveTask(task.taskId) && !forceEnterDesktop(task.displayId)) {
                 // We are outside of desktop mode and already existing desktop task is being
@@ -1423,7 +1487,7 @@
         }
         // Desktop Mode is showing and we're launching a new Task:
         // 1) Exit immersive if needed.
-        immersiveTransitionHandler.exitImmersiveIfApplicable(transition, wct, task.displayId)
+        desktopImmersiveController.exitImmersiveIfApplicable(transition, wct, task.displayId)
         // 2) minimize a Task if needed.
         val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
         if (taskToMinimize != null) {
@@ -1438,7 +1502,7 @@
         transition: IBinder
     ): WindowContainerTransaction? {
         logV("handleFullscreenTaskLaunch")
-        if (isDesktopModeShowing(task.displayId) || forceEnterDesktop(task.displayId)) {
+        if (shouldFullscreenTaskLaunchSwitchToDesktop(task)) {
             logD("Switch fullscreen task to freeform on transition: taskId=%d", task.taskId)
             return WindowContainerTransaction().also { wct ->
                 addMoveToDesktopChanges(wct, task)
@@ -1454,7 +1518,7 @@
                 val taskToMinimize =
                     addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task.taskId)
                 addPendingMinimizeTransition(transition, taskToMinimize)
-                immersiveTransitionHandler.exitImmersiveIfApplicable(
+                desktopImmersiveController.exitImmersiveIfApplicable(
                     transition, wct, task.displayId
                 )
             }
@@ -1462,6 +1526,12 @@
         return null
     }
 
+    private fun shouldFreeformTaskLaunchSwitchToFullscreen(task: RunningTaskInfo): Boolean =
+        !isDesktopModeShowing(task.displayId)
+
+    private fun shouldFullscreenTaskLaunchSwitchToDesktop(task: RunningTaskInfo): Boolean =
+        isDesktopModeShowing(task.displayId) || forceEnterDesktop(task.displayId)
+
     /**
      * If a task is not compatible with desktop mode freeform, it should always be launched in
      * fullscreen.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
index 771573d..7631ece 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java
@@ -28,7 +28,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.window.flags.Flags;
-import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
+import com.android.wm.shell.desktopmode.DesktopImmersiveController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.FocusTransitionObserver;
 import com.android.wm.shell.transition.Transitions;
@@ -48,7 +48,7 @@
  */
 public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver {
     private final Transitions mTransitions;
-    private final Optional<DesktopFullImmersiveTransitionHandler> mImmersiveTransitionHandler;
+    private final Optional<DesktopImmersiveController> mDesktopImmersiveController;
     private final WindowDecorViewModel mWindowDecorViewModel;
     private final Optional<TaskChangeListener> mTaskChangeListener;
     private final FocusTransitionObserver mFocusTransitionObserver;
@@ -60,12 +60,12 @@
             Context context,
             ShellInit shellInit,
             Transitions transitions,
-            Optional<DesktopFullImmersiveTransitionHandler> immersiveTransitionHandler,
+            Optional<DesktopImmersiveController> desktopImmersiveController,
             WindowDecorViewModel windowDecorViewModel,
             Optional<TaskChangeListener> taskChangeListener,
             FocusTransitionObserver focusTransitionObserver) {
         mTransitions = transitions;
-        mImmersiveTransitionHandler = immersiveTransitionHandler;
+        mDesktopImmersiveController = desktopImmersiveController;
         mWindowDecorViewModel = windowDecorViewModel;
         mTaskChangeListener = taskChangeListener;
         mFocusTransitionObserver = focusTransitionObserver;
@@ -89,7 +89,8 @@
             // TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository
             //  is updated from there **before** the |mWindowDecorViewModel| methods are invoked.
             //  Otherwise window decoration relayout won't run with the immersive state up to date.
-            mImmersiveTransitionHandler.ifPresent(h -> h.onTransitionReady(transition, info));
+            mDesktopImmersiveController.ifPresent(h ->
+                    h.onTransitionReady(transition, info, startT, finishT));
         }
         // Update focus state first to ensure the correct state can be queried from listeners.
         // TODO(371503964): Remove this once the unified task repository is ready.
@@ -194,10 +195,20 @@
     }
 
     @Override
-    public void onTransitionStarting(@NonNull IBinder transition) {}
+    public void onTransitionStarting(@NonNull IBinder transition) {
+        if (Flags.enableFullyImmersiveInDesktop()) {
+            // TODO(b/367268953): Remove when DesktopTaskListener is introduced.
+            mDesktopImmersiveController.ifPresent(h -> h.onTransitionStarting(transition));
+        }
+    }
 
     @Override
     public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) {
+        if (Flags.enableFullyImmersiveInDesktop()) {
+            // TODO(b/367268953): Remove when DesktopTaskListener is introduced.
+            mDesktopImmersiveController.ifPresent(h -> h.onTransitionMerged(merged, playing));
+        }
+
         final List<ActivityManager.RunningTaskInfo> infoOfMerged =
                 mTransitionToTaskInfo.get(merged);
         if (infoOfMerged == null) {
@@ -218,6 +229,11 @@
 
     @Override
     public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) {
+        if (Flags.enableFullyImmersiveInDesktop()) {
+            // TODO(b/367268953): Remove when DesktopTaskListener is introduced.
+            mDesktopImmersiveController.ifPresent(h -> h.onTransitionFinished(transition, aborted));
+        }
+
         final List<ActivityManager.RunningTaskInfo> taskInfo =
                 mTransitionToTaskInfo.getOrDefault(transition, Collections.emptyList());
         mTransitionToTaskInfo.remove(transition);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 766a6b3..0d89f75 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -83,8 +83,11 @@
         /** Both the display and split-state (enter/exit) is changing */
         static final int TYPE_DISPLAY_AND_SPLIT_CHANGE = 2;
 
-        /** Pip was entered while handling an intent with its own remoteTransition. */
-        static final int TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE = 3;
+        /**
+         * While handling an intent with its own remoteTransition, a PIP enter or Desktop immersive
+         * exit change is found.
+         */
+        static final int TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE = 3;
 
         /** Recents transition while split-screen foreground. */
         static final int TYPE_RECENTS_DURING_SPLIT = 4;
@@ -110,6 +113,9 @@
         /** The display changes when pip is entering. */
         static final int TYPE_ENTER_PIP_WITH_DISPLAY_CHANGE = 11;
 
+        /** Open transition during a desktop session. */
+        static final int TYPE_OPEN_IN_DESKTOP = 12;
+
         /** The default animation for this mixed transition. */
         static final int ANIM_TYPE_DEFAULT = 0;
 
@@ -296,7 +302,7 @@
                 return null;
             }
             final MixedTransition mixed = createDefaultMixedTransition(
-                    MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE, transition);
+                    MixedTransition.TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE, transition);
             mixed.mLeftoversHandler = handler.first;
             mActiveTransitions.add(mixed);
             if (mixed.mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
@@ -334,6 +340,20 @@
                         MixedTransition.TYPE_UNFOLD, transition));
             }
             return wct;
+        } else if (mDesktopTasksController != null
+                && mDesktopTasksController.shouldPlayDesktopAnimation(request)) {
+            final Pair<Transitions.TransitionHandler, WindowContainerTransaction> handler =
+                    mPlayer.dispatchRequest(transition, request, /* skip= */ this);
+            if (handler == null) {
+                return null;
+            }
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a desktop request, so"
+                    + " treat it as Mixed. handler=%s", handler.first);
+            final MixedTransition mixed = createDefaultMixedTransition(
+                    MixedTransition.TYPE_OPEN_IN_DESKTOP, transition);
+            mixed.mLeftoversHandler = handler.first;
+            mActiveTransitions.add(mixed);
+            return handler.second;
         }
         return null;
     }
@@ -341,7 +361,7 @@
     private DefaultMixedTransition createDefaultMixedTransition(int type, IBinder transition) {
         return new DefaultMixedTransition(
                 type, transition, mPlayer, this, mPipHandler, mSplitHandler, mKeyguardHandler,
-                mUnfoldHandler, mActivityEmbeddingController);
+                mUnfoldHandler, mActivityEmbeddingController, mDesktopTasksController);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
index c8921d2..3d3de88 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java
@@ -30,6 +30,7 @@
 
 import com.android.internal.protolog.ProtoLog;
 import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
+import com.android.wm.shell.desktopmode.DesktopTasksController;
 import com.android.wm.shell.keyguard.KeyguardTransitionHandler;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
@@ -39,15 +40,19 @@
 class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition {
     private final UnfoldTransitionHandler mUnfoldHandler;
     private final ActivityEmbeddingController mActivityEmbeddingController;
+    @Nullable
+    private final DesktopTasksController mDesktopTasksController;
 
     DefaultMixedTransition(int type, IBinder transition, Transitions player,
             MixedTransitionHandler mixedHandler, PipTransitionController pipHandler,
             StageCoordinator splitHandler, KeyguardTransitionHandler keyguardHandler,
             UnfoldTransitionHandler unfoldHandler,
-            ActivityEmbeddingController activityEmbeddingController) {
+            ActivityEmbeddingController activityEmbeddingController,
+            @Nullable DesktopTasksController desktopTasksController) {
         super(type, transition, player, mixedHandler, pipHandler, splitHandler, keyguardHandler);
         mUnfoldHandler = unfoldHandler;
         mActivityEmbeddingController = activityEmbeddingController;
+        mDesktopTasksController = desktopTasksController;
 
         switch (type) {
             case TYPE_UNFOLD:
@@ -57,7 +62,8 @@
             case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING:
             case TYPE_ENTER_PIP_FROM_SPLIT:
             case TYPE_KEYGUARD:
-            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+            case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE:
+            case TYPE_OPEN_IN_DESKTOP:
             default:
                 break;
         }
@@ -85,11 +91,14 @@
             case TYPE_KEYGUARD ->
                     animateKeyguard(this, info, startTransaction, finishTransaction, finishCallback,
                             mKeyguardHandler, mPipHandler);
-            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE ->
-                    animateOpenIntentWithRemoteAndPip(transition, info, startTransaction,
+            case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE ->
+                    animateOpenIntentWithRemoteAndPipOrDesktop(transition, info, startTransaction,
                             finishTransaction, finishCallback);
             case TYPE_UNFOLD ->
                     animateUnfold(info, startTransaction, finishTransaction, finishCallback);
+            case TYPE_OPEN_IN_DESKTOP ->
+                    animateOpenInDesktop(
+                            transition, info, startTransaction, finishTransaction, finishCallback);
             default -> throw new IllegalStateException(
                     "Starting default mixed animation with unknown or illegal type: " + mType);
         };
@@ -146,31 +155,34 @@
         return true;
     }
 
-    private boolean animateOpenIntentWithRemoteAndPip(
+    private boolean animateOpenIntentWithRemoteAndPipOrDesktop(
             @NonNull IBinder transition, @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for opening an intent"
-                + " with a remote transition and PIP #%d", info.getDebugId());
-        boolean handledToPip = tryAnimateOpenIntentWithRemoteAndPip(
+                + " with a remote transition and PIP or Desktop #%d", info.getDebugId());
+        boolean handledToPipOrDesktop = tryAnimateOpenIntentWithRemoteAndPipOrDesktop(
                 info, startTransaction, finishTransaction, finishCallback);
         // Consume the transition on remote handler if the leftover handler already handle this
         // transition. And if it cannot, the transition will be handled by remote handler, so don't
         // consume here.
-        // Need to check leftOverHandler as it may change in #animateOpenIntentWithRemoteAndPip
-        if (handledToPip && mHasRequestToRemote
+        // Need to check leftOverHandler as it may change in
+        // #animateOpenIntentWithRemoteAndPipOrDesktop
+        if (handledToPipOrDesktop && mHasRequestToRemote
                 && mLeftoversHandler != mPlayer.getRemoteTransitionHandler()) {
             mPlayer.getRemoteTransitionHandler().onTransitionConsumed(transition, false, null);
         }
-        return handledToPip;
+        return handledToPipOrDesktop;
     }
 
-    private boolean tryAnimateOpenIntentWithRemoteAndPip(
+    private boolean tryAnimateOpenIntentWithRemoteAndPipOrDesktop(
             @NonNull TransitionInfo info,
             @NonNull SurfaceControl.Transaction startTransaction,
             @NonNull SurfaceControl.Transaction finishTransaction,
             @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS,
+                "tryAnimateOpenIntentWithRemoteAndPipOrDesktop");
         TransitionInfo.Change pipChange = null;
         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
             TransitionInfo.Change change = info.getChanges().get(i);
@@ -183,13 +195,31 @@
                 info.getChanges().remove(i);
             }
         }
+        TransitionInfo.Change desktopChange = null;
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (mDesktopTasksController != null
+                    && mDesktopTasksController.isDesktopChange(mTransition, change)) {
+                if (desktopChange != null) {
+                    throw new IllegalStateException("More than 1 desktop changes in one"
+                            + " transition? " + info);
+                }
+                desktopChange = change;
+                info.getChanges().remove(i);
+            }
+        }
         Transitions.TransitionFinishCallback finishCB = (wct) -> {
             --mInFlightSubAnimations;
             joinFinishArgs(wct);
             if (mInFlightSubAnimations > 0) return;
             finishCallback.onTransitionFinished(mFinishWCT);
         };
-        if (pipChange == null) {
+        if ((pipChange == null && desktopChange == null)
+                || (pipChange != null && desktopChange != null)) {
+            // Don't split the transition. Let the leftovers handler handle it all.
+            // TODO: b/? - split the transition into three pieces when there's both a PIP and a
+            //  desktop change are present. For example, during remote intent open over a desktop
+            //  with both a PIP capable task and an immersive task.
             if (mLeftoversHandler != null) {
                 mInFlightSubAnimations = 1;
                 if (mLeftoversHandler.startAnimation(
@@ -198,27 +228,52 @@
                 }
             }
             return false;
-        }
-        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting PIP into a separate"
-                + " animation because remote-animation likely doesn't support it #%d",
-                info.getDebugId());
-        // Split the transition into 2 parts: the pip part and the rest.
-        mInFlightSubAnimations = 2;
-        // make a new startTransaction because pip's startEnterAnimation "consumes" it so
-        // we need a separate one to send over to launcher.
-        SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+        } else if (pipChange != null && desktopChange == null) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting PIP into a separate"
+                            + " animation because remote-animation likely doesn't support it #%d",
+                    info.getDebugId());
+            // Split the transition into 2 parts: the pip part and the rest.
+            mInFlightSubAnimations = 2;
+            // make a new startTransaction because pip's startEnterAnimation "consumes" it so
+            // we need a separate one to send over to launcher.
+            SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
 
-        mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
+            mPipHandler.startEnterAnimation(pipChange, otherStartT, finishTransaction, finishCB);
 
-        // Dispatch the rest of the transition normally.
-        if (mLeftoversHandler != null
-                && mLeftoversHandler.startAnimation(mTransition, info,
-                startTransaction, finishTransaction, finishCB)) {
+            // Dispatch the rest of the transition normally.
+            if (mLeftoversHandler != null
+                    && mLeftoversHandler.startAnimation(mTransition, info,
+                    startTransaction, finishTransaction, finishCB)) {
+                return true;
+            }
+            mLeftoversHandler = mPlayer.dispatchTransition(
+                    mTransition, info, startTransaction, finishTransaction, finishCB,
+                    mMixedHandler);
             return true;
+        } else if (pipChange == null && desktopChange != null) {
+            ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting desktop change into a"
+                            + "separate animation because remote-animation likely doesn't support"
+                            + "it #%d", info.getDebugId());
+            mInFlightSubAnimations = 2;
+            SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
+
+            mDesktopTasksController.animateDesktopChange(
+                            mTransition, desktopChange, otherStartT, finishTransaction, finishCB);
+
+            // Dispatch the rest of the transition normally.
+            if (mLeftoversHandler != null
+                    && mLeftoversHandler.startAnimation(mTransition, info,
+                    startTransaction, finishTransaction, finishCB)) {
+                return true;
+            }
+            mLeftoversHandler = mPlayer.dispatchTransition(
+                    mTransition, info, startTransaction, finishTransaction, finishCB,
+                    mMixedHandler);
+            return true;
+        } else {
+            throw new IllegalStateException(
+                    "All PIP and Immersive combinations should've been handled");
         }
-        mLeftoversHandler = mPlayer.dispatchTransition(
-                mTransition, info, startTransaction, finishTransaction, finishCB, mMixedHandler);
-        return true;
     }
 
     private boolean animateUnfold(
@@ -246,6 +301,51 @@
                 mTransition, info, startTransaction, finishTransaction, finishCB);
     }
 
+    private boolean animateOpenInDesktop(
+            @NonNull IBinder transition,
+            @NonNull TransitionInfo info,
+            @NonNull SurfaceControl.Transaction startTransaction,
+            @NonNull SurfaceControl.Transaction finishTransaction,
+            @NonNull Transitions.TransitionFinishCallback finishCallback) {
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "animateOpenInDesktop");
+        TransitionInfo.Change desktopChange = null;
+        for (int i = info.getChanges().size() - 1; i >= 0; --i) {
+            TransitionInfo.Change change = info.getChanges().get(i);
+            if (mDesktopTasksController.isDesktopChange(mTransition, change)) {
+                if (desktopChange != null) {
+                    throw new IllegalStateException("More than 1 desktop changes in one"
+                            + " transition? " + info);
+                }
+                desktopChange = change;
+                info.getChanges().remove(i);
+            }
+        }
+        final Transitions.TransitionFinishCallback finishCB = (wct) -> {
+            --mInFlightSubAnimations;
+            joinFinishArgs(wct);
+            if (mInFlightSubAnimations > 0) return;
+            finishCallback.onTransitionFinished(mFinishWCT);
+        };
+        if (desktopChange == null) {
+            if (mLeftoversHandler != null) {
+                mInFlightSubAnimations = 1;
+                if (mLeftoversHandler.startAnimation(
+                        mTransition, info, startTransaction, finishTransaction, finishCB)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Splitting desktop change into a"
+                + "separate animation #%d", info.getDebugId());
+        mInFlightSubAnimations = 2;
+        mDesktopTasksController.animateDesktopChange(
+                transition, desktopChange, startTransaction, finishTransaction, finishCB);
+        mLeftoversHandler = mPlayer.dispatchTransition(
+                mTransition, info, startTransaction, finishTransaction, finishCB, mMixedHandler);
+        return true;
+    }
+
     @Override
     void mergeAnimation(
             @NonNull IBinder transition, @NonNull TransitionInfo info,
@@ -279,7 +379,7 @@
             case TYPE_KEYGUARD:
                 mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
                 return;
-            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+            case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE:
                 mPipHandler.end();
                 if (mLeftoversHandler != null) {
                     mLeftoversHandler.mergeAnimation(
@@ -289,6 +389,10 @@
             case TYPE_UNFOLD:
                 mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback);
                 return;
+            case TYPE_OPEN_IN_DESKTOP:
+                mDesktopTasksController.mergeAnimation(
+                        transition, info, t, mergeTarget, finishCallback);
+                return;
             default:
                 throw new IllegalStateException("Playing a default mixed transition with unknown or"
                         + " illegal type: " + mType);
@@ -310,12 +414,14 @@
             case TYPE_KEYGUARD:
                 mKeyguardHandler.onTransitionConsumed(transition, aborted, finishT);
                 break;
-            case TYPE_OPTIONS_REMOTE_AND_PIP_CHANGE:
+            case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE:
                 mLeftoversHandler.onTransitionConsumed(transition, aborted, finishT);
                 break;
             case TYPE_UNFOLD:
                 mUnfoldHandler.onTransitionConsumed(transition, aborted, finishT);
                 break;
+            case TYPE_OPEN_IN_DESKTOP:
+                mDesktopTasksController.onTransitionConsumed(transition, aborted, finishT);
             default:
                 break;
         }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
similarity index 72%
rename from libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
rename to libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
index 5842dfa..e83f5c7 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopFullImmersiveTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt
@@ -15,6 +15,7 @@
  */
 package com.android.wm.shell.desktopmode
 
+import android.app.ActivityManager.RunningTaskInfo
 import android.app.WindowConfiguration.WINDOW_CONFIG_BOUNDS
 import android.graphics.Rect
 import android.os.Binder
@@ -58,13 +59,13 @@
 import org.mockito.kotlin.whenever
 
 /**
- * Tests for [DesktopFullImmersiveTransitionHandler].
+ * Tests for [DesktopImmersiveController].
  *
- * Usage: atest WMShellUnitTests:DesktopFullImmersiveTransitionHandlerTest
+ * Usage: atest WMShellUnitTests:DesktopImmersiveControllerTest
  */
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
-class DesktopFullImmersiveTransitionHandlerTest : ShellTestCase() {
+class DesktopImmersiveControllerTest : ShellTestCase() {
 
     @JvmField @Rule val setFlagsRule = SetFlagsRule()
 
@@ -75,7 +76,7 @@
     @Mock private lateinit var mockDisplayLayout: DisplayLayout
     private val transactionSupplier = { SurfaceControl.Transaction() }
 
-    private lateinit var immersiveHandler: DesktopFullImmersiveTransitionHandler
+    private lateinit var controller: DesktopImmersiveController
 
     @Before
     fun setUp() {
@@ -87,7 +88,7 @@
         whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { invocation ->
             (invocation.getArgument(0) as Rect).set(STABLE_BOUNDS)
         }
-        immersiveHandler = DesktopFullImmersiveTransitionHandler(
+        controller = DesktopImmersiveController(
             transitions = mockTransitions,
             desktopRepository = desktopRepository,
             displayController = mockDisplayController,
@@ -100,7 +101,7 @@
     fun enterImmersive_transitionReady_updatesRepository() {
         val task = createFreeformTask()
         val mockBinder = mock(IBinder::class.java)
-        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
@@ -108,16 +109,14 @@
             immersive = false
         )
 
-        immersiveHandler.moveTaskToImmersive(task)
-        immersiveHandler.onTransitionReady(
+        controller.moveTaskToImmersive(task)
+        controller.onTransitionReady(
             transition = mockBinder,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply {
-                        taskInfo = task
-                    }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isTrue()
@@ -128,7 +127,7 @@
     fun enterImmersive_savesPreImmersiveBounds() {
         val task = createFreeformTask()
         val mockBinder = mock(IBinder::class.java)
-        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
@@ -137,16 +136,14 @@
         )
         assertThat(desktopRepository.removeBoundsBeforeFullImmersive(task.taskId)).isNull()
 
-        immersiveHandler.moveTaskToImmersive(task)
-        immersiveHandler.onTransitionReady(
+        controller.moveTaskToImmersive(task)
+        controller.onTransitionReady(
             transition = mockBinder,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply {
-                        taskInfo = task
-                    }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.removeBoundsBeforeFullImmersive(task.taskId)).isNotNull()
@@ -156,7 +153,7 @@
     fun exitImmersive_transitionReady_updatesRepository() {
         val task = createFreeformTask()
         val mockBinder = mock(IBinder::class.java)
-        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
@@ -164,16 +161,14 @@
             immersive = true
         )
 
-        immersiveHandler.moveTaskToNonImmersive(task)
-        immersiveHandler.onTransitionReady(
+        controller.moveTaskToNonImmersive(task)
+        controller.onTransitionReady(
             transition = mockBinder,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply {
-                        taskInfo = task
-                    }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
@@ -184,7 +179,7 @@
     fun exitImmersive_onTransitionReady_removesBoundsBeforeImmersive() {
         val task = createFreeformTask()
         val mockBinder = mock(IBinder::class.java)
-        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
         desktopRepository.setTaskInFullImmersiveState(
             displayId = task.displayId,
@@ -193,16 +188,14 @@
         )
         desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, Rect(100, 100, 600, 600))
 
-        immersiveHandler.moveTaskToNonImmersive(task)
-        immersiveHandler.onTransitionReady(
+        controller.moveTaskToNonImmersive(task)
+        controller.onTransitionReady(
             transition = mockBinder,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply {
-                        taskInfo = task
-                    }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
@@ -217,16 +210,15 @@
             immersive = true
         )
 
-        immersiveHandler.onTransitionReady(
+        controller.onTransitionReady(
             transition = mock(IBinder::class.java),
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply {
-                        taskInfo = task
-                        setRotation(/* start= */ Surface.ROTATION_0, /* end= */ Surface.ROTATION_90)
-                    }
-                )
-            )
+                changes = listOf(createChange(task).apply {
+                    setRotation(/* start= */ Surface.ROTATION_0, /* end= */ Surface.ROTATION_90)
+                })
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
@@ -236,28 +228,28 @@
     fun enterImmersive_inProgress_ignores() {
         val task = createFreeformTask()
         val mockBinder = mock(IBinder::class.java)
-        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
 
-        immersiveHandler.moveTaskToImmersive(task)
-        immersiveHandler.moveTaskToImmersive(task)
+        controller.moveTaskToImmersive(task)
+        controller.moveTaskToImmersive(task)
 
         verify(mockTransitions, times(1))
-            .startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))
+            .startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))
     }
 
     @Test
     fun exitImmersive_inProgress_ignores() {
         val task = createFreeformTask()
         val mockBinder = mock(IBinder::class.java)
-        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler)))
+        whenever(mockTransitions.startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)))
             .thenReturn(mockBinder)
 
-        immersiveHandler.moveTaskToNonImmersive(task)
-        immersiveHandler.moveTaskToNonImmersive(task)
+        controller.moveTaskToNonImmersive(task)
+        controller.moveTaskToNonImmersive(task)
 
         verify(mockTransitions, times(1))
-            .startTransition(eq(TRANSIT_CHANGE), any(), eq(immersiveHandler))
+            .startTransition(eq(TRANSIT_CHANGE), any(), eq(controller))
     }
 
     @Test
@@ -273,9 +265,9 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
-        assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
                     && exit.taskId == task.taskId
         }).isTrue()
@@ -294,9 +286,9 @@
             immersive = false
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
-        assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
                     && exit.taskId == task.taskId
         }).isFalse()
@@ -315,7 +307,7 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
         assertThat(wct.hasBoundsChange(task.token)).isTrue()
     }
@@ -333,7 +325,7 @@
             immersive = false
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
         assertThat(wct.hasBoundsChange(task.token)).isFalse()
     }
@@ -351,13 +343,13 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(
+        controller.exitImmersiveIfApplicable(
             wct = wct,
             displayId = DEFAULT_DISPLAY,
             excludeTaskId = task.taskId
         )?.invoke(transition)
 
-        assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
                     && exit.taskId == task.taskId
         }).isFalse()
@@ -375,7 +367,7 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
 
         assertThat(wct.hasBoundsChange(task.token)).isTrue()
     }
@@ -392,7 +384,7 @@
             immersive = false
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(wct, task)
+        controller.exitImmersiveIfApplicable(wct, task)
 
         assertThat(wct.hasBoundsChange(task.token)).isFalse()
     }
@@ -410,9 +402,9 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(wct, task)?.invoke(transition)
+        controller.exitImmersiveIfApplicable(wct, task)?.invoke(transition)
 
-        assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
                     && exit.taskId == task.taskId
         }).isTrue()
@@ -431,9 +423,9 @@
             immersive = false
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(wct, task)?.invoke(transition)
+        controller.exitImmersiveIfApplicable(wct, task)?.invoke(transition)
 
-        assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
                     && exit.taskId == task.taskId
         }).isFalse()
@@ -441,7 +433,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-    fun onTransitionReady_pendingExit_removesPendingExit() {
+    fun onTransitionReady_pendingExit_removesPendingExitOnFinish() {
         val task = createFreeformTask()
         whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
         val wct = WindowContainerTransaction()
@@ -451,18 +443,19 @@
             taskId = task.taskId,
             immersive = true
         )
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
-        immersiveHandler.onTransitionReady(
+        controller.onTransitionReady(
             transition = transition,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
+        controller.onTransitionFinished(transition, aborted = false)
 
-        assertThat(immersiveHandler.pendingExternalExitTransitions.any { exit ->
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
             exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
                     && exit.taskId == task.taskId
         }).isFalse()
@@ -470,6 +463,42 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun onTransitionReady_pendingExit_withMerge_removesPendingExitOnFinish() {
+        val task = createFreeformTask()
+        whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+        val wct = WindowContainerTransaction()
+        val transition = Binder()
+        val mergedToTransition = Binder()
+        desktopRepository.setTaskInFullImmersiveState(
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+            immersive = true
+        )
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+        controller.onTransitionReady(
+            transition = transition,
+            info = createTransitionInfo(
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
+        )
+        controller.onTransitionMerged(transition, mergedToTransition)
+        controller.onTransitionFinished(mergedToTransition, aborted = false)
+
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
+            exit.transition == transition && exit.displayId == DEFAULT_DISPLAY
+                    && exit.taskId == task.taskId
+        }).isFalse()
+        assertThat(controller.pendingExternalExitTransitions.any { exit ->
+            exit.transition == mergedToTransition && exit.displayId == DEFAULT_DISPLAY
+                    && exit.taskId == task.taskId
+        }).isFalse()
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
     fun onTransitionReady_pendingExit_updatesRepository() {
         val task = createFreeformTask()
         whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
@@ -480,15 +509,15 @@
             taskId = task.taskId,
             immersive = true
         )
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
-        immersiveHandler.onTransitionReady(
+        controller.onTransitionReady(
             transition = transition,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.isTaskInFullImmersiveState(task.taskId)).isFalse()
@@ -510,15 +539,15 @@
             immersive = true
         )
         desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, Rect(100, 100, 600, 600))
-        immersiveHandler.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
 
-        immersiveHandler.onTransitionReady(
+        controller.onTransitionReady(
             transition = transition,
             info = createTransitionInfo(
-                changes = listOf(
-                    TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }
-                )
-            )
+                changes = listOf(createChange(task))
+            ),
+            startTransaction = SurfaceControl.Transaction(),
+            finishTransaction = SurfaceControl.Transaction(),
         )
 
         assertThat(desktopRepository.removeBoundsBeforeMaximize(task.taskId)).isNull()
@@ -537,7 +566,7 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
 
         assertThat(
             wct.hasBoundsChange(task.token, calculateMaximizeBounds(mockDisplayLayout, task))
@@ -561,7 +590,7 @@
         val preImmersiveBounds = Rect(100, 100, 500, 500)
         desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, preImmersiveBounds)
 
-        immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
 
         assertThat(
             wct.hasBoundsChange(task.token, preImmersiveBounds)
@@ -584,7 +613,7 @@
             immersive = true
         )
 
-        immersiveHandler.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
+        controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task)
 
         assertThat(
             wct.hasBoundsChange(task.token, calculateInitialBounds(mockDisplayLayout, task))
@@ -602,13 +631,32 @@
             taskId = task.taskId,
             immersive = true
         )
-        immersiveHandler.exitImmersiveIfApplicable(wct, task)?.invoke(Binder())
+        controller.exitImmersiveIfApplicable(wct, task)?.invoke(Binder())
 
-        immersiveHandler.moveTaskToNonImmersive(task)
+        controller.moveTaskToNonImmersive(task)
 
         verify(mockTransitions, never()).startTransition(any(), any(), any())
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    fun exitImmersiveIfApplicable_inImmersive_isImmersiveChange() {
+        val task = createFreeformTask()
+        whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)
+        val wct = WindowContainerTransaction()
+        val transition = Binder()
+        val change = createChange(task)
+        desktopRepository.setTaskInFullImmersiveState(
+            displayId = DEFAULT_DISPLAY,
+            taskId = task.taskId,
+            immersive = true
+        )
+
+        controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY)
+
+        assertThat(controller.isImmersiveChange(transition, change)).isTrue()
+    }
+
     private fun createTransitionInfo(
         @TransitionType type: Int = TRANSIT_CHANGE,
         @TransitionFlags flags: Int = 0,
@@ -617,6 +665,11 @@
         changes.forEach { change -> addChange(change) }
     }
 
+    private fun createChange(task: RunningTaskInfo): TransitionInfo.Change =
+        TransitionInfo.Change(task.token, SurfaceControl()).apply {
+            taskInfo = task
+        }
+
     private fun WindowContainerTransaction.hasBoundsChange(token: WindowContainerToken): Boolean =
         this.changes.any { change ->
             change.key == token.asBinder()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
index 7a3d44b..bc2b36c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt
@@ -201,7 +201,7 @@
   lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler
   @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler
   @Mock
-  lateinit var mockDesktopFullImmersiveTransitionHandler: DesktopFullImmersiveTransitionHandler
+  lateinit var mMockDesktopImmersiveController: DesktopImmersiveController
   @Mock lateinit var launchAdjacentController: LaunchAdjacentController
   @Mock lateinit var splitScreenController: SplitScreenController
   @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler
@@ -322,7 +322,7 @@
         dragAndDropTransitionHandler,
         toggleResizeDesktopTaskTransitionHandler,
         dragToDesktopTransitionHandler,
-        mockDesktopFullImmersiveTransitionHandler,
+        mMockDesktopImmersiveController,
         taskRepository,
         desktopModeLoggerTransitionObserver,
         launchAdjacentController,
@@ -1773,7 +1773,7 @@
 
     controller.minimizeTask(task)
 
-    verify(mockDesktopFullImmersiveTransitionHandler).exitImmersiveIfApplicable(any(), eq(task))
+    verify(mMockDesktopImmersiveController).exitImmersiveIfApplicable(any(), eq(task))
   }
 
   @Test
@@ -1783,7 +1783,7 @@
     val runOnTransit = RunOnStartTransitionCallback()
     whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any()))
       .thenReturn(transition)
-    whenever(mockDesktopFullImmersiveTransitionHandler.exitImmersiveIfApplicable(any(), eq(task)))
+    whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task)))
       .thenReturn(runOnTransit)
 
     controller.minimizeTask(task)
@@ -3092,13 +3092,13 @@
     val transition = Binder()
     whenever(transitions.startTransition(eq(TRANSIT_OPEN), any(), anyOrNull()))
       .thenReturn(transition)
-    whenever(mockDesktopFullImmersiveTransitionHandler
+    whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId)))
       .thenReturn(runOnStartTransit)
 
     runOpenInstance(immersiveTask, freeformTask.taskId)
 
-    verify(mockDesktopFullImmersiveTransitionHandler)
+    verify(mMockDesktopImmersiveController)
       .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId))
     runOnStartTransit.assertOnlyInvocation(transition)
   }
@@ -3446,7 +3446,7 @@
 
     controller.toggleDesktopTaskFullImmersiveState(task)
 
-    verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToImmersive(task)
+    verify(mMockDesktopImmersiveController).moveTaskToImmersive(task)
   }
 
   @Test
@@ -3456,7 +3456,7 @@
 
     controller.toggleDesktopTaskFullImmersiveState(task)
 
-    verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToNonImmersive(task)
+    verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(task)
   }
 
   @Test
@@ -3468,7 +3468,7 @@
     task.requestedVisibleTypes = WindowInsets.Type.statusBars()
     controller.onTaskInfoChanged(task)
 
-    verify(mockDesktopFullImmersiveTransitionHandler).moveTaskToNonImmersive(task)
+    verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(task)
   }
 
   @Test
@@ -3480,7 +3480,7 @@
     task.requestedVisibleTypes = WindowInsets.Type.statusBars()
     controller.onTaskInfoChanged(task)
 
-    verify(mockDesktopFullImmersiveTransitionHandler, never()).moveTaskToNonImmersive(task)
+    verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(task)
   }
 
   @Test
@@ -3489,13 +3489,13 @@
     val wct = WindowContainerTransaction()
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
-    whenever(mockDesktopFullImmersiveTransitionHandler
+    whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)).thenReturn(runOnStartTransit)
     whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
 
     controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
 
-    verify(mockDesktopFullImmersiveTransitionHandler)
+    verify(mMockDesktopImmersiveController)
       .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)
     runOnStartTransit.assertOnlyInvocation(transition)
   }
@@ -3506,13 +3506,13 @@
     val wct = WindowContainerTransaction()
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
-    whenever(mockDesktopFullImmersiveTransitionHandler
+    whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)).thenReturn(runOnStartTransit)
     whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition)
 
     controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN)
 
-    verify(mockDesktopFullImmersiveTransitionHandler)
+    verify(mMockDesktopImmersiveController)
       .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)
     runOnStartTransit.assertOnlyInvocation(transition)
   }
@@ -3522,14 +3522,14 @@
     val task = setUpFreeformTask(background = true)
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
-    whenever(mockDesktopFullImmersiveTransitionHandler
+    whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)))
       .thenReturn(runOnStartTransit)
     whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
 
     controller.moveTaskToFront(task.taskId, remoteTransition = null)
 
-    verify(mockDesktopFullImmersiveTransitionHandler)
+    verify(mMockDesktopImmersiveController)
       .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId))
     runOnStartTransit.assertOnlyInvocation(transition)
   }
@@ -3539,14 +3539,14 @@
     val task = setUpFreeformTask(background = false)
     val runOnStartTransit = RunOnStartTransitionCallback()
     val transition = Binder()
-    whenever(mockDesktopFullImmersiveTransitionHandler
+    whenever(mMockDesktopImmersiveController
       .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId)))
       .thenReturn(runOnStartTransit)
     whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition)
 
     controller.moveTaskToFront(task.taskId, remoteTransition = null)
 
-    verify(mockDesktopFullImmersiveTransitionHandler)
+    verify(mMockDesktopImmersiveController)
       .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId))
     runOnStartTransit.assertOnlyInvocation(transition)
   }
@@ -3560,7 +3560,7 @@
 
     controller.handleRequest(binder, createTransition(task))
 
-    verify(mockDesktopFullImmersiveTransitionHandler)
+    verify(mMockDesktopImmersiveController)
       .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId))
   }
 
@@ -3572,10 +3572,117 @@
 
     controller.handleRequest(binder, createTransition(task))
 
-    verify(mockDesktopFullImmersiveTransitionHandler)
+    verify(mMockDesktopImmersiveController)
       .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId))
   }
 
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() {
+    val triggerTask = setUpFullscreenTask(displayId = 5)
+    taskRepository.setTaskInFullImmersiveState(
+      displayId = triggerTask.displayId,
+      taskId = triggerTask.taskId,
+      immersive = true
+    )
+
+    assertThat(controller.shouldPlayDesktopAnimation(
+      TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+    )).isFalse()
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_notOpening_doesNotPlay() {
+    val triggerTask = setUpFreeformTask(displayId = 5)
+    taskRepository.setTaskInFullImmersiveState(
+      displayId = triggerTask.displayId,
+      taskId = triggerTask.taskId,
+      immersive = true
+    )
+
+    assertThat(controller.shouldPlayDesktopAnimation(
+      TransitionRequestInfo(TRANSIT_CHANGE, triggerTask, /* remoteTransition= */ null)
+    )).isFalse()
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_notImmersive_doesNotPlay() {
+    val triggerTask = setUpFreeformTask(displayId = 5)
+    taskRepository.setTaskInFullImmersiveState(
+      displayId = triggerTask.displayId,
+      taskId = triggerTask.taskId,
+      immersive = false
+    )
+
+    assertThat(controller.shouldPlayDesktopAnimation(
+      TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+    )).isFalse()
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_fullscreenEntersDesktop_plays() {
+    // At least one freeform task to be in a desktop.
+    val existingTask = setUpFreeformTask(displayId = 5)
+    val triggerTask = setUpFullscreenTask(displayId = 5)
+    assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue()
+    taskRepository.setTaskInFullImmersiveState(
+      displayId = existingTask.displayId,
+      taskId = existingTask.taskId,
+      immersive = true
+    )
+
+    assertThat(
+      controller.shouldPlayDesktopAnimation(
+        TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+      )
+    ).isTrue()
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() {
+    val triggerTask = setUpFullscreenTask(displayId = 5)
+    assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
+
+    assertThat(controller.shouldPlayDesktopAnimation(
+      TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+    )).isFalse()
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_freeformStaysInDesktop_plays() {
+    // At least one freeform task to be in a desktop.
+    val existingTask = setUpFreeformTask(displayId = 5)
+    val triggerTask = setUpFreeformTask(displayId = 5, active = false)
+    assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue()
+    taskRepository.setTaskInFullImmersiveState(
+      displayId = existingTask.displayId,
+      taskId = existingTask.taskId,
+      immersive = true
+    )
+
+    assertThat(
+      controller.shouldPlayDesktopAnimation(
+        TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+      )
+    ).isTrue()
+  }
+
+  @Test
+  @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+  fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() {
+    val triggerTask = setUpFreeformTask(displayId = 5, active = false)
+    assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse()
+
+    assertThat(controller.shouldPlayDesktopAnimation(
+      TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null)
+    )).isFalse()
+  }
+
   private class RunOnStartTransitionCallback : ((IBinder) -> Unit) {
     var invocations = 0
       private set
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
index 7ae0bcd..90ab2b8 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java
@@ -43,7 +43,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.window.flags.Flags;
-import com.android.wm.shell.desktopmode.DesktopFullImmersiveTransitionHandler;
+import com.android.wm.shell.desktopmode.DesktopImmersiveController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.FocusTransitionObserver;
 import com.android.wm.shell.transition.TransitionInfoBuilder;
@@ -70,7 +70,7 @@
     @Mock
     private Transitions mTransitions;
     @Mock
-    private DesktopFullImmersiveTransitionHandler mDesktopFullImmersiveTransitionHandler;
+    private DesktopImmersiveController mDesktopImmersiveController;
     @Mock
     private WindowDecorViewModel mWindowDecorViewModel;
     @Mock
@@ -92,7 +92,7 @@
 
         mTransitionObserver = new FreeformTaskTransitionObserver(
                 context, mShellInit, mTransitions,
-                Optional.of(mDesktopFullImmersiveTransitionHandler),
+                Optional.of(mDesktopImmersiveController),
                 mWindowDecorViewModel, Optional.of(mTaskChangeListener), mFocusTransitionObserver);
 
         final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(
@@ -321,7 +321,7 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
-    public void onTransitionReady_forwardsToDesktopImmersiveHandler() {
+    public void onTransitionReady_forwardsToDesktopImmersiveController() {
         final IBinder transition = mock(IBinder.class);
         final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CHANGE, 0).build();
         final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class);
@@ -329,7 +329,38 @@
 
         mTransitionObserver.onTransitionReady(transition, info, startT, finishT);
 
-        verify(mDesktopFullImmersiveTransitionHandler).onTransitionReady(transition, info);
+        verify(mDesktopImmersiveController).onTransitionReady(transition, info, startT, finishT);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    public void onTransitionMerged_forwardsToDesktopImmersiveController() {
+        final IBinder merged = mock(IBinder.class);
+        final IBinder playing = mock(IBinder.class);
+
+        mTransitionObserver.onTransitionMerged(merged, playing);
+
+        verify(mDesktopImmersiveController).onTransitionMerged(merged, playing);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    public void onTransitionStarting_forwardsToDesktopImmersiveController() {
+        final IBinder transition = mock(IBinder.class);
+
+        mTransitionObserver.onTransitionStarting(transition);
+
+        verify(mDesktopImmersiveController).onTransitionStarting(transition);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
+    public void onTransitionFinished_forwardsToDesktopImmersiveController() {
+        final IBinder transition = mock(IBinder.class);
+
+        mTransitionObserver.onTransitionFinished(transition, /* aborted= */ false);
+
+        verify(mDesktopImmersiveController).onTransitionFinished(transition, /* aborted= */ false);
     }
 
     private static TransitionInfo.Change createChange(int mode, int taskId, int windowingMode) {