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

Commit 3e523f55 authored by Jorge Gil's avatar Jorge Gil
Browse files

[16/N] Desks: Create minimization root for each desk

Creates an additional root task for each desk to host minimized desktop
windows. Also adds DesksOrganizer#minimizeTask to move a task into it.

The minimized root is top-level (child of the DefaultTDA) for now, and
uses WCT#setHidden to always remain invisible.

Flag: com.android.window.flags.enable_multiple_desktops_backend
Bug: 391485148
Test: put tast into desktop, then minimize it, verify WM hierarchy is as
expected

Change-Id: I00c0ff07760e65508e19fea5733899e81c1f9cd7
parent fb317fbe
Loading
Loading
Loading
Loading
+19 −5
Original line number Diff line number Diff line
@@ -374,7 +374,8 @@ class DesktopRepository(
     * Checks if a task is the only visible, non-closing, non-minimized task on the active desk of
     * the given display, or any display's active desk if [displayId] is [INVALID_DISPLAY].
     *
     * TODO: b/389960283 - add explicit [deskId] argument.
     * TODO: b/389960283 - consider forcing callers to use [isOnlyVisibleNonClosingTaskInDesk] with
     *   an explicit desk id instead of using this function and defaulting to the active one.
     */
    fun isOnlyVisibleNonClosingTask(taskId: Int, displayId: Int = INVALID_DISPLAY): Boolean {
        val activeDesks =
@@ -384,12 +385,25 @@ class DesktopRepository(
                desktopData.getAllActiveDesks()
            }
        return activeDesks.any { desk ->
            desk.visibleTasks
            isOnlyVisibleNonClosingTaskInDesk(
                taskId = taskId,
                deskId = desk.deskId,
                displayId = desk.displayId,
            )
        }
    }

    /**
     * Checks if a task is the only visible, non-closing, non-minimized task on the given desk of
     * the given display.
     */
    fun isOnlyVisibleNonClosingTaskInDesk(taskId: Int, deskId: Int, displayId: Int): Boolean {
        val desk = desktopData.getDesk(deskId) ?: return false
        return desk.visibleTasks
            .subtract(desk.closingTasks)
            .subtract(desk.minimizedTasks)
            .singleOrNull() == taskId
    }
    }

    /**
     * Returns the active tasks in the given display's active desk.
+24 −4
Original line number Diff line number Diff line
@@ -870,6 +870,10 @@ class DesktopTasksController(
    private fun minimizeTaskInner(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) {
        val taskId = taskInfo.taskId
        val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId)
        if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            logW("minimizeTaskInner: desk not found for task: ${taskInfo.taskId}")
            return
        }
        val displayId = taskInfo.displayId
        val wct = WindowContainerTransaction()

@@ -890,10 +894,26 @@ class DesktopTasksController(
                taskInfo = taskInfo,
                reason = DesktopImmersiveController.ExitReason.MINIMIZED,
            )

        wct.reorder(taskInfo.token, false)
        val isLastTask = taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)
        val transition: IBinder =
        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
            desksOrganizer.minimizeTask(
                wct = wct,
                deskId = checkNotNull(deskId) { "Expected non-null deskId" },
                task = taskInfo,
            )
        } else {
            wct.reorder(taskInfo.token, /* onTop= */ false)
        }
        val isLastTask =
            if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
                taskRepository.isOnlyVisibleNonClosingTaskInDesk(
                    taskId = taskId,
                    deskId = checkNotNull(deskId) { "Expected non-null deskId" },
                    displayId = displayId,
                )
            } else {
                taskRepository.isOnlyVisibleNonClosingTask(taskId = taskId, displayId = displayId)
            }
        val transition =
            freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask)
        desktopTasksLimiter.ifPresent {
            it.addPendingMinimizeChange(
+10 −0
Original line number Diff line number Diff line
@@ -40,9 +40,19 @@ interface DesksOrganizer {
        task: ActivityManager.RunningTaskInfo,
    )

    /** Minimizes the given task of the given deskId. */
    fun minimizeTask(
        wct: WindowContainerTransaction,
        deskId: Int,
        task: ActivityManager.RunningTaskInfo,
    )

    /** Whether the change is for the given desk id. */
    fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean

    /** Whether the change is for a known desk. */
    fun isDeskChange(change: TransitionInfo.Change): Boolean

    /**
     * Returns the desk id in which the task in the given change is located at the end of a
     * transition, if any.
+240 −45
Original line number Diff line number Diff line
@@ -15,7 +15,9 @@
 */
package com.android.wm.shell.desktopmode.multidesks

import android.annotation.SuppressLint
import android.app.ActivityManager.RunningTaskInfo
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD
import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
@@ -25,6 +27,7 @@ import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_TO_FRONT
import android.window.DesktopExperienceFlags
import android.window.TransitionInfo
import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import androidx.core.util.forEach
import com.android.internal.annotations.VisibleForTesting
@@ -43,8 +46,12 @@ class RootTaskDesksOrganizer(
    private val shellTaskOrganizer: ShellTaskOrganizer,
) : DesksOrganizer, ShellTaskOrganizer.TaskListener {

    private val deskCreateRequests = mutableListOf<CreateRequest>()
    @VisibleForTesting val roots = SparseArray<DeskRoot>()
    private val createDeskRootRequests = mutableListOf<CreateDeskRequest>()
    @VisibleForTesting val deskRootsByDeskId = SparseArray<DeskRoot>()
    private val createDeskMinimizationRootRequests =
        mutableListOf<CreateDeskMinimizationRootRequest>()
    @VisibleForTesting
    val deskMinimizationRootsByDeskId: MutableMap<Int, DeskMinimizationRoot> = mutableMapOf()

    init {
        if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) {
@@ -57,7 +64,7 @@ class RootTaskDesksOrganizer(

    override fun createDesk(displayId: Int, callback: OnCreateCallback) {
        logV("createDesk in display: %d", displayId)
        deskCreateRequests += CreateRequest(displayId, callback)
        createDeskRootRequests += CreateDeskRequest(displayId, callback)
        shellTaskOrganizer.createRootTask(
            displayId,
            WINDOWING_MODE_FREEFORM,
@@ -68,14 +75,14 @@ class RootTaskDesksOrganizer(

    override fun removeDesk(wct: WindowContainerTransaction, deskId: Int) {
        logV("removeDesk %d", deskId)
        val desk = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" }
        wct.removeRootTask(desk.taskInfo.token)
        deskRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) }
        deskMinimizationRootsByDeskId[deskId]?.let { root -> wct.removeRootTask(root.token) }
    }

    override fun activateDesk(wct: WindowContainerTransaction, deskId: Int) {
        logV("activateDesk %d", deskId)
        val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" }
        wct.reorder(root.taskInfo.token, /* onTop= */ true)
        val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" }
        wct.reorder(root.token, /* onTop= */ true)
        wct.setLaunchRoot(
            /* container= */ root.taskInfo.token,
            /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED),
@@ -85,7 +92,7 @@ class RootTaskDesksOrganizer(

    override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) {
        logV("deactivateDesk %d", deskId)
        val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" }
        val root = checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" }
        wct.setLaunchRoot(
            /* container= */ root.taskInfo.token,
            /* windowingModes= */ null,
@@ -98,16 +105,58 @@ class RootTaskDesksOrganizer(
        deskId: Int,
        task: RunningTaskInfo,
    ) {
        val root = roots[deskId] ?: error("Root not found for desk: $deskId")
        val root = deskRootsByDeskId[deskId] ?: error("Root not found for desk: $deskId")
        wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED)
        wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true)
    }

    override fun minimizeTask(wct: WindowContainerTransaction, deskId: Int, task: RunningTaskInfo) {
        val deskRoot =
            checkNotNull(deskRootsByDeskId[deskId]) { "Root not found for desk: $deskId" }
        val minimizationRoot =
            checkNotNull(deskMinimizationRootsByDeskId[deskId]) {
                "Minimization root not found for desk: $deskId"
            }
        val taskId = task.taskId
        if (taskId in minimizationRoot.children) {
            logV("Task #$taskId is already minimized in desk #$deskId")
            return
        }
        if (taskId !in deskRoot.children) {
            logE("Attempted to minimize task=${task.taskId} in desk=$deskId but it was not a child")
            return
        }
        wct.reparent(task.token, minimizationRoot.token, /* onTop= */ true)
    }

    override fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean =
        roots.contains(deskId) && change.taskInfo?.taskId == deskId
        (isDeskRootChange(change) && change.taskId == deskId) ||
            (getDeskMinimizationRootInChange(change)?.deskId == deskId)

    override fun isDeskChange(change: TransitionInfo.Change): Boolean =
        isDeskRootChange(change) || getDeskMinimizationRootInChange(change) != null

    private fun isDeskRootChange(change: TransitionInfo.Change): Boolean =
        change.taskId in deskRootsByDeskId

    override fun getDeskAtEnd(change: TransitionInfo.Change): Int? =
        change.taskInfo?.parentTaskId?.takeIf { it in roots }
    private fun getDeskMinimizationRootInChange(
        change: TransitionInfo.Change
    ): DeskMinimizationRoot? =
        deskMinimizationRootsByDeskId.values.find { it.rootId == change.taskId }

    private val TransitionInfo.Change.taskId: Int
        get() = taskInfo?.taskId ?: INVALID_TASK_ID

    override fun getDeskAtEnd(change: TransitionInfo.Change): Int? {
        val parentTaskId = change.taskInfo?.parentTaskId ?: return null
        if (parentTaskId in deskRootsByDeskId) {
            return parentTaskId
        }
        val deskMinimizationRoot =
            deskMinimizationRootsByDeskId.values.find { root -> root.rootId == parentTaskId }
                ?: return null
        return deskMinimizationRoot.deskId
    }

    override fun isDeskActiveAtEnd(change: TransitionInfo.Change, deskId: Int): Boolean =
        change.taskInfo?.taskId == deskId &&
@@ -115,87 +164,233 @@ class RootTaskDesksOrganizer(
            change.mode == TRANSIT_TO_FRONT

    override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) {
        if (taskInfo.parentTaskId in roots) {
        // Check whether this task is appearing inside a desk.
        if (taskInfo.parentTaskId in deskRootsByDeskId) {
            val deskId = taskInfo.parentTaskId
            val taskId = taskInfo.taskId
            logV("Task #$taskId appeared in desk #$deskId")
            addChildToDesk(taskId = taskId, deskId = deskId)
            return
        }
        val deskId = taskInfo.taskId
        check(deskId !in roots) { "A root already exists for desk: $deskId" }
        val request =
            checkNotNull(deskCreateRequests.firstOrNull { it.displayId == taskInfo.displayId }) {
                "Task ${taskInfo.taskId} appeared without pending create request"
        // Check whether this task is appearing in a minimization root.
        val minimizationRoot =
            deskMinimizationRootsByDeskId.values.singleOrNull { it.rootId == taskInfo.parentTaskId }
        if (minimizationRoot != null) {
            val deskId = minimizationRoot.deskId
            val taskId = taskInfo.taskId
            logV("Task #$taskId was minimized in desk #$deskId ")
            addChildToMinimizationRoot(taskId = taskId, deskId = deskId)
            return
        }
        // The appearing task is a root (either a desk or a minimization root), it should not exist
        // already.
        check(taskInfo.taskId !in deskRootsByDeskId) {
            "A root already exists for desk: ${taskInfo.taskId}"
        }
        check(deskMinimizationRootsByDeskId.values.none { it.rootId == taskInfo.taskId }) {
            "A minimization root already exists with rootId: ${taskInfo.taskId}"
        }

        val appearingInDisplayId = taskInfo.displayId
        // Check if there's any pending desk creation requests under this display.
        val deskRequest =
            createDeskRootRequests.firstOrNull { it.displayId == appearingInDisplayId }
        if (deskRequest != null) {
            // Appearing root matches desk request.
            val deskId = taskInfo.taskId
            logV("Desk #$deskId appeared")
        roots[deskId] = DeskRoot(deskId, taskInfo, leash)
        deskCreateRequests.remove(request)
        request.onCreateCallback.onCreated(deskId)
            deskRootsByDeskId[deskId] = DeskRoot(deskId, taskInfo, leash)
            createDeskRootRequests.remove(deskRequest)
            deskRequest.onCreateCallback.onCreated(deskId)
            createDeskMinimizationRoot(displayId = appearingInDisplayId, deskId = deskId)
            return
        }
        // Check if there's any pending minimization container creation requests under this display.
        val deskMinimizationRootRequest =
            createDeskMinimizationRootRequests.first { it.displayId == appearingInDisplayId }
        val deskId = deskMinimizationRootRequest.deskId
        logV("Minimization container for desk #$deskId appeared with id=${taskInfo.taskId}")
        val deskMinimizationRoot = DeskMinimizationRoot(deskId, taskInfo, leash)
        deskMinimizationRootsByDeskId[deskId] = deskMinimizationRoot
        createDeskMinimizationRootRequests.remove(deskMinimizationRootRequest)
        hideMinimizationRoot(deskMinimizationRoot)
    }

    override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) {
        if (roots.contains(taskInfo.taskId)) {
        if (deskRootsByDeskId.contains(taskInfo.taskId)) {
            val deskId = taskInfo.taskId
            roots[deskId] = roots[deskId].copy(taskInfo = taskInfo)
            deskRootsByDeskId[deskId] = deskRootsByDeskId[deskId].copy(taskInfo = taskInfo)
            logV("Desk #$deskId's task info changed")
            return
        }
        val minimizationRoot =
            deskMinimizationRootsByDeskId.values.find { root -> root.rootId == taskInfo.taskId }
        if (minimizationRoot != null) {
            deskMinimizationRootsByDeskId.remove(minimizationRoot.deskId)
            deskMinimizationRootsByDeskId[minimizationRoot.deskId] =
                minimizationRoot.copy(taskInfo = taskInfo)
            logV("Minimization root for desk#${minimizationRoot.deskId} task info changed")
            return
        }

        val parentTaskId = taskInfo.parentTaskId
        if (parentTaskId in deskRootsByDeskId) {
            val deskId = taskInfo.parentTaskId
            val taskId = taskInfo.taskId
            logV("onTaskInfoChanged: Task #$taskId appeared in desk #$deskId")
            addChildToDesk(taskId = taskId, deskId = deskId)
            return
        }
        // Check whether this task is appearing in a minimization root.
        val parentMinimizationRoot =
            deskMinimizationRootsByDeskId.values.singleOrNull { it.rootId == parentTaskId }
        if (parentMinimizationRoot != null) {
            val deskId = parentMinimizationRoot.deskId
            val taskId = taskInfo.taskId
            logV("onTaskInfoChanged: Task #$taskId was minimized in desk #$deskId ")
            addChildToMinimizationRoot(taskId = taskId, deskId = deskId)
            return
        }
        logE("onTaskInfoChanged: unknown task: ${taskInfo.taskId}")
    }

    override fun onTaskVanished(taskInfo: RunningTaskInfo) {
        if (roots.contains(taskInfo.taskId)) {
        if (deskRootsByDeskId.contains(taskInfo.taskId)) {
            val deskId = taskInfo.taskId
            val deskRoot = roots[deskId]
            val deskRoot = deskRootsByDeskId[deskId]
            // Use the last saved taskInfo to obtain the displayId. Using the local one here will
            // return -1 since the task is not unassociated with a display.
            val displayId = deskRoot.taskInfo.displayId
            logV("Desk #$deskId vanished from display #$displayId")
            roots.remove(deskId)
            deskRootsByDeskId.remove(deskId)
            return
        }
        val deskMinimizationRoot =
            deskMinimizationRootsByDeskId.values.singleOrNull { it.rootId == taskInfo.taskId }
        if (deskMinimizationRoot != null) {
            logV("Minimization root for desk ${deskMinimizationRoot.deskId} vanished")
            deskMinimizationRootsByDeskId.remove(deskMinimizationRoot.deskId)
            return
        }

        // Check whether the vanishing task was a child of any desk.
        // At this point, [parentTaskId] may be unset even if this is a task vanishing from a desk,
        // so search through each root to remove this if it's a child.
        roots.forEach { deskId, deskRoot ->
        deskRootsByDeskId.forEach { deskId, deskRoot ->
            if (deskRoot.children.remove(taskInfo.taskId)) {
                logV("Task #${taskInfo.taskId} vanished from desk #$deskId")
                return
            }
        }
        // Check whether the vanishing task was a child of the minimized root and remove it.
        deskMinimizationRootsByDeskId.values.forEach { root ->
            val taskId = taskInfo.taskId
            if (root.children.remove(taskId)) {
                logV("Task #$taskId vanished from minimization root of desk #${root.deskId}")
                return
            }
        }
    }

    @VisibleForTesting
    data class DeskRoot(
        val deskId: Int,
        val taskInfo: RunningTaskInfo,
        val leash: SurfaceControl,
        val children: MutableSet<Int> = mutableSetOf(),
    private fun createDeskMinimizationRoot(displayId: Int, deskId: Int) {
        createDeskMinimizationRootRequests +=
            CreateDeskMinimizationRootRequest(displayId = displayId, deskId = deskId)
        shellTaskOrganizer.createRootTask(
            displayId,
            WINDOWING_MODE_FREEFORM,
            /* listener = */ this,
            /* removeWithTaskOrganizer = */ true,
        )

    override fun dump(pw: PrintWriter, prefix: String) {
        val innerPrefix = "$prefix  "
        pw.println("$prefix$TAG")
        pw.println("${innerPrefix}Desk Roots:")
        roots.forEach { deskId, root ->
            pw.println("$innerPrefix  #$deskId visible=${root.taskInfo.isVisible}")
            pw.println("$innerPrefix    children=${root.children}")
    }

    @SuppressLint("MissingPermission")
    private fun hideMinimizationRoot(root: DeskMinimizationRoot) {
        shellTaskOrganizer.applyTransaction(
            WindowContainerTransaction().apply { setHidden(root.token, /* hidden= */ true) }
        )
    }

    private fun addChildToDesk(taskId: Int, deskId: Int) {
        roots.forEach { _, deskRoot ->
        deskRootsByDeskId.forEach { _, deskRoot ->
            if (deskRoot.deskId == deskId) {
                deskRoot.children.add(taskId)
            } else {
                deskRoot.children.remove(taskId)
            }
        }
        // A task cannot be in both a desk root and a minimization root at the same time, so make
        // sure to remove them if needed.
        deskMinimizationRootsByDeskId.values.forEach { root -> root.children.remove(taskId) }
    }

    private data class CreateRequest(val displayId: Int, val onCreateCallback: OnCreateCallback)
    private fun addChildToMinimizationRoot(taskId: Int, deskId: Int) {
        deskMinimizationRootsByDeskId.forEach { _, minimizationRoot ->
            if (minimizationRoot.deskId == deskId) {
                minimizationRoot.children += taskId
            } else {
                minimizationRoot.children -= taskId
            }
        }
        // A task cannot be in both a desk root and a minimization root at the same time, so make
        // sure to remove them if needed.
        deskRootsByDeskId.forEach { _, deskRoot -> deskRoot.children -= taskId }
    }

    @VisibleForTesting
    data class DeskRoot(
        val deskId: Int,
        val taskInfo: RunningTaskInfo,
        val leash: SurfaceControl,
        val children: MutableSet<Int> = mutableSetOf(),
    ) {
        val token: WindowContainerToken = taskInfo.token
    }

    @VisibleForTesting
    data class DeskMinimizationRoot(
        val deskId: Int,
        val taskInfo: RunningTaskInfo,
        val leash: SurfaceControl,
        val children: MutableSet<Int> = mutableSetOf(),
    ) {
        val rootId: Int
            get() = taskInfo.taskId

        val token: WindowContainerToken = taskInfo.token
    }

    private data class CreateDeskRequest(
        val displayId: Int,
        val onCreateCallback: OnCreateCallback,
    )

    private data class CreateDeskMinimizationRootRequest(val displayId: Int, val deskId: Int)

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

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

    override fun dump(pw: PrintWriter, prefix: String) {
        val innerPrefix = "$prefix  "
        pw.println("$prefix$TAG")
        pw.println("${innerPrefix}Desk Roots:")
        deskRootsByDeskId.forEach { deskId, root ->
            val minimizationRoot = deskMinimizationRootsByDeskId[deskId]
            pw.println("$innerPrefix  #$deskId visible=${root.taskInfo.isVisible}")
            pw.println("$innerPrefix    displayId=${root.taskInfo.displayId}")
            pw.println("$innerPrefix    children=${root.children}")
            pw.println("$innerPrefix    minimization root:")
            pw.println("$innerPrefix      rootId=${minimizationRoot?.rootId}")
            if (minimizationRoot != null) {
                pw.println("$innerPrefix      children=${minimizationRoot.children}")
            }
        }
    }

    companion object {
        private const val TAG = "RootTaskDesksOrganizer"
    }
+24 −0
Original line number Diff line number Diff line
@@ -3224,6 +3224,30 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase()
        assertThat(runOnTransit.lastInvoked).isEqualTo(transition)
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND)
    fun onDesktopWindowMinimize_minimizesTask() {
        val task = setUpFreeformTask()
        val transition = Binder()
        val runOnTransit = RunOnStartTransitionCallback()
        whenever(
                freeformTaskTransitionStarter.startMinimizedModeTransition(
                    any(),
                    anyInt(),
                    anyBoolean(),
                )
            )
            .thenReturn(transition)
        whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task), any()))
            .thenReturn(
                ExitResult.Exit(exitingTask = task.taskId, runOnTransitionStart = runOnTransit)
            )

        controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON)

        verify(desksOrganizer).minimizeTask(any(), /* deskId= */ eq(0), eq(task))
    }

    @Test
    fun onDesktopWindowMinimize_triesToStopTiling() {
        val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY)
Loading