Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7afc6b31 authored by Jorge Gil's avatar Jorge Gil
Browse files

Desks: Handle back-nav transitions correctly

When a task is moved to back using back navigation, handle the
transition request and make sure to minimize it correctly using
DesksOrganizer (so that it is reparented to the minimization root). Also
stop triggering desks deactivations in handleFreeformTaskLaunch when the
desk is already inactive.

In the DesktopBackNavTransitionObserver, do not assume that non-freeform
tasks are not desktop tasks. Instead, use DesksOrganizer or
DesktopRepository to check for whether a task is in desktop or part
exiting a desk.

The above changes allows back navigation of tasks to be correctly
tracked as minimizations in both the repository and for animation
purposes.

Flag: com.android.window.flags.enable_multiple_desktops_backend
Fix: 413024179
Fix: 413135711
Fix: 413134745
Test: back nav the last window or a task when it is not the last window
and verify animations work as expected without flickering. Also verify
minimize indicator is present after back nav and the app can be reopened
from taskbar.

Change-Id: Ifd2791464bbf8adccf872d11627e78a9ffcfc13d
parent 9f819162
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -1367,6 +1367,7 @@ public abstract class WMShellModule {
            Optional<DesktopUserRepositories> desktopUserRepositories,
            Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler,
            Optional<BackAnimationController> backAnimationController,
            DesksOrganizer desksOrganizer,
            DesktopState desktopState,
            ShellInit shellInit) {
        return desktopUserRepositories.flatMap(
@@ -1376,6 +1377,7 @@ public abstract class WMShellModule {
                                        repository,
                                        desktopMixedTransitionHandler.get(),
                                        backAnimationController.get(),
                                        desksOrganizer,
                                        desktopState,
                                        shellInit)));
    }
+38 −13
Original line number Diff line number Diff line
@@ -16,8 +16,7 @@

package com.android.wm.shell.desktopmode

import android.app.ActivityManager
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.ActivityManager.RunningTaskInfo
import android.os.IBinder
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_TO_BACK
@@ -27,6 +26,7 @@ import android.window.TransitionInfo
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.back.BackAnimationController
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isExitDesktopModeTransition
import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE
import com.android.wm.shell.shared.TransitionUtil
import com.android.wm.shell.shared.desktopmode.DesktopState
@@ -41,6 +41,7 @@ class DesktopBackNavTransitionObserver(
    private val desktopUserRepositories: DesktopUserRepositories,
    private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler,
    private val backAnimationController: BackAnimationController,
    private val desksOrganizer: DesksOrganizer,
    desktopState: DesktopState,
    shellInit: ShellInit,
) {
@@ -51,7 +52,7 @@ class DesktopBackNavTransitionObserver(
    }

    fun onInit() {
        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "DesktopBackNavigationTransitionObserver: onInit")
        logD("onInit")
    }

    fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
@@ -73,10 +74,7 @@ class DesktopBackNavTransitionObserver(
            if (taskInfo == null || taskInfo.taskId == -1) continue

            val desktopRepository = desktopUserRepositories.getProfile(taskInfo.userId)
            if (
                desktopRepository.isActiveTask(taskInfo.taskId) &&
                    taskInfo.windowingMode != WINDOWING_MODE_FREEFORM
            ) {
            if (desktopRepository.isExitingDesktopTask(change)) {
                desktopRepository.removeTask(taskInfo.displayId, taskInfo.taskId)
            }
        }
@@ -96,7 +94,7 @@ class DesktopBackNavTransitionObserver(
                if (
                    isInDesktop &&
                        change.mode == TRANSIT_TO_BACK &&
                        taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
                        desktopRepository.isDesktopTask(taskInfo)
                ) {
                    val isLastTask =
                        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
@@ -104,6 +102,10 @@ class DesktopBackNavTransitionObserver(
                        } else {
                            desktopRepository.isOnlyVisibleTask(taskInfo.taskId, taskInfo.displayId)
                        }
                    logD(
                        "handleBackNavigation marking to-back taskId=%d as minimized",
                        taskInfo.taskId,
                    )
                    desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId)
                    desktopMixedTransitionHandler.addPendingMixedTransition(
                        DesktopMixedTransitionHandler.PendingMixedTransition.Minimize(
@@ -137,6 +139,7 @@ class DesktopBackNavTransitionObserver(

            if (minimizingTask == null) return
            // If the transition has wallpaper closing, it means we are moving out of desktop.
            logD("handleBackNavigation marking close taskId=%d as minimized", minimizingTask)
            desktopMixedTransitionHandler.addPendingMixedTransition(
                DesktopMixedTransitionHandler.PendingMixedTransition.Minimize(
                    transition,
@@ -151,7 +154,7 @@ class DesktopBackNavTransitionObserver(
     * Given this a closing task in a closing transition, a task is assumed to be closed by back
     * navigation if:
     * 1) Desktop mode is visible.
     * 2) Task is in freeform.
     * 2) It is a desktop task.
     * 3) Task is the latest task that the back gesture is triggered on.
     * 4) It's not marked as a closing task as a result of closing it by the app header.
     *
@@ -159,14 +162,12 @@ class DesktopBackNavTransitionObserver(
     * will be rare. E.g. triggering back navigation on an app that pops up a close dialog, and
     * closing it will minimize it here.
     */
    private fun getMinimizingTaskForClosingTransition(
        taskInfo: ActivityManager.RunningTaskInfo
    ): Int? {
    private fun getMinimizingTaskForClosingTransition(taskInfo: RunningTaskInfo): Int? {
        val desktopRepository = desktopUserRepositories.getProfile(taskInfo.userId)
        val isInDesktop = desktopRepository.isAnyDeskActive(taskInfo.displayId)
        if (
            isInDesktop &&
                taskInfo.windowingMode == WINDOWING_MODE_FREEFORM &&
                desktopRepository.isDesktopTask(taskInfo) &&
                backAnimationController.latestTriggerBackTask == taskInfo.taskId &&
                !desktopRepository.isClosingTask(taskInfo.taskId)
        ) {
@@ -175,4 +176,28 @@ class DesktopBackNavTransitionObserver(
        }
        return null
    }

    private fun DesktopRepository.isDesktopTask(task: RunningTaskInfo): Boolean =
        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            isActiveTask(task.taskId)
        } else {
            task.isFreeform
        }

    private fun DesktopRepository.isExitingDesktopTask(change: TransitionInfo.Change): Boolean {
        val task = change.taskInfo ?: return false
        return if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            isActiveTask(task.taskId) && desksOrganizer.getDeskAtEnd(change) == null
        } else {
            isActiveTask(task.taskId) && !task.isFreeform
        }
    }

    private fun logD(msg: String, vararg arguments: Any?) {
        ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments)
    }

    companion object {
        private const val TAG = "DesktopBackNavTransitionObserver"
    }
}
+31 −14
Original line number Diff line number Diff line
@@ -60,7 +60,9 @@ import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_NONE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_PIP
import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.view.WindowManager.transitTypeToString
import android.widget.Toast
import android.window.DesktopExperienceFlags
import android.window.DesktopExperienceFlags.DesktopExperienceFlag
@@ -1489,7 +1491,7 @@ class DesktopTasksController(
    ): IBinder {
        logV(
            "startLaunchTransition type=%s launchingTaskId=%d deskId=%d displayId=%d",
            WindowManager.transitTypeToString(transitionType),
            transitTypeToString(transitionType),
            launchingTaskId,
            deskId,
            displayId,
@@ -2860,7 +2862,7 @@ class DesktopTasksController(
        )
        val wct = WindowContainerTransaction()
        if (!anyDeskActive && !shouldEnterDesktop) {
            // We are outside of desktop mode and already existing desktop task is being
            // We are outside of desktop mode and an already existing desktop task is being
            // launched. We should make this task go to fullscreen instead of freeform. Note
            // that this means any re-launch of a freeform window outside of desktop will be in
            // fullscreen as long as default-desktop flag is disabled.
@@ -2868,12 +2870,7 @@ class DesktopTasksController(
                addMoveToFullscreenChanges(
                    wct = wct,
                    taskInfo = task,
                    willExitDesktop =
                        willExitDesktop(
                            triggerTaskId = task.taskId,
                            displayId = task.displayId,
                            forceExitDesktop = true,
                        ),
                    willExitDesktop = false, // Already outside desktop.
                )
            runOnTransitStart?.invoke(transition)
            return wct
@@ -3108,23 +3105,43 @@ class DesktopTasksController(
    }

    /**
     * Handle task closing by removing wallpaper activity if it's the last active task.
     *
     * TODO: b/394268248 - desk needs to be deactivated.
     * Handles a closing task. This usually means deactivating and cleaning up the desk if it was
     * the last task in it. It also handles to-back transitions of the last desktop task as a
     * minimize operation.
     */
    private fun handleTaskClosing(
        task: RunningTaskInfo,
        transition: IBinder,
        requestType: Int,
        @WindowManager.TransitionType requestType: Int,
    ): WindowContainerTransaction? {
        logV("handleTaskClosing")
        logV(
            "handleTaskClosing taskId=%d closingType=%s",
            task.taskId,
            transitTypeToString(requestType),
        )
        if (!isAnyDeskActive(task.displayId)) return null
        val deskId = taskRepository.getDeskIdForTask(task.taskId)
        if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            return null
        }

        val wct = WindowContainerTransaction()
        if (
            DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue &&
                requestType == TRANSIT_TO_BACK
        ) {
            val isLastTask = taskRepository.isOnlyVisibleTask(task.taskId, task.displayId)
            logV(
                "Handling to-back of taskId=%d (isLast=%b) as minimize in deskId=%d",
                task.taskId,
                isLastTask,
                deskId,
            )
            desksOrganizer.minimizeTask(
                wct = wct,
                deskId = checkNotNull(deskId) { "Expected non-null deskId" },
                task = task,
            )
        }
        val deactivationRunnable =
            performDesktopExitCleanupIfNeeded(
                taskId = task.taskId,
+98 −10
Original line number Diff line number Diff line
@@ -32,17 +32,14 @@ import android.window.IWindowContainerToken
import android.window.TransitionInfo
import android.window.TransitionInfo.Change
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import com.android.window.flags.Flags
import com.android.wm.shell.MockToken
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.back.BackAnimationController
import com.android.wm.shell.common.ShellExecutor
import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG
import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer
import com.android.wm.shell.shared.desktopmode.FakeDesktopState
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers.anyInt
@@ -62,12 +59,11 @@ import org.mockito.kotlin.whenever
class DesktopBackNavTransitionObserverTest : ShellTestCase() {

    private val testExecutor = mock<ShellExecutor>()
    private val transitions = mock<Transitions>()
    private val userRepositories = mock<DesktopUserRepositories>()
    private val taskRepository = mock<DesktopRepository>()
    private val mixedHandler = mock<DesktopMixedTransitionHandler>()
    private val backAnimationController = mock<BackAnimationController>()
    private val wallpaperToken = MockToken().token()
    private val desksOrganizer = mock<DesksOrganizer>()
    private val desktopState = FakeDesktopState()

    private lateinit var transitionObserver: DesktopBackNavTransitionObserver
@@ -86,6 +82,7 @@ class DesktopBackNavTransitionObserverTest : ShellTestCase() {
                userRepositories,
                mixedHandler,
                backAnimationController,
                desksOrganizer,
                desktopState,
                shellInit,
            )
@@ -96,6 +93,26 @@ class DesktopBackNavTransitionObserverTest : ShellTestCase() {
    fun backNavigation_taskMinimized() {
        val task = createTaskInfo(1)
        whenever(taskRepository.isAnyDeskActive(any())).thenReturn(true)
        whenever(taskRepository.isActiveTask(task.taskId)).thenReturn(true)

        transitionObserver.onTransitionReady(
            transition = mock(),
            info = createBackNavigationTransition(task),
        )

        verify(taskRepository).minimizeTask(task.displayId, task.taskId)
        verify(mixedHandler).addPendingMixedTransition(any())
    }

    @Test
    @EnableFlags(
        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
    )
    fun backNavigation_nonFreeformDesktopTask_taskMinimized() {
        val task = createTaskInfo(1, windowingMode = WINDOWING_MODE_FULLSCREEN)
        whenever(taskRepository.isAnyDeskActive(any())).thenReturn(true)
        whenever(taskRepository.isActiveTask(task.taskId)).thenReturn(true)

        transitionObserver.onTransitionReady(
            transition = mock(),
@@ -112,6 +129,7 @@ class DesktopBackNavTransitionObserverTest : ShellTestCase() {
        val task = createTaskInfo(1)
        val transition = mock<IBinder>()
        whenever(taskRepository.isAnyDeskActive(any())).thenReturn(true)
        whenever(taskRepository.isActiveTask(task.taskId)).thenReturn(true)
        whenever(taskRepository.isOnlyVisibleTask(task.taskId, task.displayId)).thenReturn(false)
        whenever(taskRepository.hasOnlyOneVisibleTask(task.displayId)).thenReturn(false)
        whenever(taskRepository.isClosingTask(task.taskId)).thenReturn(false)
@@ -139,6 +157,7 @@ class DesktopBackNavTransitionObserverTest : ShellTestCase() {
        val task = createTaskInfo(1)
        val transition = mock<IBinder>()
        whenever(taskRepository.isAnyDeskActive(any())).thenReturn(true)
        whenever(taskRepository.isActiveTask(task.taskId)).thenReturn(true)
        whenever(taskRepository.isClosingTask(task.taskId)).thenReturn(false)
        whenever(backAnimationController.latestTriggerBackTask).thenReturn(task.taskId)

@@ -166,6 +185,7 @@ class DesktopBackNavTransitionObserverTest : ShellTestCase() {
        val task = createTaskInfo(1)
        val transition = mock<IBinder>()
        whenever(taskRepository.isAnyDeskActive(any())).thenReturn(true)
        whenever(taskRepository.isActiveTask(task.taskId)).thenReturn(true)
        whenever(taskRepository.isClosingTask(task.taskId)).thenReturn(false)
        whenever(backAnimationController.latestTriggerBackTask).thenReturn(task.taskId)

@@ -228,6 +248,7 @@ class DesktopBackNavTransitionObserverTest : ShellTestCase() {

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
    fun removeTasks_onTaskFullscreenLaunchWithOpenTransition_taskRemovedFromRepo() {
        val task = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
        whenever(taskRepository.isAnyDeskActive(any())).thenReturn(true)
@@ -242,8 +263,45 @@ class DesktopBackNavTransitionObserverTest : ShellTestCase() {
        verify(taskRepository).removeTask(task.displayId, task.taskId)
    }

    @Test
    @EnableFlags(
        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
    )
    fun removeTasks_onTaskFullscreenInDeskLaunchWithOpenTransition_taskNotRemovedFromRepo() {
        val deskId = 0
        val task = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
        val transitionInfo = createOpenChangeTransition(task)
        whenever(taskRepository.isAnyDeskActive(any())).thenReturn(true)
        whenever(taskRepository.isActiveTask(task.taskId)).thenReturn(true)
        whenever(desksOrganizer.getDeskAtEnd(transitionInfo.changes.first())).thenReturn(deskId)

        transitionObserver.onTransitionReady(transition = mock(), info = transitionInfo)

        verify(taskRepository, never()).removeTask(task.displayId, task.taskId)
    }

    @Test
    @EnableFlags(
        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
    )
    fun removeTasks_onTaskOutsideDeskLaunchWithOpenTransition_taskRemovedFromRepo() {
        val task = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
        val transitionInfo = createOpenChangeTransition(task)
        whenever(taskRepository.isAnyDeskActive(any())).thenReturn(true)
        whenever(taskRepository.isActiveTask(task.taskId)).thenReturn(true)
        whenever(desksOrganizer.getDeskAtEnd(transitionInfo.changes.first())).thenReturn(null)

        transitionObserver.onTransitionReady(transition = mock(), info = transitionInfo)

        verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId)
        verify(taskRepository).removeTask(task.displayId, task.taskId)
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION)
    @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
    fun removeTasks_onTaskFullscreenLaunchExitDesktopTransition_taskRemovedFromRepo() {
        val task = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
        whenever(taskRepository.isAnyDeskActive(any())).thenReturn(true)
@@ -258,10 +316,40 @@ class DesktopBackNavTransitionObserverTest : ShellTestCase() {
        verify(taskRepository).removeTask(task.displayId, task.taskId)
    }

    private fun WindowContainerTransaction.assertIndexInBounds(index: Int) {
        assertWithMessage("WCT does not have a hierarchy operation at index $index")
            .that(hierarchyOps.size)
            .isGreaterThan(index)
    @Test
    @EnableFlags(
        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
    )
    fun removeTasks_onTaskFullscreenInDeskLaunchExitDesktopTransition_taskNotRemovedFromRepo() {
        val deskId = 0
        val task = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
        val transitionInfo = createOpenChangeTransition(task, TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG)
        whenever(taskRepository.isAnyDeskActive(any())).thenReturn(true)
        whenever(taskRepository.isActiveTask(task.taskId)).thenReturn(true)
        whenever(desksOrganizer.getDeskAtEnd(transitionInfo.changes.first())).thenReturn(deskId)

        transitionObserver.onTransitionReady(transition = mock(), info = transitionInfo)

        verify(taskRepository, never()).removeTask(task.displayId, task.taskId)
    }

    @Test
    @EnableFlags(
        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION,
        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
    )
    fun removeTasks_onTaskOutsideDeskLaunchExitDesktopTransition_taskRemovedFromRepo() {
        val task = createTaskInfo(1, WINDOWING_MODE_FULLSCREEN)
        val transitionInfo = createOpenChangeTransition(task, TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG)
        whenever(taskRepository.isAnyDeskActive(any())).thenReturn(true)
        whenever(taskRepository.isActiveTask(task.taskId)).thenReturn(true)
        whenever(desksOrganizer.getDeskAtEnd(transitionInfo.changes.first())).thenReturn(null)

        transitionObserver.onTransitionReady(transition = mock(), info = transitionInfo)

        verify(taskRepository, never()).minimizeTask(task.displayId, task.taskId)
        verify(taskRepository).removeTask(task.displayId, task.taskId)
    }

    private fun createOpenChangeTransition(
+71 −22
Original line number Diff line number Diff line
@@ -4933,20 +4933,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
        assertThat(desktopTasksLimiter.hasTaskLimitTransitionForTesting(transition)).isTrue()
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
    fun handleRequest_freeformTaskFromInactiveDesk_tracksDeskDeactivation() {
        val deskId = 0
        val freeformTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = deskId)
        taskRepository.setDeskInactive(deskId = deskId)

        val transition = Binder()
        controller.handleRequest(transition, createTransition(freeformTask))

        verify(desksTransitionsObserver)
            .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId))
    }

    @Test
    fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() {
        taskRepository.setDeskInactive(deskId = 0)
@@ -5916,20 +5902,83 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY)
    fun handleRequest_closeTransition_minimizadTask_withWallpaper_removesWallpaper() {
        val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
        val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
    @EnableFlags(
        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
        Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
    )
    fun handleRequest_toBackTransition_noActiveDesk_notHandled() {
        taskRepository.setDeskInactive(deskId = 0)
        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)

        taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId)
        // Task is being minimized so mark it as not visible.
        taskRepository.updateTask(displayId = DEFAULT_DISPLAY, task2.taskId, isVisible = false)
        val result =
            controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK))
            controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))

        assertNull(result, "Should not handle request")
    }

    @Test
    @EnableFlags(
        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
        Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
    )
    fun handleRequest_toBackTransition_minimizesTask() {
        taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0)

        val result =
            controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK))

        assertNotNull(result) { "Should handle request" }
        verify(desksOrganizer).minimizeTask(result, deskId = 0, task)
    }

    @Test
    @EnableFlags(
        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
        Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
    )
    fun handleRequest_toBackTransition_lastTask_deactivatesDesk() {
        taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0)

        val transition = Binder()
        val result =
            controller.handleRequest(transition, createTransition(task, type = TRANSIT_TO_BACK))

        assertNotNull(result) { "Should handle request" }
        verify(desksOrganizer).deactivateDesk(result, deskId = 0)
        verify(desksTransitionsObserver)
            .addPendingTransition(DeskTransition.DeactivateDesk(token = transition, deskId = 0))
    }

    @Test
    @EnableFlags(
        Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,
        Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND,
        Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY,
    )
    fun handleRequest_toBackTransition_notLastTask_doesNotDeactivateDesk() {
        taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0)
        setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0)
        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, deskId = 0)

        val transition = Binder()
        controller.handleRequest(transition, createTransition(task, type = TRANSIT_TO_BACK))

        verify(desksOrganizer, never()).deactivateDesk(any(), deskId = eq(0))
        verify(desksTransitionsObserver, never())
            .addPendingTransition(
                argThat {
                    this is DeskTransition.RemoveDesk &&
                        this.token == transition &&
                        this.deskId == 0
                }
            )
    }

    @Test
    fun handleRequest_freeformTask_displayDoesntHandleDesktop_returnNull() {
        desktopState.overrideDesktopModeSupportPerDisplay[SECOND_DISPLAY] = false