diff options
7 files changed, 657 insertions, 119 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index c60fc1646c32..7c6cf4a8b37f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -332,29 +332,22 @@ class DesktopRepository( return false } - /** - * Adds given task to the closing task list for [displayId]'s active desk. - * - * TODO: b/389960283 - add explicit [deskId] argument. - */ - fun addClosingTask(displayId: Int, taskId: Int) { - val activeDesk = - desktopData.getActiveDesk(displayId) - ?: error("Expected active desk in display: $displayId") - if (activeDesk.closingTasks.add(taskId)) { - logD( - "Added closing task=%d displayId=%d deskId=%d", - taskId, - displayId, - activeDesk.deskId, - ) + /** Adds given task to the closing task list of its desk. */ + fun addClosingTask(displayId: Int, deskId: Int?, taskId: Int) { + val desk = + deskId?.let { desktopData.getDesk(it) } + ?: checkNotNull(desktopData.getActiveDesk(displayId)) { + "Expected active desk in display: $displayId" + } + if (desk.closingTasks.add(taskId)) { + logD("Added closing task=%d displayId=%d deskId=%d", taskId, displayId, desk.deskId) } else { // If the task hasn't been removed from closing list after it disappeared. logW( "Task with taskId=%d displayId=%d deskId=%d is already closing", taskId, displayId, - activeDesk.deskId, + desk.deskId, ) } } @@ -392,7 +385,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 = @@ -402,14 +396,27 @@ class DesktopRepository( desktopData.getAllActiveDesks() } return activeDesks.any { desk -> - desk.visibleTasks - .subtract(desk.closingTasks) - .subtract(desk.minimizedTasks) - .singleOrNull() == taskId + 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. * * TODO: b/389960283 - migrate callers to [getActiveTaskIdsInDesk]. 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 0fbb84075179..45adfe4112a6 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 @@ -802,6 +802,9 @@ class DesktopTasksController( ): ((IBinder) -> Unit) { val taskId = taskInfo.taskId val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) + if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + error("Did not find desk for task: $taskId") + } snapEventHandler.removeTaskIfTiled(displayId, taskId) val shouldExitDesktop = willExitDesktop( @@ -819,7 +822,7 @@ class DesktopTasksController( shouldEndUpAtHome = true, ) - taskRepository.addClosingTask(displayId, taskId) + taskRepository.addClosingTask(displayId = displayId, deskId = deskId, taskId = taskId) taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding(displayId, taskId) ) @@ -871,6 +874,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() @@ -891,10 +898,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( @@ -1232,9 +1255,9 @@ class DesktopTasksController( // home. if (Flags.enablePerDisplayDesktopWallpaperActivity()) { performDesktopExitCleanupIfNeeded( - task.taskId, - task.displayId, - wct, + taskId = task.taskId, + displayId = task.displayId, + wct = wct, forceToFullscreen = false, // TODO: b/371096166 - Temporary turing home relaunch off to prevent home stealing // display focus. Remove shouldEndUpAtHome = false when home focus handling @@ -1801,6 +1824,7 @@ class DesktopTasksController( private fun performDesktopExitCleanupIfNeeded( taskId: Int, + deskId: Int? = null, displayId: Int, wct: WindowContainerTransaction, forceToFullscreen: Boolean, @@ -1814,13 +1838,14 @@ class DesktopTasksController( // |RunOnTransitStart| when the transition is started. return performDesktopExitCleanUp( wct = wct, - deskId = null, + deskId = deskId, displayId = displayId, willExitDesktop = true, shouldEndUpAtHome = shouldEndUpAtHome, ) } + /** TODO: b/394268248 - update [deskId] to be non-null. */ private fun performDesktopExitCleanUp( wct: WindowContainerTransaction, deskId: Int?, @@ -2373,17 +2398,28 @@ class DesktopTasksController( ): WindowContainerTransaction? { logV("handleTaskClosing") if (!isDesktopModeShowing(task.displayId)) return null + val deskId = taskRepository.getDeskIdForTask(task.taskId) + if (deskId == null && DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + return null + } val wct = WindowContainerTransaction() - performDesktopExitCleanupIfNeeded( - task.taskId, - task.displayId, - wct, - forceToFullscreen = false, - ) + val deactivationRunnable = + performDesktopExitCleanupIfNeeded( + taskId = task.taskId, + deskId = deskId, + displayId = task.displayId, + wct = wct, + forceToFullscreen = false, + ) + deactivationRunnable?.invoke(transition) if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { - taskRepository.addClosingTask(task.displayId, task.taskId) + taskRepository.addClosingTask( + displayId = task.displayId, + deskId = deskId, + taskId = task.taskId, + ) snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId) } @@ -2587,9 +2623,9 @@ class DesktopTasksController( wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) performDesktopExitCleanupIfNeeded( - taskInfo.taskId, - taskInfo.displayId, - wct, + taskId = taskInfo.taskId, + displayId = taskInfo.displayId, + wct = wct, forceToFullscreen = false, shouldEndUpAtHome = false, ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt index 0f2f3711a9a3..fc359d7d67b6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -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. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt index 339932cabd2c..f576258ebdaa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt @@ -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,51 +164,176 @@ 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" - } - logV("Desk #$deskId appeared") - roots[deskId] = DeskRoot(deskId, taskInfo, leash) - deskCreateRequests.remove(request) - request.onCreateCallback.onCreated(deskId) + // 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") + 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 + } + } + } + + private fun createDeskMinimizationRoot(displayId: Int, deskId: Int) { + createDeskMinimizationRootRequests += + CreateDeskMinimizationRootRequest(displayId = displayId, deskId = deskId) + shellTaskOrganizer.createRootTask( + displayId, + WINDOWING_MODE_FREEFORM, + /* listener = */ this, + /* removeWithTaskOrganizer = */ true, + ) + } + + @SuppressLint("MissingPermission") + private fun hideMinimizationRoot(root: DeskMinimizationRoot) { + shellTaskOrganizer.applyTransaction( + WindowContainerTransaction().apply { setHidden(root.token, /* hidden= */ true) } + ) + } + + private fun addChildToDesk(taskId: Int, deskId: Int) { + 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 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 @@ -168,34 +342,55 @@ class RootTaskDesksOrganizer( 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:") - roots.forEach { deskId, root -> + 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}") - } - } - - private fun addChildToDesk(taskId: Int, deskId: Int) { - roots.forEach { _, deskRoot -> - if (deskRoot.deskId == deskId) { - deskRoot.children.add(taskId) - } else { - deskRoot.children.remove(taskId) + pw.println("$innerPrefix minimization root:") + pw.println("$innerPrefix rootId=${minimizationRoot?.rootId}") + if (minimizationRoot != null) { + pw.println("$innerPrefix children=${minimizationRoot.children}") } } } - private data class CreateRequest(val displayId: Int, val onCreateCallback: OnCreateCallback) - - private fun logV(msg: String, vararg arguments: Any?) { - ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) - } - companion object { private const val TAG = "RootTaskDesksOrganizer" } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index ed9b97d264f7..9bff287e314a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -333,7 +333,7 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { @Test fun isOnlyVisibleNonClosingTask_singleVisibleClosingTask() { repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) - repo.addClosingTask(DEFAULT_DISPLAY, 1) + repo.addClosingTask(displayId = DEFAULT_DISPLAY, deskId = 0, taskId = 1) // A visible task that's closing assertThat(repo.isVisibleTask(1)).isTrue() 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 fcd92ac2678a..2e63c4f51792 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 @@ -2827,7 +2827,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun onDesktopWindowClose_singleActiveTask_isClosing() { val task = setUpFreeformTask() - taskRepository.addClosingTask(DEFAULT_DISPLAY, task.taskId) + taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, deskId = 0, taskId = task.taskId) val wct = WindowContainerTransaction() controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) @@ -2864,7 +2864,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() - taskRepository.addClosingTask(DEFAULT_DISPLAY, task2.taskId) + taskRepository.addClosingTask( + displayId = DEFAULT_DISPLAY, + deskId = 0, + taskId = task2.taskId, + ) val wct = WindowContainerTransaction() controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1) @@ -3225,6 +3229,30 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @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) val transition = Binder() @@ -3972,7 +4000,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + taskRepository.addClosingTask( + displayId = DEFAULT_DISPLAY, + deskId = 0, + taskId = task2.taskId, + ) val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) @@ -4083,6 +4115,36 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_closeTransition_onlyDesktopTask_deactivatesDesk() { + val task = setUpFreeformTask() + + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + verify(desksOrganizer).deactivateDesk(any(), /* deskId= */ eq(0)) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun handleRequest_closeTransition_onlyDesktopTask_addsDeactivatesDeskTransition() { + val transition = Binder() + val task = setUpFreeformTask() + + controller.handleRequest(transition, createTransition(task, type = TRANSIT_CLOSE)) + + verify(desksTransitionsObserver) + .addPendingTransition(DeskTransition.DeactivateDesk(token = transition, deskId = 0)) + } + + @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_closeTransition_multipleTasks_noWallpaper_doesNotHandle() { val task1 = setUpFreeformTask() @@ -4115,7 +4177,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + taskRepository.addClosingTask( + displayId = DEFAULT_DISPLAY, + deskId = 0, + taskId = task2.taskId, + ) val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt index 8b10ca1a2a70..96b85ad2729e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -22,6 +22,7 @@ import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.Change import android.window.WindowContainerTransaction.HierarchyOp import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT import androidx.test.filters.SmallTest @@ -29,15 +30,19 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskMinimizationRoot import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRoot import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat +import kotlin.test.assertNotNull import org.junit.Assert.assertEquals import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.mockito.kotlin.argThat import org.mockito.kotlin.mock /** @@ -75,6 +80,43 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test + fun testCreateDesk_createsMinimizationRoot() { + val callback = FakeOnCreateCallback() + organizer.createDesk(Display.DEFAULT_DISPLAY, callback) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + + val minimizationRoot = organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId] + assertNotNull(minimizationRoot) + assertThat(minimizationRoot.deskId).isEqualTo(freeformRoot.taskId) + assertThat(minimizationRoot.rootId).isEqualTo(minimizationRootTask.taskId) + } + + @Test + fun testCreateMinimizationRoot_marksHidden() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + + verify(mockShellTaskOrganizer) + .applyTransaction( + argThat { wct -> + wct.changes.any { change -> + change.key == minimizationRootTask.token.asBinder() && + (change.value.changeMask and Change.CHANGE_HIDDEN != 0) && + change.value.hidden + } + } + ) + } + + @Test fun testOnTaskAppeared_withoutRequest_throws() { val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } @@ -105,57 +147,122 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test + fun testOnTaskAppeared_duplicateMinimizedRoot_throws() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + val minimizationRootTask = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + + assertThrows(Exception::class.java) { + organizer.onTaskAppeared(minimizationRootTask, SurfaceControl()) + } + } + + @Test fun testOnTaskVanished_removesRoot() { val desk = createDesk() - organizer.onTaskVanished(desk.taskInfo) + organizer.onTaskVanished(desk.deskRoot.taskInfo) + + assertThat(organizer.deskRootsByDeskId.contains(desk.deskRoot.deskId)).isFalse() + } + + @Test + fun testOnTaskVanished_removesMinimizedRoot() { + val desk = createDesk() + + organizer.onTaskVanished(desk.deskRoot.taskInfo) + organizer.onTaskVanished(desk.minimizationRoot.taskInfo) - assertThat(organizer.roots.contains(desk.deskId)).isFalse() + assertThat(organizer.deskMinimizationRootsByDeskId.contains(desk.deskRoot.deskId)).isFalse() } @Test fun testDesktopWindowAppearsInDesk() { val desk = createDesk() - val child = createFreeformTask().apply { parentTaskId = desk.deskId } + val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } organizer.onTaskAppeared(child, SurfaceControl()) - assertThat(desk.children).contains(child.taskId) + assertThat(desk.deskRoot.children).contains(child.taskId) + } + + @Test + fun testDesktopWindowAppearsInDeskMinimizationRoot() { + val desk = createDesk() + val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } + + organizer.onTaskAppeared(child, SurfaceControl()) + + assertThat(desk.minimizationRoot.children).contains(child.taskId) + } + + @Test + fun testDesktopWindowMovesToMinimizationRoot() { + val desk = createDesk() + val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + organizer.onTaskAppeared(child, SurfaceControl()) + + child.parentTaskId = desk.minimizationRoot.rootId + organizer.onTaskInfoChanged(child) + + assertThat(desk.deskRoot.children).doesNotContain(child.taskId) + assertThat(desk.minimizationRoot.children).contains(child.taskId) } @Test fun testDesktopWindowDisappearsFromDesk() { val desk = createDesk() - val child = createFreeformTask().apply { parentTaskId = desk.deskId } + val child = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } organizer.onTaskAppeared(child, SurfaceControl()) organizer.onTaskVanished(child) - assertThat(desk.children).doesNotContain(child.taskId) + assertThat(desk.deskRoot.children).doesNotContain(child.taskId) } @Test - fun testRemoveDesk() { + fun testDesktopWindowDisappearsFromDeskMinimizationRoot() { + val desk = createDesk() + val child = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } + + organizer.onTaskAppeared(child, SurfaceControl()) + organizer.onTaskVanished(child) + + assertThat(desk.minimizationRoot.children).doesNotContain(child.taskId) + } + + @Test + fun testRemoveDesk_removesDeskRoot() { val desk = createDesk() val wct = WindowContainerTransaction() - organizer.removeDesk(wct, desk.deskId) + organizer.removeDesk(wct, desk.deskRoot.deskId) assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK && - hop.container == desk.taskInfo.token.asBinder() + hop.container == desk.deskRoot.token.asBinder() } ) .isTrue() } @Test - fun testRemoveDesk_didNotExist_throws() { - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + fun testRemoveDesk_removesMinimizationRoot() { + val desk = createDesk() val wct = WindowContainerTransaction() - assertThrows(Exception::class.java) { organizer.removeDesk(wct, freeformRoot.taskId) } + organizer.removeDesk(wct, desk.deskRoot.deskId) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK && + hop.container == desk.minimizationRoot.token.asBinder() + } + ) + .isTrue() } @Test @@ -163,20 +270,20 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { val desk = createDesk() val wct = WindowContainerTransaction() - organizer.activateDesk(wct, desk.deskId) + organizer.activateDesk(wct, desk.deskRoot.deskId) assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REORDER && hop.toTop && - hop.container == desk.taskInfo.token.asBinder() + hop.container == desk.deskRoot.taskInfo.token.asBinder() } ) .isTrue() assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT && - hop.container == desk.taskInfo.token.asBinder() + hop.container == desk.deskRoot.taskInfo.token.asBinder() } ) .isTrue() @@ -196,14 +303,14 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { val desktopTask = createFreeformTask().apply { parentTaskId = -1 } val wct = WindowContainerTransaction() - organizer.moveTaskToDesk(wct, desk.deskId, desktopTask) + organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, desktopTask) assertThat( wct.hierarchyOps.any { hop -> hop.isReparent && hop.toTop && hop.container == desktopTask.token.asBinder() && - hop.newParent == desk.taskInfo.token.asBinder() + hop.newParent == desk.deskRoot.taskInfo.token.asBinder() } ) .isTrue() @@ -231,13 +338,26 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun testGetDeskAtEnd() { val desk = createDesk() - val task = createFreeformTask().apply { parentTaskId = desk.deskId } + val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + val endDesk = + organizer.getDeskAtEnd( + TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } + ) + + assertThat(endDesk).isEqualTo(desk.deskRoot.deskId) + } + + @Test + fun testGetDeskAtEnd_inMinimizationRoot() { + val desk = createDesk() + + val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } val endDesk = organizer.getDeskAtEnd( TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } ) - assertThat(endDesk).isEqualTo(desk.deskId) + assertThat(endDesk).isEqualTo(desk.deskRoot.deskId) } @Test @@ -264,14 +384,14 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { fun deactivateDesk_clearsLaunchRoot() { val wct = WindowContainerTransaction() val desk = createDesk() - organizer.activateDesk(wct, desk.deskId) + organizer.activateDesk(wct, desk.deskRoot.deskId) - organizer.deactivateDesk(wct, desk.deskId) + organizer.deactivateDesk(wct, desk.deskRoot.deskId) assertThat( wct.hierarchyOps.any { hop -> hop.type == HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT && - hop.container == desk.taskInfo.token.asBinder() && + hop.container == desk.deskRoot.taskInfo.token.asBinder() && hop.windowingModes == null && hop.activityTypes == null } @@ -280,25 +400,129 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { } @Test - fun isDeskChange() { + fun isDeskChange_forDeskId() { val desk = createDesk() assertThat( organizer.isDeskChange( - TransitionInfo.Change(desk.taskInfo.token, desk.leash).apply { - taskInfo = desk.taskInfo + TransitionInfo.Change(desk.deskRoot.taskInfo.token, desk.deskRoot.leash).apply { + taskInfo = desk.deskRoot.taskInfo }, - desk.deskId, + desk.deskRoot.deskId, + ) + ) + .isTrue() + } + + @Test + fun isDeskChange_forDeskId_inMinimizationRoot() { + val desk = createDesk() + + assertThat( + organizer.isDeskChange( + change = + TransitionInfo.Change( + desk.minimizationRoot.token, + desk.minimizationRoot.leash, + ) + .apply { taskInfo = desk.minimizationRoot.taskInfo }, + deskId = desk.deskRoot.deskId, + ) + ) + .isTrue() + } + + @Test + fun isDeskChange_anyDesk() { + val desk = createDesk() + + assertThat( + organizer.isDeskChange( + change = + TransitionInfo.Change(desk.deskRoot.taskInfo.token, desk.deskRoot.leash) + .apply { taskInfo = desk.deskRoot.taskInfo } + ) + ) + .isTrue() + } + + @Test + fun isDeskChange_anyDesk_inMinimizationRoot() { + val desk = createDesk() + + assertThat( + organizer.isDeskChange( + change = + TransitionInfo.Change( + desk.minimizationRoot.taskInfo.token, + desk.minimizationRoot.leash, + ) + .apply { taskInfo = desk.minimizationRoot.taskInfo } ) ) .isTrue() } - private fun createDesk(): DeskRoot { + @Test + fun minimizeTask() { + val desk = createDesk() + val task = createFreeformTask().apply { parentTaskId = desk.deskRoot.deskId } + val wct = WindowContainerTransaction() + organizer.moveTaskToDesk(wct, desk.deskRoot.deskId, task) + organizer.onTaskAppeared(task, SurfaceControl()) + + organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.isReparent && + hop.container == task.token.asBinder() && + hop.newParent == desk.minimizationRoot.token.asBinder() + } + ) + .isTrue() + } + + @Test + fun minimizeTask_alreadyMinimized_noOp() { + val desk = createDesk() + val task = createFreeformTask().apply { parentTaskId = desk.minimizationRoot.rootId } + val wct = WindowContainerTransaction() + organizer.onTaskAppeared(task, SurfaceControl()) + + organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task) + + assertThat(wct.isEmpty).isTrue() + } + + @Test + fun minimizeTask_inDifferentDesk_noOp() { + val desk = createDesk() + val otherDesk = createDesk() + val task = createFreeformTask().apply { parentTaskId = otherDesk.deskRoot.deskId } + val wct = WindowContainerTransaction() + organizer.onTaskAppeared(task, SurfaceControl()) + + organizer.minimizeTask(wct, deskId = desk.deskRoot.deskId, task) + + assertThat(wct.isEmpty).isTrue() + } + + private data class DeskRoots( + val deskRoot: DeskRoot, + val minimizationRoot: DeskMinimizationRoot, + ) + + private fun createDesk(): DeskRoots { organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - return organizer.roots[freeformRoot.taskId] + val minimizationRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(minimizationRoot, SurfaceControl()) + return DeskRoots( + organizer.deskRootsByDeskId[freeformRoot.taskId], + checkNotNull(organizer.deskMinimizationRootsByDeskId[freeformRoot.taskId]), + ) } private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback { |