Merge "Minimize a Task whenever we reach the Desktop Task limit" into main
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 1408ead..26e7acb 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.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.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 @@
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 @@
@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 32c22c0..fcddcad 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 @@
"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 @@
}
/**
+ * 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 50cea01..2d508b2 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 @@
*/
val activeTasks: ArraySet<Int> = ArraySet(),
val visibleTasks: ArraySet<Int> = ArraySet(),
+ val minimizedTasks: ArraySet<Int> = ArraySet(),
var stashed: Boolean = false
)
@@ -202,6 +203,13 @@
}
}
+ /** 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 @@
}
/**
+ * 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 @@
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 @@
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 @@
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 068661a..0a9e5d0 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.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 @@
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 @@
)
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 @@
)
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 @@
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 @@
?: 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 @@
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 @@
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 @@
} ?: 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 @@
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 @@
)
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 @@
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 0000000..3404d37
--- /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 0061d03..e341f2d 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 @@
* 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 79bb540..74b8f83 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 @@
/**
* 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 f2bdcae..6fea203 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 @@
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 @@
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 @@
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 b2b54ac..dca7be1 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 @@
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 64f6041..ad4b720 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.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 @@
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 @@
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 @@
launchAdjacentController,
recentsTransitionHandler,
multiInstanceHelper,
- shellExecutor
+ shellExecutor,
+ Optional.of(desktopTasksLimiter),
)
}
@@ -409,6 +415,25 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
}
@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 @@
.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 0000000..38ea034
--- /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
+ )
+ }
+}