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

Commit d77b2209 authored by Jorge Gil's avatar Jorge Gil
Browse files

[22/N] Desks: Implement move-to-next-display

Ensures move-to-next display continues to work with the multi-desk
backend. In short:
1) Reparents the task to default desk in the next display, rather than
   the TDA
2) Activates that desk
3) Deactivates the origin desk, if applicable
When the thing moving is a split-task:
4) Adds missing desktop clean up (which includes desk deactivation) of
   the desk in the destination display

Flag: com.android.window.flags.enable_multiple_desktops_backend
Bug: 394268248
Test: with 2 displays, open a task, then use `adb
shell dumpsys activity service SystemUIService WMShell desktopmode
moveToNextDisplay <taskId>` to move it to the other display and verify
hierarchy/repository state is correct. There's multiple edge cases to
test here such as:
- The task can be fullscreen/desktop/split
- The origin desk may need to be deactivated if moving the last task
- The destination desk may or may not be activated already
- When moving a split root, and there's a desk active in the destination
  display it needs to be deactivated

Change-Id: I972fa586108f0c01b14c192aa0a30c0fad1d66fd
parent e8aa3de1
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -68,6 +68,8 @@ class DesktopDisplayEventHandler(
        //  desk has been recreated here, which may result in a crash-loop if the repository is
        //  checking that the desk exists before adding a task to it. See b/391984373.
        desktopTasksController.createDesk(displayId)
        // TODO: b/393978539 - consider activating the desk on creation when applicable, such as
        //  for connected displays.
    }

    override fun onDisplayRemoved(displayId: Int) {
+0 −1
Original line number Diff line number Diff line
@@ -238,7 +238,6 @@ class DesktopRepository(
    }

    /** Returns the id of the active desk in the given display, if any. */
    @VisibleForTesting
    fun getActiveDeskId(displayId: Int): Int? = desktopData.getActiveDesk(displayId)?.deskId

    /** Returns the id of the desk to which this task belongs. */
+72 −21
Original line number Diff line number Diff line
@@ -1251,36 +1251,77 @@ class DesktopTasksController(
            return
        }

        val wct = WindowContainerTransaction()
        val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)
        if (displayAreaInfo == null) {
            logW("moveToDisplay: display not found")
            return
        }

        val wct = WindowContainerTransaction()

        // check if the task is part of splitscreen
        if (
            Flags.enableNonDefaultDisplaySplit() &&
                Flags.enableMoveToNextDisplayShortcut() &&
                splitScreenController.isTaskInSplitScreen(task.taskId)
        ) {
            val activeDeskId = taskRepository.getActiveDeskId(displayId)
            logV("moveToDisplay: moving split root to displayId=%d", displayId)
            val stageCoordinatorRootTaskToken =
                splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId(
                    DEFAULT_DISPLAY
                )

            wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */)
            transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
            val deactivationRunnable =
                if (activeDeskId != null) {
                    // Split is being placed on top of an existing desk in the target display. Make
                    // sure it is cleaned up.
                    performDesktopExitCleanUp(
                        wct = wct,
                        deskId = activeDeskId,
                        displayId = displayId,
                        willExitDesktop = true,
                        shouldEndUpAtHome = false,
                    )
                } else {
                    null
                }
            val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
            deactivationRunnable?.invoke(transition)
            return
        }

        val destinationDeskId = taskRepository.getDefaultDeskId(displayId)
        if (destinationDeskId == null) {
            logW("moveToDisplay: desk not found for display: $displayId")
            return
        }

        // TODO: b/393977830 and b/397437641 - do not assume that freeform==desktop.
        if (!task.isFreeform) {
            addMoveToDesktopChanges(wct, task, displayId)
        } else if (Flags.enableMoveToNextDisplayShortcut()) {
            applyFreeformDisplayChange(wct, task, displayId)
        }

        val activationRunnable: RunOnTransitStart?
        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            desksOrganizer.moveTaskToDesk(wct, destinationDeskId, task)
            prepareForDeskActivation(displayId, wct)
            desksOrganizer.activateDesk(wct, destinationDeskId)
            activationRunnable = { transition ->
                desksTransitionObserver.addPendingTransition(
                    DeskTransition.ActiveDeskWithTask(
                        token = transition,
                        displayId = displayId,
                        deskId = destinationDeskId,
                        enterTaskId = task.taskId,
                    )
                )
            }
        } else {
            wct.reparent(task.token, displayAreaInfo.token, /* onTop= */ true)
            activationRunnable = null
        }
        if (Flags.enableDisplayFocusInShellTransitions()) {
            // Bring the destination display to top with includingParents=true, so that the
            // destination display gains the display focus, which makes the top task in the display
@@ -1288,22 +1329,31 @@ class DesktopTasksController(
            wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true)
        }

        // TODO: b/394268248 - desk needs to be deactivated when moving the last task and going
        //  home.
        if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
        val sourceDisplayId = task.displayId
        val sourceDeskId = taskRepository.getDeskIdForTask(task.taskId)
        val shouldExitDesktopIfNeeded =
            Flags.enablePerDisplayDesktopWallpaperActivity() ||
                DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue
        val deactivationRunnable =
            if (shouldExitDesktopIfNeeded) {
                performDesktopExitCleanupIfNeeded(
                    taskId = task.taskId,
                displayId = task.displayId,
                    deskId = sourceDeskId,
                    displayId = sourceDisplayId,
                    wct = wct,
                    forceToFullscreen = false,
                // TODO: b/371096166 - Temporary turing home relaunch off to prevent home stealing
                    // TODO: b/371096166 - Temporary turing home relaunch off to prevent home
                    // stealing
                    // display focus. Remove shouldEndUpAtHome = false when home focus handling
                    // with connected display is implemented in wm core.
                    shouldEndUpAtHome = false,
                )
            } else {
                null
            }

        transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
        val transition = transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null)
        deactivationRunnable?.invoke(transition)
        activationRunnable?.invoke(transition)
    }

    /**
@@ -2484,6 +2534,7 @@ class DesktopTasksController(
        val displayLayout = displayController.getDisplayLayout(displayId) ?: return
        val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId)!!
        val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode
        // TODO: b/397437641 - reconsider the windowing mode choice when multiple desks is enabled.
        val targetWindowingMode =
            if (tdaWindowingMode == WINDOWING_MODE_FREEFORM) {
                // Display windowing is freeform, set to undefined and inherit it
+20 −9
Original line number Diff line number Diff line
@@ -31,12 +31,14 @@ class DesksTransitionObserver(
    private val desktopUserRepositories: DesktopUserRepositories,
    private val desksOrganizer: DesksOrganizer,
) {
    private val deskTransitions = mutableMapOf<IBinder, DeskTransition>()
    private val deskTransitions = mutableMapOf<IBinder, MutableSet<DeskTransition>>()

    /** Adds a pending desk transition to be tracked. */
    fun addPendingTransition(transition: DeskTransition) {
        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
        deskTransitions[transition.token] = transition
        val transitions = deskTransitions[transition.token] ?: mutableSetOf()
        transitions += transition
        deskTransitions[transition.token] = transitions
    }

    /**
@@ -45,7 +47,11 @@ class DesksTransitionObserver(
     */
    fun onTransitionReady(transition: IBinder, info: TransitionInfo) {
        if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return
        val deskTransition = deskTransitions.remove(transition) ?: return
        val deskTransitions = deskTransitions.remove(transition) ?: return
        deskTransitions.forEach { deskTransition -> handleDeskTransition(info, deskTransition) }
    }

    private fun handleDeskTransition(info: TransitionInfo, deskTransition: DeskTransition) {
        logD("Desk transition ready: %s", deskTransition)
        val desktopRepository = desktopUserRepositories.current
        when (deskTransition) {
@@ -60,17 +66,22 @@ class DesksTransitionObserver(
                deskTransition.onDeskRemovedListener?.onDeskRemoved(displayId, deskId)
            }
            is DeskTransition.ActivateDesk -> {
                val activeDeskChange =
                val activateDeskChange =
                    info.changes.find { change ->
                        desksOrganizer.isDeskActiveAtEnd(change, deskTransition.deskId)
                    }
                activeDeskChange?.let {
                if (activateDeskChange == null) {
                    // Always activate even if there is no change in the transition for the
                    // activated desk. This is necessary because some activation requests, such as
                    // those involving empty desks, may not contain visibility changes that are
                    // reported in the transition change list.
                    logD("Activating desk without transition change")
                }
                desktopRepository.setActiveDesk(
                    displayId = deskTransition.displayId,
                    deskId = deskTransition.deskId,
                )
            }
            }
            is DeskTransition.ActiveDeskWithTask -> {
                val withTask =
                    info.changes.find { change ->
+73 −0
Original line number Diff line number Diff line
@@ -2779,6 +2779,79 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
        assertThat(taskChange.includingParents()).isTrue()
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
    fun moveToNextDisplay_toDeskInOtherDisplay_movesToDeskAndActivates() {
        val transition = Binder()
        val targetDeskId = 4
        taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId)
        taskRepository.setDeskInactive(deskId = targetDeskId)
        // Set up two display ids
        whenever(rootTaskDisplayAreaOrganizer.displayIds)
            .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
        // Create a mock for the target display area: second display
        val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
        whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
            .thenReturn(secondDisplayArea)
        whenever(transitions.startTransition(eq(TRANSIT_CHANGE), any(), anyOrNull()))
            .thenReturn(transition)

        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
        taskRepository.addTaskToDesk(
            displayId = DEFAULT_DISPLAY,
            deskId = 0,
            taskId = task.taskId,
            isVisible = true,
        )
        controller.moveToNextDisplay(task.taskId)

        verify(desksOrganizer).moveTaskToDesk(any(), eq(targetDeskId), eq(task))
        verify(desksOrganizer).activateDesk(any(), eq(targetDeskId))
        verify(desksTransitionsObserver)
            .addPendingTransition(
                DeskTransition.ActiveDeskWithTask(
                    token = transition,
                    displayId = SECOND_DISPLAY,
                    deskId = targetDeskId,
                    enterTaskId = task.taskId,
                )
            )
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
    fun moveToNextDisplay_wasLastTaskInSourceDesk_deactivates() {
        val transition = Binder()
        val sourceDeskId = 0
        val targetDeskId = 4
        taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = targetDeskId)
        taskRepository.setDeskInactive(deskId = targetDeskId)
        // Set up two display ids
        whenever(rootTaskDisplayAreaOrganizer.displayIds)
            .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY))
        // Create a mock for the target display area: second display
        val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0)
        whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY))
            .thenReturn(secondDisplayArea)
        whenever(transitions.startTransition(eq(TRANSIT_CHANGE), any(), anyOrNull()))
            .thenReturn(transition)

        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
        taskRepository.addTaskToDesk(
            displayId = DEFAULT_DISPLAY,
            deskId = sourceDeskId,
            taskId = task.taskId,
            isVisible = true,
        )
        controller.moveToNextDisplay(task.taskId)

        verify(desksOrganizer).deactivateDesk(any(), eq(sourceDeskId))
        verify(desksTransitionsObserver)
            .addPendingTransition(
                DeskTransition.DeactivateDesk(token = transition, deskId = sourceDeskId)
            )
    }

    @Test
    fun getTaskWindowingMode() {
        val fullscreenTask = setUpFullscreenTask()
Loading