diff options
| author | 2024-04-02 16:10:29 +0000 | |
|---|---|---|
| committer | 2024-04-19 16:34:26 +0000 | |
| commit | 1096b70b427df48f0db5458083ad7050c7b156dc (patch) | |
| tree | 90f69ea432f3cefde537c5b3bd0330d97d8a994b | |
| parent | b43e44ee9a7a69abbe22d8568c6f702127b0a241 (diff) | |
Minimize a Task whenever we reach the Desktop Task limit
- Introduce DesktopTasksLimiter to limit the number of visible Desktop
Tasks
- Keep track of minimized Desktop Tasks in DesktopModeTaskRepository
- Call into DesktopTasksLimiter from DesktopTasksController whenever we
handle a transition that might cause us to hit the task limit.
- to minimize a Task we reorder it to the bottom of the Desktop Tasks,
placing it behind the Home Task. We mark the task as minimized in
DesktopModeTaskRepository later when the transition is ready.
- Add a SystemProperty 'persist.wm.debug.desktop_max_task_limit' to
control the maximum Task limit. To update the limit to X tasks, run:
'adb shell setprop persist.wm.debug.desktop_max_task_limit X'
Bug: 332503075, 316115519
Test: unit tests
Change-Id: Ia34b67b4628d50e99347d9680e5a869ca666eaa3
11 files changed, 945 insertions, 40 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 1408eadf544e..26e7acbedd51 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -29,6 +29,7 @@ import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.UiEventLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.launcher3.icons.IconProvider; +import com.android.window.flags.Flags; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.WindowManagerShellWrapper; @@ -59,6 +60,7 @@ import com.android.wm.shell.desktopmode.DesktopModeLoggerTransitionObserver; import com.android.wm.shell.desktopmode.DesktopModeStatus; import com.android.wm.shell.desktopmode.DesktopModeTaskRepository; import com.android.wm.shell.desktopmode.DesktopTasksController; +import com.android.wm.shell.desktopmode.DesktopTasksLimiter; import com.android.wm.shell.desktopmode.DesktopTasksTransitionObserver; import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler; @@ -517,23 +519,39 @@ public abstract class WMShellModule { LaunchAdjacentController launchAdjacentController, RecentsTransitionHandler recentsTransitionHandler, MultiInstanceHelper multiInstanceHelper, - @ShellMainThread ShellExecutor mainExecutor - ) { + @ShellMainThread ShellExecutor mainExecutor, + Optional<DesktopTasksLimiter> desktopTasksLimiter) { return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController, displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, dragAndDropController, transitions, enterDesktopTransitionHandler, exitDesktopTransitionHandler, toggleResizeDesktopTaskTransitionHandler, dragToDesktopTransitionHandler, desktopModeTaskRepository, desktopModeLoggerTransitionObserver, launchAdjacentController, - recentsTransitionHandler, multiInstanceHelper, mainExecutor); + recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter); } @WMSingleton @Provides + static Optional<DesktopTasksLimiter> provideDesktopTasksLimiter( + Transitions transitions, + @DynamicOverride DesktopModeTaskRepository desktopModeTaskRepository, + ShellTaskOrganizer shellTaskOrganizer) { + if (!DesktopModeStatus.isEnabled() || !Flags.enableDesktopWindowingTaskLimit()) { + return Optional.empty(); + } + return Optional.of( + new DesktopTasksLimiter( + transitions, desktopModeTaskRepository, shellTaskOrganizer)); + } + + + @WMSingleton + @Provides static DragToDesktopTransitionHandler provideDragToDesktopTransitionHandler( Context context, Transitions transitions, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, + Optional<DesktopTasksLimiter> desktopTasksLimiter) { return new DragToDesktopTransitionHandler(context, transitions, rootTaskDisplayAreaOrganizer); } @@ -541,7 +559,8 @@ public abstract class WMShellModule { @WMSingleton @Provides static EnterDesktopTaskTransitionHandler provideEnterDesktopModeTaskTransitionHandler( - Transitions transitions) { + Transitions transitions, + Optional<DesktopTasksLimiter> desktopTasksLimiter) { return new EnterDesktopTaskTransitionHandler(transitions); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java index 32c22c01a828..fcddcad3a949 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -77,6 +77,22 @@ public class DesktopModeStatus { "persist.wm.debug.desktop_mode_enforce_device_restrictions", true); /** + * Default value for {@code MAX_TASK_LIMIT}. + */ + @VisibleForTesting + public static final int DEFAULT_MAX_TASK_LIMIT = 4; + + // TODO(b/335131008): add a config-overlay field for the max number of tasks in Desktop Mode + /** + * Flag declaring the maximum number of Tasks to show in Desktop Mode at any one time. + * + * <p> The limit does NOT affect Picture-in-Picture, Bubbles, or System Modals (like a screen + * recording window, or Bluetooth pairing window). + */ + private static final int MAX_TASK_LIMIT = SystemProperties.getInt( + "persist.wm.debug.desktop_max_task_limit", DEFAULT_MAX_TASK_LIMIT); + + /** * Return {@code true} if desktop windowing is enabled */ public static boolean isEnabled() { @@ -124,6 +140,13 @@ public class DesktopModeStatus { } /** + * Return the maximum limit on the number of Tasks to show in Desktop Mode at any one time. + */ + static int getMaxTaskLimit() { + return MAX_TASK_LIMIT; + } + + /** * Return {@code true} if the current device supports desktop mode. */ @VisibleForTesting diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index 50cea01fa281..2d508b2e6e3d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -47,6 +47,7 @@ class DesktopModeTaskRepository { */ val activeTasks: ArraySet<Int> = ArraySet(), val visibleTasks: ArraySet<Int> = ArraySet(), + val minimizedTasks: ArraySet<Int> = ArraySet(), var stashed: Boolean = false ) @@ -202,6 +203,13 @@ class DesktopModeTaskRepository { } } + /** Return whether the given Task is minimized. */ + fun isMinimizedTask(taskId: Int): Boolean { + return displayData.valueIterator().asSequence().any { data -> + data.minimizedTasks.contains(taskId) + } + } + /** * Check if a task with the given [taskId] is the only active task on its display */ @@ -219,6 +227,25 @@ class DesktopModeTaskRepository { } /** + * Returns whether Desktop Mode is currently showing any tasks, i.e. whether any Desktop Tasks + * are visible. + */ + fun isDesktopModeShowing(displayId: Int): Boolean = getVisibleTaskCount(displayId) > 0 + + /** + * Returns a list of Tasks IDs representing all active non-minimized Tasks on the given display, + * ordered from front to back. + */ + fun getActiveNonMinimizedTasksOrderedFrontToBack(displayId: Int): List<Int> { + val activeTasks = getActiveTasks(displayId) + val allTasksInZOrder = getFreeformTasksInZOrder() + return activeTasks + // Don't show already minimized Tasks + .filter { taskId -> !isMinimizedTask(taskId) } + .sortedBy { taskId -> allTasksInZOrder.indexOf(taskId) } + } + + /** * Get a list of freeform tasks, ordered from top-bottom (top at index 0). */ // TODO(b/278084491): pass in display id @@ -255,6 +282,7 @@ class DesktopModeTaskRepository { val prevCount = getVisibleTaskCount(displayId) if (visible) { displayData.getOrCreate(displayId).visibleTasks.add(taskId) + unminimizeTask(displayId, taskId) } else { displayData[displayId]?.visibleTasks?.remove(taskId) } @@ -312,6 +340,24 @@ class DesktopModeTaskRepository { freeformTasksInZOrder.add(0, taskId) } + /** Mark a Task as minimized. */ + fun minimizeTask(displayId: Int, taskId: Int) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeTaskRepository: minimize Task: display=%d, task=%d", + displayId, taskId) + displayData.getOrCreate(displayId).minimizedTasks.add(taskId) + } + + /** Mark a Task as non-minimized. */ + fun unminimizeTask(displayId: Int, taskId: Int) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopModeTaskRepository: unminimize Task: display=%d, task=%d", + displayId, taskId) + displayData[displayId]?.minimizedTasks?.remove(taskId) + } + /** * Remove the task from the ordered list. */ @@ -325,7 +371,7 @@ class DesktopModeTaskRepository { boundsBeforeMaximizeByTaskId.remove(taskId) KtProtoLog.d( WM_SHELL_DESKTOP_MODE, - "DesktopTaskRepo: remaining freeform tasks: " + freeformTasksInZOrder.toDumpString() + "DesktopTaskRepo: remaining freeform tasks: %s", freeformTasksInZOrder.toDumpString(), ) } 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 068661a6a666..0a9e5d0d5345 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 @@ -88,6 +88,7 @@ import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener import com.android.wm.shell.windowdecor.extension.isFreeform import com.android.wm.shell.windowdecor.extension.isFullscreen import java.io.PrintWriter +import java.util.Optional import java.util.concurrent.Executor import java.util.function.Consumer @@ -113,7 +114,8 @@ class DesktopTasksController( private val launchAdjacentController: LaunchAdjacentController, private val recentsTransitionHandler: RecentsTransitionHandler, private val multiInstanceHelper: MultiInstanceHelper, - @ShellMainThread private val mainExecutor: ShellExecutor + @ShellMainThread private val mainExecutor: ShellExecutor, + private val desktopTasksLimiter: Optional<DesktopTasksLimiter>, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, DragAndDropController.DragAndDropListener { @@ -341,11 +343,13 @@ class DesktopTasksController( ) exitSplitIfApplicable(wct, task) // Bring other apps to front first - bringDesktopAppsToFront(task.displayId, wct) + val taskToMinimize = + bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) addMoveToDesktopChanges(wct, task) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - enterDesktopTaskTransitionHandler.moveToDesktop(wct) + val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct) + addPendingMinimizeTransition(transition, taskToMinimize) } else { shellTaskOrganizer.applyTransaction(wct) } @@ -382,10 +386,14 @@ class DesktopTasksController( ) val wct = WindowContainerTransaction() exitSplitIfApplicable(wct, taskInfo) - bringDesktopAppsToFront(taskInfo.displayId, wct) + moveHomeTaskToFront(wct) + val taskToMinimize = + bringDesktopAppsToFrontBeforeShowingNewTask( + taskInfo.displayId, wct, taskInfo.taskId) addMoveToDesktopChanges(wct, taskInfo) wct.setBounds(taskInfo.token, freeformBounds) - dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct) + val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct) + transition?.let { addPendingMinimizeTransition(it, taskToMinimize) } } /** @@ -507,8 +515,10 @@ class DesktopTasksController( val wct = WindowContainerTransaction() wct.reorder(taskInfo.token, true) + val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo) if (Transitions.ENABLE_SHELL_TRANSITIONS) { - transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */) + val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */) + addPendingMinimizeTransition(transition, taskToMinimize) } else { shellTaskOrganizer.applyTransaction(wct) } @@ -688,9 +698,20 @@ class DesktopTasksController( ?: WINDOWING_MODE_UNDEFINED } - private fun bringDesktopAppsToFront(displayId: Int, wct: WindowContainerTransaction) { - KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: bringDesktopAppsToFront") - val activeTasks = desktopModeTaskRepository.getActiveTasks(displayId) + private fun bringDesktopAppsToFrontBeforeShowingNewTask( + displayId: Int, + wct: WindowContainerTransaction, + newTaskIdInFront: Int + ): RunningTaskInfo? = bringDesktopAppsToFront(displayId, wct, newTaskIdInFront) + + private fun bringDesktopAppsToFront( + displayId: Int, + wct: WindowContainerTransaction, + newTaskIdInFront: Int? = null + ): RunningTaskInfo? { + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: bringDesktopAppsToFront, newTaskIdInFront=%s", + newTaskIdInFront ?: "null") if (Flags.enableDesktopWindowingWallpaperActivity()) { // Add translucent wallpaper activity to show the wallpaper underneath @@ -700,13 +721,21 @@ class DesktopTasksController( moveHomeTaskToFront(wct) } - // Then move other tasks on top of it - val allTasksInZOrder = desktopModeTaskRepository.getFreeformTasksInZOrder() - activeTasks - // Sort descending as the top task is at index 0. It should be ordered to top last - .sortedByDescending { taskId -> allTasksInZOrder.indexOf(taskId) } - .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } - .forEach { task -> wct.reorder(task.token, true /* onTop */) } + val nonMinimizedTasksOrderedFrontToBack = + desktopModeTaskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId) + // If we're adding a new Task we might need to minimize an old one + val taskToMinimize: RunningTaskInfo? = + if (newTaskIdInFront != null && desktopTasksLimiter.isPresent) { + desktopTasksLimiter.get().getTaskToMinimizeIfNeeded( + nonMinimizedTasksOrderedFrontToBack, newTaskIdInFront) + } else { null } + nonMinimizedTasksOrderedFrontToBack + // If there is a Task to minimize, let it stay behind the Home Task + .filter { taskId -> taskId != taskToMinimize?.taskId } + .mapNotNull { taskId -> shellTaskOrganizer.getRunningTaskInfo(taskId) } + .reversed() // Start from the back so the front task is brought forward last + .forEach { task -> wct.reorder(task.token, true /* onTop */) } + return taskToMinimize } private fun moveHomeTaskToFront(wct: WindowContainerTransaction) { @@ -824,13 +853,13 @@ class DesktopTasksController( when { request.type == TRANSIT_TO_BACK -> handleBackNavigation(task) // If display has tasks stashed, handle as stashed launch - task.isStashed -> handleStashedTaskLaunch(task) + task.isStashed -> handleStashedTaskLaunch(task, transition) // Check if the task has a top transparent activity shouldLaunchAsModal(task) -> handleTransparentTaskLaunch(task) // Check if fullscreen task should be updated - task.isFullscreen -> handleFullscreenTaskLaunch(task) + task.isFullscreen -> handleFullscreenTaskLaunch(task, transition) // Check if freeform task should be updated - task.isFreeform -> handleFreeformTaskLaunch(task) + task.isFreeform -> handleFreeformTaskLaunch(task, transition) else -> { null } @@ -878,10 +907,12 @@ class DesktopTasksController( } ?: false } - private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + private fun handleFreeformTaskLaunch( + task: RunningTaskInfo, + transition: IBinder + ): WindowContainerTransaction? { KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFreeformTaskLaunch") - val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) - if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) { + if (!desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: switch freeform task to fullscreen oon transition" + @@ -892,13 +923,23 @@ class DesktopTasksController( addMoveToFullscreenChanges(wct, task) } } + // Desktop Mode is showing and we're launching a new Task - we might need to minimize + // a Task. + val wct = WindowContainerTransaction() + val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) + if (taskToMinimize != null) { + addPendingMinimizeTransition(transition, taskToMinimize) + return wct + } return null } - private fun handleFullscreenTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + private fun handleFullscreenTaskLaunch( + task: RunningTaskInfo, + transition: IBinder + ): WindowContainerTransaction? { KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: handleFullscreenTaskLaunch") - val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) - if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { + if (desktopModeTaskRepository.isDesktopModeShowing(task.displayId)) { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: switch fullscreen task to freeform on transition" + @@ -907,21 +948,30 @@ class DesktopTasksController( ) return WindowContainerTransaction().also { wct -> addMoveToDesktopChanges(wct, task) + // Desktop Mode is already showing and we're launching a new Task - we might need to + // minimize another Task. + val taskToMinimize = addAndGetMinimizeChangesIfNeeded(task.displayId, wct, task) + addPendingMinimizeTransition(transition, taskToMinimize) } } return null } - private fun handleStashedTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction { + private fun handleStashedTaskLaunch( + task: RunningTaskInfo, + transition: IBinder + ): WindowContainerTransaction { KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: launch apps with stashed on transition taskId=%d", task.taskId ) val wct = WindowContainerTransaction() - bringDesktopAppsToFront(task.displayId, wct) + val taskToMinimize = + bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) addMoveToDesktopChanges(wct, task) desktopModeTaskRepository.setStashed(task.displayId, false) + addPendingMinimizeTransition(transition, taskToMinimize) return wct } @@ -1002,6 +1052,28 @@ class DesktopTasksController( wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } + /** Returns the ID of the Task that will be minimized, or null if no task will be minimized. */ + private fun addAndGetMinimizeChangesIfNeeded( + displayId: Int, + wct: WindowContainerTransaction, + newTaskInfo: RunningTaskInfo + ): RunningTaskInfo? { + if (!desktopTasksLimiter.isPresent) return null + return desktopTasksLimiter.get().addAndGetMinimizeTaskChangesIfNeeded( + displayId, wct, newTaskInfo) + } + + private fun addPendingMinimizeTransition( + transition: IBinder, + taskToMinimize: RunningTaskInfo? + ) { + if (taskToMinimize == null) return + desktopTasksLimiter.ifPresent { + it.addPendingMinimizeChange( + transition, taskToMinimize.displayId, taskToMinimize.taskId) + } + } + /** Enter split by using the focused desktop task in given `displayId`. */ fun enterSplit( displayId: Int, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt new file mode 100644 index 000000000000..3404d376fe92 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.os.IBinder +import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.TransitionInfo +import android.window.WindowContainerTransaction +import androidx.annotation.VisibleForTesting +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.protolog.ShellProtoLogGroup +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TransitionObserver +import com.android.wm.shell.util.KtProtoLog + +/** + * Limits the number of tasks shown in Desktop Mode. + * + * This class should only be used if + * [com.android.window.flags.Flags.enableDesktopWindowingTaskLimit()] is true. + */ +class DesktopTasksLimiter ( + transitions: Transitions, + private val taskRepository: DesktopModeTaskRepository, + private val shellTaskOrganizer: ShellTaskOrganizer, +) { + private val minimizeTransitionObserver = MinimizeTransitionObserver() + + init { + transitions.registerObserver(minimizeTransitionObserver) + } + + private data class TaskDetails (val displayId: Int, val taskId: Int) + + // TODO(b/333018485): replace this observer when implementing the minimize-animation + private inner class MinimizeTransitionObserver : TransitionObserver { + private val mPendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>() + + fun addPendingTransitionToken(transition: IBinder, taskDetails: TaskDetails) { + mPendingTransitionTokensAndTasks[transition] = taskDetails + } + + override fun onTransitionReady( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction + ) { + val taskToMinimize = mPendingTransitionTokensAndTasks.remove(transition) ?: return + + if (!taskRepository.isActiveTask(taskToMinimize.taskId)) return + + if (!isTaskReorderedToBackOrInvisible(info, taskToMinimize)) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: task %d is not reordered to back nor invis", + taskToMinimize.taskId) + return + } + this@DesktopTasksLimiter.markTaskMinimized( + taskToMinimize.displayId, taskToMinimize.taskId) + } + + /** + * Returns whether the given Task is being reordered to the back in the given transition, or + * is already invisible. + * + * <p> This check can be used to double-check that a task was indeed minimized before + * marking it as such. + */ + private fun isTaskReorderedToBackOrInvisible( + info: TransitionInfo, + taskDetails: TaskDetails + ): Boolean { + val taskChange = info.changes.find { change -> + change.taskInfo?.taskId == taskDetails.taskId } + if (taskChange == null) { + return !taskRepository.isVisibleTask(taskDetails.taskId) + } + return taskChange.mode == TRANSIT_TO_BACK + } + + override fun onTransitionStarting(transition: IBinder) {} + + override fun onTransitionMerged(merged: IBinder, playing: IBinder) { + mPendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer -> + mPendingTransitionTokensAndTasks[playing] = taskToTransfer + } + } + + override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: transition %s finished", transition) + mPendingTransitionTokensAndTasks.remove(transition) + } + } + + /** + * Mark a task as minimized, this should only be done after the corresponding transition has + * finished so we don't minimize the task if the transition fails. + */ + private fun markTaskMinimized(displayId: Int, taskId: Int) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: marking %d as minimized", taskId) + taskRepository.minimizeTask(displayId, taskId) + } + + /** + * Add a minimize-transition to [wct] if adding [newFrontTaskInfo] brings us over the task + * limit. + * + * @param transition the transition that the minimize-transition will be appended to, or null if + * the transition will be started later. + * @return the ID of the minimized task, or null if no task is being minimized. + */ + fun addAndGetMinimizeTaskChangesIfNeeded( + displayId: Int, + wct: WindowContainerTransaction, + newFrontTaskInfo: RunningTaskInfo, + ): RunningTaskInfo? { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: addMinimizeBackTaskChangesIfNeeded, newFrontTask=%d", + newFrontTaskInfo.taskId) + val newTaskListOrderedFrontToBack = createOrderedTaskListWithGivenTaskInFront( + taskRepository.getActiveNonMinimizedTasksOrderedFrontToBack(displayId), + newFrontTaskInfo.taskId) + val taskToMinimize = getTaskToMinimizeIfNeeded(newTaskListOrderedFrontToBack) + if (taskToMinimize != null) { + wct.reorder(taskToMinimize.token, false /* onTop */) + return taskToMinimize + } + return null + } + + /** + * Add a pending minimize transition change, to update the list of minimized apps once the + * transition goes through. + */ + fun addPendingMinimizeChange(transition: IBinder, displayId: Int, taskId: Int) { + minimizeTransitionObserver.addPendingTransitionToken( + transition, TaskDetails(displayId, taskId)) + } + + /** + * Returns the maximum number of tasks that should ever be displayed at the same time in Desktop + * Mode. + */ + fun getMaxTaskLimit(): Int = DesktopModeStatus.getMaxTaskLimit() + + /** + * Returns the Task to minimize given 1. a list of visible tasks ordered from front to back and + * 2. a new task placed in front of all the others. + */ + fun getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack: List<Int>, + newTaskIdInFront: Int + ): RunningTaskInfo? { + return getTaskToMinimizeIfNeeded( + createOrderedTaskListWithGivenTaskInFront( + visibleFreeformTaskIdsOrderedFrontToBack, newTaskIdInFront)) + } + + /** Returns the Task to minimize given a list of visible tasks ordered from front to back. */ + fun getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack: List<Int> + ): RunningTaskInfo? { + if (visibleFreeformTaskIdsOrderedFrontToBack.size <= getMaxTaskLimit()) { + KtProtoLog.v( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: no need to minimize; tasks below limit") + // No need to minimize anything + return null + } + val taskToMinimize = + shellTaskOrganizer.getRunningTaskInfo( + visibleFreeformTaskIdsOrderedFrontToBack.last()) + if (taskToMinimize == null) { + KtProtoLog.e( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DesktopTasksLimiter: taskToMinimize == null") + return null + } + return taskToMinimize + } + + private fun createOrderedTaskListWithGivenTaskInFront( + existingTaskIdsOrderedFrontToBack: List<Int>, + newTaskId: Int + ): List<Int> { + return listOf(newTaskId) + + existingTaskIdsOrderedFrontToBack.filter { taskId -> taskId != newTaskId } + } + + @VisibleForTesting + fun getTransitionObserver(): TransitionObserver { + return minimizeTransitionObserver + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 0061d03af8e9..e341f2d4d4b4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -150,20 +150,20 @@ class DragToDesktopTransitionHandler( * windowing mode changes to the dragged task. This is called when the dragged task is released * inside the desktop drop zone. */ - fun finishDragToDesktopTransition(wct: WindowContainerTransaction) { + fun finishDragToDesktopTransition(wct: WindowContainerTransaction): IBinder? { if (!inProgress) { // Don't attempt to finish a drag to desktop transition since there is no transition in // progress which means that the drag to desktop transition was never successfully // started. - return + return null } if (requireTransitionState().startAborted) { // Don't attempt to complete the drag-to-desktop since the start transition didn't // succeed as expected. Just reset the state as if nothing happened. clearState() - return + return null } - transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) + return transitions.startTransition(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, wct, this) } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java index 79bb5408df82..74b8f831cdc0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/EnterDesktopTaskTransitionHandler.java @@ -78,10 +78,12 @@ public class EnterDesktopTaskTransitionHandler implements Transitions.Transition /** * Starts Transition of type TRANSIT_MOVE_TO_DESKTOP * @param wct WindowContainerTransaction for transition + * @return the token representing the started transition */ - public void moveToDesktop(@NonNull WindowContainerTransaction wct) { + public IBinder moveToDesktop(@NonNull WindowContainerTransaction wct) { final IBinder token = mTransitions.startTransition(TRANSIT_MOVE_TO_DESKTOP, wct, this); mPendingTransitionTokens.add(token); + return token; } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index f2bdcae31956..6fea2036dbd1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -95,6 +95,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.isEnabled()) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.taskId); + repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); if (taskInfo.isVisible) { if (repository.addActiveTask(taskInfo.displayId, taskInfo.taskId)) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, @@ -116,6 +117,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.isEnabled()) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.removeFreeformTask(taskInfo.taskId); + repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); if (repository.removeActiveTask(taskInfo.taskId)) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, "Removing active freeform task: #%d", taskInfo.taskId); @@ -162,6 +164,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, if (DesktopModeStatus.isEnabled() && taskInfo.isFocused) { mDesktopModeTaskRepository.ifPresent(repository -> { repository.addOrMoveFreeformTaskToTop(taskInfo.taskId); + repository.unminimizeTask(taskInfo.displayId, taskInfo.taskId); }); } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index b2b54acf4585..dca7be12fffc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -483,6 +483,102 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull() } + @Test + fun minimizeTaskNotCalled_noTasksMinimized() { + assertThat(repo.isMinimizedTask(taskId = 0)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() + } + + @Test + fun minimizeTask_onlyThatTaskIsMinimized() { + repo.minimizeTask(displayId = 0, taskId = 0) + + assertThat(repo.isMinimizedTask(taskId = 0)).isTrue() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + } + + @Test + fun unminimizeTask_taskNoLongerMinimized() { + repo.minimizeTask(displayId = 0, taskId = 0) + repo.unminimizeTask(displayId = 0, taskId = 0) + + assertThat(repo.isMinimizedTask(taskId = 0)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + } + + @Test + fun unminimizeTask_nonExistentTask_doesntCrash() { + repo.unminimizeTask(displayId = 0, taskId = 0) + + assertThat(repo.isMinimizedTask(taskId = 0)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 1)).isFalse() + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + } + + + @Test + fun updateVisibleFreeformTasks_toVisible_taskIsUnminimized() { + repo.minimizeTask(displayId = 10, taskId = 2) + + repo.updateVisibleFreeformTasks(displayId = 10, taskId = 2, visible = true) + + assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() + } + + @Test + fun isDesktopModeShowing_noActiveTasks_returnsFalse() { + assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + fun isDesktopModeShowing_noTasksVisible_returnsFalse() { + repo.addActiveTask(displayId = 0, taskId = 1) + repo.addActiveTask(displayId = 0, taskId = 2) + + assertThat(repo.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() { + repo.addActiveTask(displayId = 0, taskId = 1) + repo.addActiveTask(displayId = 0, taskId = 2) + repo.updateVisibleFreeformTasks(displayId = 0, taskId = 1, visible = true) + + assertThat(repo.isDesktopModeShowing(displayId = 0)).isTrue() + } + + @Test + fun getActiveNonMinimizedTasksOrderedFrontToBack_returnsFreeformTasksInCorrectOrder() { + repo.addActiveTask(displayId = 0, taskId = 1) + repo.addActiveTask(displayId = 0, taskId = 2) + repo.addActiveTask(displayId = 0, taskId = 3) + // The front-most task will be the one added last through addOrMoveFreeformTaskToTop + repo.addOrMoveFreeformTaskToTop(taskId = 3) + repo.addOrMoveFreeformTaskToTop(taskId = 2) + repo.addOrMoveFreeformTaskToTop(taskId = 1) + + assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0)).isEqualTo( + listOf(1, 2, 3)) + } + + @Test + fun getActiveNonMinimizedTasksOrderedFrontToBack_minimizedTaskNotIncluded() { + repo.addActiveTask(displayId = 0, taskId = 1) + repo.addActiveTask(displayId = 0, taskId = 2) + repo.addActiveTask(displayId = 0, taskId = 3) + // The front-most task will be the one added last through addOrMoveFreeformTaskToTop + repo.addOrMoveFreeformTaskToTop(taskId = 3) + repo.addOrMoveFreeformTaskToTop(taskId = 2) + repo.addOrMoveFreeformTaskToTop(taskId = 1) + repo.minimizeTask(displayId = 0, taskId = 2) + + assertThat(repo.getActiveNonMinimizedTasksOrderedFrontToBack(displayId = 0)).isEqualTo( + listOf(1, 3)) + } + + class TestListener : DesktopModeTaskRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 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 64f604119a8b..ad4b720facd7 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 @@ -105,6 +105,7 @@ import org.mockito.Mockito.verify import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.capture import org.mockito.quality.Strictness +import java.util.Optional import org.mockito.Mockito.`when` as whenever /** @@ -145,6 +146,7 @@ class DesktopTasksControllerTest : ShellTestCase() { private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit private lateinit var desktopModeTaskRepository: DesktopModeTaskRepository + private lateinit var desktopTasksLimiter: DesktopTasksLimiter private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener private val shellExecutor = TestShellExecutor() @@ -160,9 +162,12 @@ class DesktopTasksControllerTest : ShellTestCase() { shellInit = Mockito.spy(ShellInit(testExecutor)) desktopModeTaskRepository = DesktopModeTaskRepository() + desktopTasksLimiter = + DesktopTasksLimiter(transitions, desktopModeTaskRepository, shellTaskOrganizer) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + whenever(enterDesktopTransitionHandler.moveToDesktop(any())).thenAnswer { Binder() } whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> (i.arguments.first() as Rect).set(STABLE_BOUNDS) @@ -203,7 +208,8 @@ class DesktopTasksControllerTest : ShellTestCase() { launchAdjacentController, recentsTransitionHandler, multiInstanceHelper, - shellExecutor + shellExecutor, + Optional.of(desktopTasksLimiter), ) } @@ -409,6 +415,25 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun showDesktopApps_dontReorderMinimizedTask() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val minimizedTask = setUpFreeformTask() + markTaskHidden(freeformTask) + markTaskHidden(minimizedTask) + desktopModeTaskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = getLatestWct( + type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(2) + // Reorder home and freeform task to top, don't reorder the minimized task + wct.assertReorderAt(index = 0, homeTask, toTop = true) + wct.assertReorderAt(index = 1, freeformTask, toTop = true) + } + + @Test fun getVisibleTaskCount_noTasks_returnsZero() { assertThat(controller.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) } @@ -606,6 +631,24 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveToDesktop_bringsTasksOverLimit_dontShowBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val homeTask = setUpHomeTask() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + val newTask = setUpFullscreenTask() + + controller.moveToDesktop(newTask) + + val wct = getLatestMoveToDesktopWct() + assertThat(wct.hierarchyOps.size).isEqualTo(taskLimit + 1) // visible tasks + home + wct.assertReorderAt(0, homeTask) + for (i in 1..<taskLimit) { // Skipping freeformTasks[0] + wct.assertReorderAt(index = i, task = freeformTasks[i]) + } + wct.assertReorderAt(taskLimit, newTask) + } + + @Test fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() { val task = setUpFreeformTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -659,6 +702,20 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + setUpHomeTask() + val freeformTasks = (1..taskLimit + 1).map { _ -> setUpFreeformTask() } + + controller.moveTaskToFront(freeformTasks[0]) + + val wct = getLatestWct(type = TRANSIT_TO_FRONT) + assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize + wct.assertReorderAt(0, freeformTasks[0], toTop = true) + wct.assertReorderAt(1, freeformTasks[1], toTop = false) + } + + @Test fun moveToNextDisplay_noOtherDisplays() { whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -777,6 +834,38 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + // Make sure we only reorder the new task to top (we don't reorder the old task to bottom) + assertThat(wct?.hierarchyOps?.size).isEqualTo(1) + wct!!.assertReorderAt(0, fullscreenTask, toTop = true) + } + + @Test + fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + // Make sure we reorder the new task to top, and the back task to the bottom + assertThat(wct!!.hierarchyOps.size).isEqualTo(2) + wct!!.assertReorderAt(0, fullscreenTask, toTop = true) + wct!!.assertReorderAt(1, freeformTasks[0], toTop = false) + } + + @Test fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -841,6 +930,22 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val freeformTasks = (1..taskLimit).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val newFreeformTask = createFreeformTask() + + val wct = + controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN)) + + assertThat(wct?.hierarchyOps?.size).isEqualTo(1) + wct!!.assertReorderAt(0, freeformTasks[0], toTop = false) // Reorder to the bottom + } + + @Test fun handleRequest_freeformTask_freeformNotVisible_returnSwitchToFullscreenWCT() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -1352,11 +1457,16 @@ private fun WindowContainerTransaction.assertIndexInBounds(index: Int) { .isGreaterThan(index) } -private fun WindowContainerTransaction.assertReorderAt(index: Int, task: RunningTaskInfo) { +private fun WindowContainerTransaction.assertReorderAt( + index: Int, + task: RunningTaskInfo, + toTop: Boolean? = null +) { assertIndexInBounds(index) val op = hierarchyOps[index] assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) assertThat(op.container).isEqualTo(task.token.asBinder()) + toTop?.let { assertThat(op.toTop).isEqualTo(it) } } private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt new file mode 100644 index 000000000000..38ea03471b07 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager.RunningTaskInfo +import android.os.Binder +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.AndroidTestingRunner +import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TRANSIT_TO_BACK +import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask +import com.android.wm.shell.transition.TransitionInfoBuilder +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.util.StubTransaction +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.quality.Strictness + + +/** + * Test class for {@link DesktopTasksLimiter} + * + * Usage: atest WMShellUnitTests:DesktopTasksLimiterTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopTasksLimiterTest : ShellTestCase() { + + @JvmField + @Rule + val setFlagsRule = SetFlagsRule() + + @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock lateinit var transitions: Transitions + + private lateinit var mockitoSession: StaticMockitoSession + private lateinit var desktopTasksLimiter: DesktopTasksLimiter + private lateinit var desktopTaskRepo: DesktopModeTaskRepository + + @Before + fun setUp() { + mockitoSession = ExtendedMockito.mockitoSession().strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java).startMocking() + `when`(DesktopModeStatus.isEnabled()).thenReturn(true) + + desktopTaskRepo = DesktopModeTaskRepository() + + desktopTasksLimiter = DesktopTasksLimiter( + transitions, desktopTaskRepo, shellTaskOrganizer) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() + } + + // Currently, the task limit can be overridden through an adb flag. This test ensures the limit + // hasn't been overridden. + @Test + fun getMaxTaskLimit_isSameAsConstant() { + assertThat(desktopTasksLimiter.getMaxTaskLimit()).isEqualTo( + DesktopModeStatus.DEFAULT_MAX_TASK_LIMIT) + } + + @Test + fun addPendingMinimizeTransition_taskIsNotMinimized() { + val task = setUpFreeformTask() + markTaskHidden(task) + + desktopTasksLimiter.addPendingMinimizeChange(Binder(), displayId = 1, taskId = task.taskId) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() + } + + @Test + fun onTransitionReady_noPendingTransition_taskIsNotMinimized() { + val task = setUpFreeformTask() + markTaskHidden(task) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + Binder() /* transition */, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() + } + + @Test + fun onTransitionReady_differentPendingTransition_taskIsNotMinimized() { + val pendingTransition = Binder() + val taskTransition = Binder() + val task = setUpFreeformTask() + markTaskHidden(task) + desktopTasksLimiter.addPendingMinimizeChange( + pendingTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + taskTransition /* transition */, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() + } + + @Test + fun onTransitionReady_pendingTransition_noTaskChange_taskVisible_taskIsNotMinimized() { + val transition = Binder() + val task = setUpFreeformTask() + markTaskVisible(task) + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() + } + + @Test + fun onTransitionReady_pendingTransition_noTaskChange_taskInvisible_taskIsMinimized() { + val transition = Binder() + val task = setUpFreeformTask() + markTaskHidden(task) + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() + } + + @Test + fun onTransitionReady_pendingTransition_changeTaskToBack_taskIsMinimized() { + val transition = Binder() + val task = setUpFreeformTask() + desktopTasksLimiter.addPendingMinimizeChange( + transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() + } + + @Test + fun onTransitionReady_transitionMergedFromPending_taskIsMinimized() { + val mergedTransition = Binder() + val newTransition = Binder() + val task = setUpFreeformTask() + desktopTasksLimiter.addPendingMinimizeChange( + mergedTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + desktopTasksLimiter.getTransitionObserver().onTransitionMerged( + mergedTransition, newTransition) + + desktopTasksLimiter.getTransitionObserver().onTransitionReady( + newTransition, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction() /* finishTransaction */) + + assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() + } + + @Test + fun addAndGetMinimizeTaskChangesIfNeeded_tasksWithinLimit_noTaskMinimized() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + (1..<taskLimit).forEach { _ -> setUpFreeformTask() } + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( + displayId = DEFAULT_DISPLAY, + wct = wct, + newFrontTaskInfo = setUpFreeformTask()) + + assertThat(minimizedTaskId).isNull() + assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added + } + + @Test + fun addAndGetMinimizeTaskChangesIfNeeded_tasksAboveLimit_backTaskMinimized() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + // The following list will be ordered bottom -> top, as the last task is moved to top last. + val tasks = (1..taskLimit).map { setUpFreeformTask() } + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( + displayId = DEFAULT_DISPLAY, + wct = wct, + newFrontTaskInfo = setUpFreeformTask()) + + assertThat(minimizedTaskId).isEqualTo(tasks.first()) + assertThat(wct.hierarchyOps.size).isEqualTo(1) + assertThat(wct.hierarchyOps[0].type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) + assertThat(wct.hierarchyOps[0].toTop).isFalse() // Reorder to bottom + } + + @Test + fun addAndGetMinimizeTaskChangesIfNeeded_nonMinimizedTasksWithinLimit_noTaskMinimized() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val tasks = (1..taskLimit).map { setUpFreeformTask() } + desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId) + + val wct = WindowContainerTransaction() + val minimizedTaskId = + desktopTasksLimiter.addAndGetMinimizeTaskChangesIfNeeded( + displayId = 0, + wct = wct, + newFrontTaskInfo = setUpFreeformTask()) + + assertThat(minimizedTaskId).isNull() + assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added + } + + @Test + fun getTaskToMinimizeIfNeeded_tasksWithinLimit_returnsNull() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val tasks = (1..taskLimit).map { setUpFreeformTask() } + + val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }) + + assertThat(minimizedTask).isNull() + } + + @Test + fun getTaskToMinimizeIfNeeded_tasksAboveLimit_returnsBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val tasks = (1..taskLimit + 1).map { setUpFreeformTask() } + + val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }) + + // first == front, last == back + assertThat(minimizedTask).isEqualTo(tasks.last()) + } + + @Test + fun getTaskToMinimizeIfNeeded_withNewTask_tasksAboveLimit_returnsBackTask() { + val taskLimit = desktopTasksLimiter.getMaxTaskLimit() + val tasks = (1..taskLimit).map { setUpFreeformTask() } + + val minimizedTask = desktopTasksLimiter.getTaskToMinimizeIfNeeded( + visibleFreeformTaskIdsOrderedFrontToBack = tasks.map { it.taskId }, + newTaskIdInFront = setUpFreeformTask().taskId) + + // first == front, last == back + assertThat(minimizedTask).isEqualTo(tasks.last()) + } + + private fun setUpFreeformTask( + displayId: Int = DEFAULT_DISPLAY, + ): RunningTaskInfo { + val task = createFreeformTask(displayId) + `when`(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + desktopTaskRepo.addActiveTask(displayId, task.taskId) + desktopTaskRepo.addOrMoveFreeformTaskToTop(task.taskId) + return task + } + + private fun markTaskVisible(task: RunningTaskInfo) { + desktopTaskRepo.updateVisibleFreeformTasks( + task.displayId, + task.taskId, + visible = true + ) + } + + private fun markTaskHidden(task: RunningTaskInfo) { + desktopTaskRepo.updateVisibleFreeformTasks( + task.displayId, + task.taskId, + visible = false + ) + } +} |