diff options
Diffstat (limited to 'libs')
17 files changed, 981 insertions, 134 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 fdfaa90ac8b9..c64c98750b0a 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 @@ -108,6 +108,8 @@ import com.android.wm.shell.desktopmode.education.AppToWebEducationController; import com.android.wm.shell.desktopmode.education.AppToWebEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; import com.android.wm.shell.desktopmode.education.data.AppToWebEducationDatastoreRepository; +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer; +import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer; import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository; import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer; import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializerImpl; @@ -703,6 +705,16 @@ public abstract class WMShellModule { @WMSingleton @Provides + static DesksOrganizer provideDesksOrganizer( + @NonNull ShellInit shellInit, + @NonNull ShellCommandHandler shellCommandHandler, + @NonNull ShellTaskOrganizer shellTaskOrganizer + ) { + return new RootTaskDesksOrganizer(shellInit, shellCommandHandler, shellTaskOrganizer); + } + + @WMSingleton + @Provides @DynamicOverride static DesktopTasksController provideDesktopTasksController( Context context, @@ -741,7 +753,8 @@ public abstract class WMShellModule { DesktopTilingDecorViewModel desktopTilingDecorViewModel, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, Optional<BubbleController> bubbleController, - OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver) { + OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver, + DesksOrganizer desksOrganizer) { return new DesktopTasksController( context, shellInit, @@ -775,7 +788,8 @@ public abstract class WMShellModule { desktopTilingDecorViewModel, desktopWallpaperActivityTokenProvider, bubbleController, - overviewToDesktopTransitionObserver); + overviewToDesktopTransitionObserver, + desksOrganizer); } @WMSingleton @@ -1183,10 +1197,11 @@ public abstract class WMShellModule { Transitions transitions, DisplayController displayController, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - IWindowManager windowManager + IWindowManager windowManager, + Optional<DesktopUserRepositories> desktopUserRepositories, + Optional<DesktopTasksController> desktopTasksController ) { - if (!DesktopModeStatus.canEnterDesktopMode(context) - || !Flags.enableDisplayWindowingModeSwitching()) { + if (!DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.empty(); } return Optional.of( @@ -1196,7 +1211,9 @@ public abstract class WMShellModule { transitions, displayController, rootTaskDisplayAreaOrganizer, - windowManager)); + windowManager, + desktopUserRepositories.get(), + desktopTasksController.get())); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index 43e8d2a30930..760d2124b845 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -24,9 +24,14 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.IWindowManager import android.view.WindowManager.TRANSIT_CHANGE import android.window.WindowContainerTransaction +import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener +import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions @@ -38,7 +43,12 @@ class DesktopDisplayEventHandler( private val displayController: DisplayController, private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, private val windowManager: IWindowManager, -) : OnDisplaysChangedListener { + private val desktopUserRepositories: DesktopUserRepositories, + private val desktopTasksController: DesktopTasksController, +) : OnDisplaysChangedListener, OnDeskRemovedListener { + + private val desktopRepository: DesktopRepository + get() = desktopUserRepositories.current init { shellInit.addInitCallback({ onInit() }, this) @@ -46,23 +56,43 @@ class DesktopDisplayEventHandler( private fun onInit() { displayController.addDisplayWindowListener(this) + + if (Flags.enableMultipleDesktopsBackend()) { + desktopTasksController.onDeskRemovedListener = this + } } override fun onDisplayAdded(displayId: Int) { - if (displayId == DEFAULT_DISPLAY) { + if (displayId != DEFAULT_DISPLAY) { + refreshDisplayWindowingMode() + } + + if (!supportsDesks(displayId)) { + logV("Display #$displayId does not support desks") return } - refreshDisplayWindowingMode() + logV("Creating new desk in new display#$displayId") + desktopTasksController.createDesk(displayId) } override fun onDisplayRemoved(displayId: Int) { - if (displayId == DEFAULT_DISPLAY) { - return + if (displayId != DEFAULT_DISPLAY) { + refreshDisplayWindowingMode() + } + + // TODO: b/362720497 - move desks in closing display to the remaining desk. + } + + override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) { + val remainingDesks = desktopRepository.getNumberOfDesks(lastDisplayId) + if (remainingDesks == 0) { + logV("All desks removed from display#$lastDisplayId, creating empty desk") + desktopTasksController.createDesk(lastDisplayId) } - refreshDisplayWindowingMode() } private fun refreshDisplayWindowingMode() { + if (!Flags.enableDisplayWindowingModeSwitching()) return // TODO: b/375319538 - Replace the check with a DisplayManager API once it's available. val isExtendedDisplayEnabled = 0 != @@ -98,4 +128,16 @@ class DesktopDisplayEventHandler( wct.setWindowingMode(tdaInfo.token, targetDisplayWindowingMode) transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) } + + // TODO: b/362720497 - connected/projected display considerations. + private fun supportsDesks(displayId: Int): Boolean = + DesktopModeStatus.canEnterDesktopMode(context) + + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + companion object { + private const val TAG = "DesktopDisplayEventHandler" + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index 9b9988457808..164d04bbde65 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -110,8 +110,8 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: display id should be an integer") return false } - pw.println("Not implemented.") - return false + controller.createDesk(displayId) + return true } private fun runActivateDesk(args: Array<String>, pw: PrintWriter): Boolean { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index fa696682de28..6636770895fa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -171,6 +171,9 @@ class DesktopRepository( /** Returns a list of all [Desk]s in the repository. */ private fun desksSequence(): Sequence<Desk> = desktopData.desksSequence() + /** Returns the number of desks in the given display. */ + fun getNumberOfDesks(displayId: Int) = desktopData.getNumberOfDesks(displayId) + /** Adds [regionListener] to inform about changes to exclusion regions for all Desktop tasks. */ fun setExclusionRegionListener(regionListener: Consumer<Region>, executor: Executor) { desktopGestureExclusionListener = regionListener @@ -201,11 +204,11 @@ class DesktopRepository( /** Adds the given desk under the given display. */ fun addDesk(displayId: Int, deskId: Int) { - desktopData.getOrCreateDesk(displayId, deskId) + desktopData.createDesk(displayId, deskId) } /** Returns the default desk in the given display. */ - fun getDefaultDesk(displayId: Int): Int? = desktopData.getDefaultDesk(displayId)?.deskId + private fun getDefaultDesk(displayId: Int): Desk? = desktopData.getDefaultDesk(displayId) /** Sets the given desk as the active one in the given display. */ fun setActiveDesk(displayId: Int, deskId: Int) { @@ -229,15 +232,14 @@ class DesktopRepository( * TODO: b/389960283 - add explicit [deskId] argument. */ private fun addActiveTask(displayId: Int, taskId: Int) { - val activeDeskId = - desktopData.getActiveDesk(displayId)?.deskId - ?: error("Expected active desk in display: $displayId") + val activeDesk = desktopData.getDefaultDesk(displayId) + checkNotNull(activeDesk) { "Expected desk in display: $displayId" } // Removes task if it is active on another desk excluding [activeDesk]. - removeActiveTask(taskId, excludedDeskId = activeDeskId) + removeActiveTask(taskId, excludedDeskId = activeDesk.deskId) - if (desktopData.getOrCreateDesk(displayId, activeDeskId).activeTasks.add(taskId)) { - logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, activeDeskId) + if (activeDesk.activeTasks.add(taskId)) { + logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, activeDesk.deskId) updateActiveTasksListeners(displayId) } } @@ -266,18 +268,23 @@ class DesktopRepository( * TODO: b/389960283 - add explicit [deskId] argument. */ fun addClosingTask(displayId: Int, taskId: Int) { - val activeDeskId = - desktopData.getActiveDesk(displayId)?.deskId + val activeDesk = + desktopData.getActiveDesk(displayId) ?: error("Expected active desk in display: $displayId") - if (desktopData.getOrCreateDesk(displayId, activeDeskId).closingTasks.add(taskId)) { - logD("Added closing task=%d displayId=%d deskId=%d", taskId, displayId, activeDeskId) + if (activeDesk.closingTasks.add(taskId)) { + logD( + "Added closing task=%d displayId=%d deskId=%d", + taskId, + displayId, + activeDesk.deskId, + ) } else { // If the task hasn't been removed from closing list after it disappeared. logW( "Task with taskId=%d displayId=%d deskId=%d is already closing", taskId, displayId, - activeDeskId, + activeDesk.deskId, ) } } @@ -323,7 +330,7 @@ class DesktopRepository( /** * Returns the active tasks in the given display's active desk. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - migrate callers to [getActiveTaskIdsInDesk]. */ @VisibleForTesting fun getActiveTasks(displayId: Int): ArraySet<Int> = @@ -332,19 +339,27 @@ class DesktopRepository( /** * Returns the minimized tasks in the given display's active desk. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - migrate callers to [getMinimizedTaskIdsInDesk]. */ fun getMinimizedTasks(displayId: Int): ArraySet<Int> = ArraySet(desktopData.getActiveDesk(displayId)?.minimizedTasks) + @VisibleForTesting + fun getMinimizedTaskIdsInDesk(deskId: Int): ArraySet<Int> = + ArraySet(desktopData.getDesk(deskId)?.minimizedTasks) + /** * Returns all active non-minimized tasks for [displayId] ordered from top to bottom. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - migrate callers to [getExpandedTasksIdsInDeskOrdered]. */ fun getExpandedTasksOrdered(displayId: Int): List<Int> = getFreeformTasksInZOrder(displayId).filter { !isMinimizedTask(it) } + @VisibleForTesting + fun getExpandedTasksIdsInDeskOrdered(deskId: Int): List<Int> = + getFreeformTasksIdsInDeskInZOrder(deskId).filter { !isMinimizedTask(it) } + /** * Returns the count of active non-minimized tasks for [displayId]. * @@ -357,11 +372,15 @@ class DesktopRepository( /** * Returns a list of freeform tasks, ordered from top-bottom (top at index 0). * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - migrate callers to [getFreeformTasksIdsInDeskInZOrder]. */ @VisibleForTesting fun getFreeformTasksInZOrder(displayId: Int): ArrayList<Int> = - ArrayList(desktopData.getActiveDesk(displayId)?.freeformTasksInZOrder ?: emptyList()) + ArrayList(desktopData.getDefaultDesk(displayId)?.freeformTasksInZOrder ?: emptyList()) + + @VisibleForTesting + fun getFreeformTasksIdsInDeskInZOrder(deskId: Int): ArrayList<Int> = + ArrayList(desktopData.getDesk(deskId)?.freeformTasksInZOrder ?: emptyList()) /** Returns the tasks inside the given desk. */ fun getActiveTaskIdsInDesk(deskId: Int): Set<Int> = @@ -401,8 +420,8 @@ class DesktopRepository( } val prevCount = getVisibleTaskCount(displayId) if (isVisible) { - desktopData.getActiveDesk(displayId)?.visibleTasks?.add(taskId) - ?: error("Expected non-null active desk in display $displayId") + desktopData.getDefaultDesk(displayId)?.visibleTasks?.add(taskId) + ?: error("Expected non-null desk in display $displayId") unminimizeTask(displayId, taskId) } else { desktopData.getActiveDesk(displayId)?.visibleTasks?.remove(taskId) @@ -587,17 +606,15 @@ class DesktopRepository( * TODO: b/389960283 - add explicit [deskId] argument. */ private fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) { - val activeDesk = - desktopData.getActiveDesk(displayId) - ?: error("Expected a desk to be active in display: $displayId") + val desk = getDefaultDesk(displayId) ?: error("Expected a desk in display: $displayId") logD( "Add or move task to top: display=%d taskId=%d deskId=%d", taskId, displayId, - activeDesk.deskId, + desk.deskId, ) - desktopData.forAllDesks { _, desk -> desk.freeformTasksInZOrder.remove(taskId) } - activeDesk.freeformTasksInZOrder.add(0, taskId) + desktopData.forAllDesks { _, desk1 -> desk1.freeformTasksInZOrder.remove(taskId) } + desk.freeformTasksInZOrder.add(0, taskId) // Unminimize the task if it is minimized. unminimizeTask(displayId, taskId) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { @@ -835,13 +852,8 @@ class DesktopRepository( /** An interface for the desktop hierarchy's data managed by this repository. */ private interface DesktopData { - /** - * Returns the existing desk or creates a new entry if needed. - * - * TODO: 389787966 - consider removing this as it cannot be assumed a desk can be created in - * all devices / form-factors. - */ - fun getOrCreateDesk(displayId: Int, deskId: Int): Desk + /** Creates a desk record. */ + fun createDesk(displayId: Int, deskId: Int) /** Returns the desk with the given id, or null if it does not exist. */ fun getDesk(deskId: Int): Desk? @@ -894,7 +906,8 @@ class DesktopRepository( /** * A [DesktopData] implementation that only supports one desk per display. * - * Internally, it reuses the displayId as that display's single desk's id. + * Internally, it reuses the displayId as that display's single desk's id. It also never truly + * "removes" a desk, it just clears its content. */ private class SingleDesktopData : DesktopData { private val deskByDisplayId = @@ -907,12 +920,13 @@ class DesktopRepository( } } - override fun getOrCreateDesk(displayId: Int, deskId: Int): Desk { - check(displayId == deskId) - return deskByDisplayId.getOrCreate(displayId) + override fun createDesk(displayId: Int, deskId: Int) { + check(displayId == deskId) { "Display and desk ids must match" } + deskByDisplayId.getOrCreate(displayId) } - override fun getDesk(deskId: Int): Desk = getOrCreateDesk(deskId, deskId) + override fun getDesk(deskId: Int): Desk = + checkNotNull(deskByDisplayId[deskId]) { "Expected desk $deskId to exist" } override fun getActiveDesk(displayId: Int): Desk { // TODO: 389787966 - consider migrating to an "active" state instead of checking the @@ -927,7 +941,7 @@ class DesktopRepository( // existence of visible desktop windows, among other factors. } - override fun getDefaultDesk(displayId: Int): Desk = getOrCreateDesk(displayId, displayId) + override fun getDefaultDesk(displayId: Int): Desk = getDesk(deskId = displayId) override fun getAllActiveDesks(): Set<Desk> = deskByDisplayId.valueIterator().asSequence().toSet() @@ -943,7 +957,7 @@ class DesktopRepository( } override fun forAllDesks(displayId: Int, consumer: (Desk) -> Unit) { - consumer(getOrCreateDesk(displayId, displayId)) + consumer(getDesk(deskId = displayId)) } override fun desksSequence(): Sequence<Desk> = deskByDisplayId.valueIterator().asSequence() @@ -962,16 +976,14 @@ class DesktopRepository( private class MultiDesktopData : DesktopData { private val desktopDisplays = SparseArray<DesktopDisplay>() - override fun getOrCreateDesk(displayId: Int, deskId: Int): Desk { + override fun createDesk(displayId: Int, deskId: Int) { val display = desktopDisplays[displayId] ?: DesktopDisplay(displayId).also { desktopDisplays[displayId] = it } - val desk = - display.orderedDesks.find { desk -> desk.deskId == deskId } - ?: Desk(deskId = deskId, displayId = displayId).also { - display.orderedDesks.add(it) - } - return desk + check(display.orderedDesks.none { desk -> desk.deskId == deskId }) { + "Attempting to create desk#$deskId that already exists in display#$displayId" + } + display.orderedDesks.add(Desk(deskId = deskId, displayId = displayId)) } override fun getDesk(deskId: Int): Desk? { @@ -999,7 +1011,8 @@ class DesktopRepository( override fun getDefaultDesk(displayId: Int): Desk? { val display = desktopDisplays[displayId] ?: return null - return display.orderedDesks.firstOrNull() + return display.orderedDesks.find { it.deskId == display.activeDeskId } + ?: display.orderedDesks.firstOrNull() } override fun getAllActiveDesks(): Set<Desk> { 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 3ae553596631..6c57dc7056a6 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 @@ -102,6 +102,8 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCR import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer +import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -180,6 +182,7 @@ class DesktopTasksController( private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, private val bubbleController: Optional<BubbleController>, private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver, + private val desksOrganizer: DesksOrganizer, ) : RemoteCallable<DesktopTasksController>, Transitions.TransitionHandler, @@ -232,6 +235,9 @@ class DesktopTasksController( // Used to prevent handleRequest from moving the new fullscreen task to freeform. private var dragAndDropFullscreenCookie: Binder? = null + // A listener that is invoked after a desk has been remove from the system. */ + var onDeskRemovedListener: OnDeskRemovedListener? = null + init { desktopMode = DesktopModeImpl() if (DesktopModeStatus.canEnterDesktopMode(context)) { @@ -415,6 +421,18 @@ class DesktopTasksController( return isFreeformDisplay } + /** Creates a new desk in the given display. */ + fun createDesk(displayId: Int) { + if (Flags.enableMultipleDesktopsBackend()) { + desksOrganizer.createDesk(displayId) { deskId -> + taskRepository.addDesk(displayId = displayId, deskId = deskId) + } + } else { + // In single-desk, the desk reuses the display id. + taskRepository.addDesk(displayId = displayId, deskId = displayId) + } + } + /** Moves task to desktop mode if task is running, else launches it in desktop mode. */ @JvmOverloads fun moveTaskToDesktop( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt new file mode 100644 index 000000000000..5cbb59fbf323 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2025 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.multidesks + +import android.app.ActivityManager +import android.window.TransitionInfo +import android.window.WindowContainerTransaction + +/** An organizer of desk containers in which to host child desktop windows. */ +interface DesksOrganizer { + /** Creates a new desk container in the given display. */ + fun createDesk(displayId: Int, callback: OnCreateCallback) + + /** Activates the given desk, making it visible in its display. */ + fun activateDesk(wct: WindowContainerTransaction, deskId: Int) + + /** Removes the given desk and its desktop windows. */ + fun removeDesk(wct: WindowContainerTransaction, deskId: Int) + + /** Moves the given task to the given desk. */ + fun moveTaskToDesk( + wct: WindowContainerTransaction, + deskId: Int, + task: ActivityManager.RunningTaskInfo, + ) + + /** + * Returns the desk id in which the task in the given change is located at the end of a + * transition, if any. + */ + fun getDeskAtEnd(change: TransitionInfo.Change): Int? + + /** A callback that is invoked when the desk container is created. */ + fun interface OnCreateCallback { + /** Calls back when the [deskId] has been created. */ + fun onCreated(deskId: Int) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/OnDeskRemovedListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/OnDeskRemovedListener.kt new file mode 100644 index 000000000000..452ddb1ff8fb --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/OnDeskRemovedListener.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2025 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.multidesks + +/** A listener for removals of desks. */ +fun interface OnDeskRemovedListener { + /** Called when a desk has been removed from the system. */ + fun onDeskRemoved(lastDisplayId: Int, deskId: Int) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt new file mode 100644 index 000000000000..79c48c5e9594 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2025 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.multidesks + +import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD +import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED +import android.util.SparseArray +import android.view.SurfaceControl +import android.window.TransitionInfo +import android.window.WindowContainerTransaction +import androidx.core.util.forEach +import com.android.internal.annotations.VisibleForTesting +import com.android.internal.protolog.ProtoLog +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.sysui.ShellCommandHandler +import com.android.wm.shell.sysui.ShellInit +import java.io.PrintWriter + +/** A [DesksOrganizer] that uses root tasks as the container of each desk. */ +class RootTaskDesksOrganizer( + shellInit: ShellInit, + shellCommandHandler: ShellCommandHandler, + private val shellTaskOrganizer: ShellTaskOrganizer, +) : DesksOrganizer, ShellTaskOrganizer.TaskListener { + + private val deskCreateRequests = mutableListOf<CreateRequest>() + @VisibleForTesting val roots = SparseArray<DeskRoot>() + + init { + if (Flags.enableMultipleDesktopsBackend()) { + shellInit.addInitCallback( + { shellCommandHandler.addDumpCallback(this::dump, this) }, + this, + ) + } + } + + override fun createDesk(displayId: Int, callback: OnCreateCallback) { + logV("createDesk in display: %d", displayId) + deskCreateRequests += CreateRequest(displayId, callback) + shellTaskOrganizer.createRootTask( + displayId, + WINDOWING_MODE_FREEFORM, + /* listener = */ this, + /* removeWithTaskOrganizer = */ true, + ) + } + + override fun removeDesk(wct: WindowContainerTransaction, deskId: Int) { + logV("removeDesk %d", deskId) + val desk = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" } + wct.removeRootTask(desk.taskInfo.token) + } + + override fun activateDesk(wct: WindowContainerTransaction, deskId: Int) { + logV("activateDesk %d", deskId) + val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" } + wct.reorder(root.taskInfo.token, /* onTop= */ true) + wct.setLaunchRoot( + /* container= */ root.taskInfo.token, + /* windowingModes= */ intArrayOf(WINDOWING_MODE_FREEFORM, WINDOWING_MODE_UNDEFINED), + /* activityTypes= */ intArrayOf(ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD), + ) + } + + override fun moveTaskToDesk( + wct: WindowContainerTransaction, + deskId: Int, + task: RunningTaskInfo, + ) { + val root = roots[deskId] ?: error("Root not found for desk: $deskId") + wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true) + } + + override fun getDeskAtEnd(change: TransitionInfo.Change): Int? = + change.taskInfo?.parentTaskId?.takeIf { it in roots } + + override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) { + if (taskInfo.parentTaskId in roots) { + val deskId = taskInfo.parentTaskId + val taskId = taskInfo.taskId + logV("Task #$taskId appeared in desk #$deskId") + addChildToDesk(taskId = taskId, deskId = deskId) + return + } + val deskId = taskInfo.taskId + check(deskId !in roots) { "A root already exists for desk: $deskId" } + val request = + checkNotNull(deskCreateRequests.firstOrNull { it.displayId == taskInfo.displayId }) { + "Task ${taskInfo.taskId} appeared without pending create request" + } + logV("Desk #$deskId appeared") + roots[deskId] = DeskRoot(deskId, taskInfo, leash) + deskCreateRequests.remove(request) + request.onCreateCallback.onCreated(deskId) + } + + override fun onTaskInfoChanged(taskInfo: RunningTaskInfo) { + if (roots.contains(taskInfo.taskId)) { + val deskId = taskInfo.taskId + roots[deskId] = roots[deskId].copy(taskInfo = taskInfo) + } + } + + override fun onTaskVanished(taskInfo: RunningTaskInfo) { + if (roots.contains(taskInfo.taskId)) { + val deskId = taskInfo.taskId + val deskRoot = roots[deskId] + // Use the last saved taskInfo to obtain the displayId. Using the local one here will + // return -1 since the task is not unassociated with a display. + val displayId = deskRoot.taskInfo.displayId + logV("Desk #$deskId vanished from display #$displayId") + roots.remove(deskId) + return + } + // At this point, [parentTaskId] may be unset even if this is a task vanishing from a desk, + // so search through each root to remove this if it's a child. + roots.forEach { deskId, deskRoot -> + if (deskRoot.children.remove(taskInfo.taskId)) { + logV("Task #${taskInfo.taskId} vanished from desk #$deskId") + return + } + } + } + + @VisibleForTesting + data class DeskRoot( + val deskId: Int, + val taskInfo: RunningTaskInfo, + val leash: SurfaceControl, + val children: MutableSet<Int> = mutableSetOf(), + ) + + override fun dump(pw: PrintWriter, prefix: String) { + val innerPrefix = "$prefix " + pw.println("$prefix$TAG") + pw.println("${innerPrefix}Desk Roots:") + roots.forEach { deskId, root -> + pw.println("$innerPrefix #$deskId visible=${root.taskInfo.isVisible}") + pw.println("$innerPrefix children=${root.children}") + } + } + + private fun addChildToDesk(taskId: Int, deskId: Int) { + roots.forEach { _, deskRoot -> + if (deskRoot.deskId == deskId) { + deskRoot.children.add(taskId) + } else { + deskRoot.children.remove(taskId) + } + } + } + + private data class CreateRequest(val displayId: Int, val onCreateCallback: OnCreateCallback) + + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + companion object { + private const val TAG = "RootTaskDesksOrganizer" + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt index 58a49a035bb6..5a89451ffdbc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode.persistence import android.content.Context import android.window.DesktopModeFlags +import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.shared.annotations.ShellMainThread @@ -54,10 +55,22 @@ class DesktopRepositoryInitializerImpl( DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } ?: persistentDesktop.zOrderedTasksCount var visibleTasksCount = 0 + repository.addDesk( + displayId = persistentDesktop.displayId, + deskId = + if (Flags.enableMultipleDesktopsBackend()) { + persistentDesktop.desktopId + } else { + // When disabled, desk ids are always the display id. + persistentDesktop.displayId + }, + ) persistentDesktop.zOrderedTasksList // Reverse it so we initialize the repo from bottom to top. .reversed() .mapNotNull { taskId -> persistentDesktop.tasksByTaskIdMap[taskId] } + // TODO: b/362720497 - add tasks to their respective desk when multi-desk + // persistence is implemented. .forEach { task -> if ( task.desktopTaskState == DesktopTaskState.VISIBLE && diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt index ecad5217b87f..957fdf995776 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt @@ -183,6 +183,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_resizeable_doNothing() { + userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask() taskStackListener.onActivityRequestedOrientationChanged( @@ -195,6 +197,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_nonResizeableFullscreen_doNothing() { + userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = createFullscreenTask() task.isResizeable = false val activityInfo = ActivityInfo() @@ -214,6 +218,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_nonResizeablePortrait_requestSameOrientation_doNothing() { + userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask(isResizeable = false) val newTask = setUpFreeformTask( @@ -228,6 +234,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_notInDesktopMode_doNothing() { + userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask(isResizeable = false) userRepositories.current.updateTask(task.displayId, task.taskId, isVisible = false) @@ -241,6 +249,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_nonResizeablePortrait_respectLandscapeRequest() { + userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask(isResizeable = false) val oldBounds = task.configuration.windowConfiguration.bounds val newTask = @@ -263,6 +273,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_nonResizeableLandscape_respectPortraitRequest() { + userRepositories.current.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + userRepositories.current.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val oldBounds = Rect(0, 0, 500, 200) val task = setUpFreeformTask( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt index 6a3717427e93..5d8d5a716504 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt @@ -20,6 +20,8 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.content.ContentResolver import android.os.Binder +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.provider.Settings import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.testing.AndroidTestingRunner @@ -29,20 +31,25 @@ import android.view.WindowManager.TRANSIT_CHANGE import android.window.DisplayAreaInfo import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.dx.mockito.inline.extended.ExtendedMockito.never +import com.android.dx.mockito.inline.extended.StaticMockitoSession +import com.android.window.flags.Flags import com.android.wm.shell.MockToken import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions 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.ArgumentCaptor import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito.anyInt @@ -53,6 +60,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.eq import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness /** * Test class for [DesktopDisplayEventHandler] @@ -63,18 +71,33 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) class DesktopDisplayEventHandlerTest : ShellTestCase() { + @JvmField @Rule val setFlagsRule = SetFlagsRule() + @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var transitions: Transitions @Mock lateinit var displayController: DisplayController @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var mockWindowManager: IWindowManager + @Mock private lateinit var mockDesktopUserRepositories: DesktopUserRepositories + @Mock private lateinit var mockDesktopRepository: DesktopRepository + @Mock private lateinit var mockDesktopTasksController: DesktopTasksController + private lateinit var mockitoSession: StaticMockitoSession private lateinit var shellInit: ShellInit private lateinit var handler: DesktopDisplayEventHandler + private val onDisplaysChangedListenerCaptor = argumentCaptor<OnDisplaysChangedListener>() + @Before fun setUp() { + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + shellInit = spy(ShellInit(testExecutor)) + whenever(mockDesktopUserRepositories.current).thenReturn(mockDesktopRepository) whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) @@ -86,8 +109,17 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { displayController, rootTaskDisplayAreaOrganizer, mockWindowManager, + mockDesktopUserRepositories, + mockDesktopTasksController, ) shellInit.init() + verify(displayController) + .addDisplayWindowListener(onDisplaysChangedListenerCaptor.capture()) + } + + @After + fun tearDown() { + mockitoSession.finishMocking() } private fun testDisplayWindowingModeSwitch( @@ -96,8 +128,6 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { expectTransition: Boolean, ) { val externalDisplayId = 100 - val captor = ArgumentCaptor.forClass(OnDisplaysChangedListener::class.java) - verify(displayController).addDisplayWindowListener(captor.capture()) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = defaultWindowingMode whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { defaultWindowingMode } @@ -111,12 +141,12 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { // The external display connected. whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId)) - captor.value.onDisplayAdded(externalDisplayId) + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(externalDisplayId) tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM // The external display disconnected. whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) .thenReturn(intArrayOf(DEFAULT_DISPLAY)) - captor.value.onDisplayRemoved(externalDisplayId) + onDisplaysChangedListenerCaptor.lastValue.onDisplayRemoved(externalDisplayId) if (expectTransition) { val arg = argumentCaptor<WindowContainerTransaction>() @@ -159,6 +189,44 @@ class DesktopDisplayEventHandlerTest : ShellTestCase() { ) } + @Test + fun testDisplayAdded_supportsDesks_createsDesk() { + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(true) + + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + + verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY) + } + + @Test + fun testDisplayAdded_cannotEnterDesktopMode_doesNotCreateDesk() { + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false) + + onDisplaysChangedListenerCaptor.lastValue.onDisplayAdded(DEFAULT_DISPLAY) + + verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun testDeskRemoved_noDesksRemain_createsDesk() { + whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(0) + + handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1) + + verify(mockDesktopTasksController).createDesk(DEFAULT_DISPLAY) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun testDeskRemoved_desksRemain_doesNotCreateDesk() { + whenever(mockDesktopRepository.getNumberOfDesks(DEFAULT_DISPLAY)).thenReturn(1) + + handler.onDeskRemoved(DEFAULT_DISPLAY, deskId = 1) + + verify(mockDesktopTasksController, never()).createDesk(DEFAULT_DISPLAY) + } + private class ExtendedDisplaySettingsSession( private val contentResolver: ContentResolver, private val overrideValue: Int, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 8d73f3f59afd..5736793cd6a9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -18,12 +18,13 @@ package com.android.wm.shell.desktopmode import android.graphics.Rect import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.platform.test.flag.junit.SetFlagsRule -import android.testing.AndroidTestingRunner import android.util.ArraySet import android.view.Display.DEFAULT_DISPLAY import android.view.Display.INVALID_DISPLAY import androidx.test.filters.SmallTest +import com.android.window.flags.Flags import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP import com.android.wm.shell.ShellTestCase @@ -56,6 +57,8 @@ import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters /** * Tests for [@link DesktopRepository]. @@ -63,11 +66,11 @@ import org.mockito.kotlin.whenever * Build/Install/Run: atest WMShellUnitTests:DesktopRepositoryTest */ @SmallTest -@RunWith(AndroidTestingRunner::class) +@RunWith(ParameterizedAndroidJunit4::class) @ExperimentalCoroutinesApi -class DesktopRepositoryTest : ShellTestCase() { +class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() + @JvmField @Rule val setFlagsRule = SetFlagsRule(flags) private lateinit var repo: DesktopRepository private lateinit var shellInit: ShellInit @@ -86,6 +89,8 @@ class DesktopRepositoryTest : ShellTestCase() { whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }) .thenReturn(Desktop.getDefaultInstance()) shellInit.init() + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DESKTOP_ID) + repo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DESKTOP_ID) } @After @@ -137,6 +142,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun addTask_multipleDisplays_notifiesCorrectListener() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val listener = TestListener() repo.addActiveTaskListener(listener) @@ -150,6 +156,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun addTask_multipleDisplays_moveToAnotherDisplay() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.addTask(SECOND_DISPLAY, taskId = 1, isVisible = true) assertThat(repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY)).isEmpty() @@ -310,19 +317,21 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun isOnlyVisibleNonClosingTask_multipleDisplays() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) repo.updateTask(SECOND_DISPLAY, taskId = 3, isVisible = true) // Not the only task on DEFAULT_DISPLAY assertThat(repo.isVisibleTask(1)).isTrue() - assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(1, DEFAULT_DISPLAY)).isFalse() // Not the only task on DEFAULT_DISPLAY assertThat(repo.isVisibleTask(2)).isTrue() - assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse() + assertThat(repo.isOnlyVisibleNonClosingTask(2, DEFAULT_DISPLAY)).isFalse() // The only visible task on SECOND_DISPLAY assertThat(repo.isVisibleTask(3)).isTrue() - assertThat(repo.isOnlyVisibleNonClosingTask(3)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(3, SECOND_DISPLAY)).isTrue() // Not a visible task assertThat(repo.isVisibleTask(99)).isFalse() assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() @@ -343,6 +352,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun addListener_tasksOnDifferentDisplay_doesNotNotify() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.updateTask(SECOND_DISPLAY, taskId = 1, isVisible = true) val listener = TestVisibilityListener() val executor = TestShellExecutor() @@ -351,7 +361,7 @@ class DesktopRepositoryTest : ShellTestCase() { assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0) // One call as adding listener notifies it - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(0) + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) } @Test @@ -365,11 +375,14 @@ class DesktopRepositoryTest : ShellTestCase() { executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2) + // 1 from registration, 2 for the updates. + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) } @Test fun updateTask_visibleTask_addVisibleTaskNotifiesListenerForThatDisplay() { + repo.addDesk(displayId = 1, deskId = 1) + repo.setActiveDesk(displayId = 1, deskId = 1) val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) @@ -378,22 +391,27 @@ class DesktopRepositoryTest : ShellTestCase() { executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) + // 1 for the registration, 1 for the update. + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2) assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(0) - assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(0) + // 1 for the registration. + assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1) repo.updateTask(displayId = 1, taskId = 2, isVisible = true) executor.flushAll() // Listener for secondary display is notified assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1) - assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1) + // 1 for the registration, 1 for the update. + assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(2) // No changes to listener for default display - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2) } @Test fun updateTask_taskOnDefaultBecomesVisibleOnSecondDisplay_listenersNotified() { + repo.addDesk(displayId = 1, deskId = 1) + repo.setActiveDesk(displayId = 1, deskId = 1) val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) @@ -406,14 +424,15 @@ class DesktopRepositoryTest : ShellTestCase() { repo.updateTask(displayId = 1, taskId = 1, isVisible = true) executor.flushAll() - // Default display should have 2 calls - // 1 - visible task added - // 2 - visible task removed - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2) + // Default display should have 3 calls + // 1 - listener registered + // 2 - visible task added + // 3 - visible task removed + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0) - // Secondary display should have 1 call for visible task added - assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1) + // Secondary display should have 2 calls for registration + visible task added + assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(2) assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1) } @@ -431,13 +450,13 @@ class DesktopRepositoryTest : ShellTestCase() { repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = false) executor.flushAll() - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4) repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = false) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0) - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4) + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(5) } /** @@ -458,7 +477,8 @@ class DesktopRepositoryTest : ShellTestCase() { repo.updateTask(INVALID_DISPLAY, taskId = 1, isVisible = false) executor.flushAll() - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) + // 1 from registering, 1x3 for each update including the one to the invalid display. + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4) assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) } @@ -497,6 +517,8 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun getVisibleTaskCount_multipleDisplays_returnsCorrectCount() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0) @@ -674,8 +696,6 @@ class DesktopRepositoryTest : ShellTestCase() { repo.removeTask(INVALID_DISPLAY, taskId = 1) - val invalidDisplayTasks = repo.getFreeformTasksInZOrder(INVALID_DISPLAY) - assertThat(invalidDisplayTasks).isEmpty() val validDisplayTasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) assertThat(validDisplayTasks).isEmpty() } @@ -746,6 +766,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeTask_validDisplay_differentDisplay_doesNotRemovesTask() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeTask(SECOND_DISPLAY, taskId = 1) @@ -758,6 +779,7 @@ class DesktopRepositoryTest : ShellTestCase() { @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) fun removeTask_validDisplayButDifferentDisplay_persistenceEnabled_doesNotRemoveTask() { runTest(StandardTestDispatcher()) { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeTask(SECOND_DISPLAY, taskId = 1) @@ -784,10 +806,10 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeTask_removesTaskBoundsBeforeMaximize() { val taskId = 1 - repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200)) - repo.removeTask(THIRD_DISPLAY, taskId) + repo.removeTask(DEFAULT_DISPLAY, taskId) assertThat(repo.removeBoundsBeforeMaximize(taskId)).isNull() } @@ -795,16 +817,17 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeTask_removesTaskBoundsBeforeImmersive() { val taskId = 1 - repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) repo.saveBoundsBeforeFullImmersive(taskId, Rect(0, 0, 200, 200)) - repo.removeTask(THIRD_DISPLAY, taskId) + repo.removeTask(DEFAULT_DISPLAY, taskId) assertThat(repo.removeBoundsBeforeFullImmersive(taskId)).isNull() } @Test fun removeTask_removesActiveTask() { + repo.addDesk(THIRD_DISPLAY, THIRD_DISPLAY) val taskId = 1 val listener = TestListener() repo.addActiveTaskListener(listener) @@ -829,6 +852,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeTask_updatesTaskVisibility() { + repo.addDesk(displayId = THIRD_DISPLAY, deskId = THIRD_DISPLAY) val taskId = 1 repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) @@ -930,8 +954,8 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun updateTask_minimizedTaskBecomesVisible_unminimizesTask() { - repo.minimizeTask(displayId = 10, taskId = 2) - repo.updateTask(displayId = 10, taskId = 2, isVisible = true) + repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) + repo.updateTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true) val isMinimizedTask = repo.isMinimizedTask(taskId = 2) @@ -1003,34 +1027,34 @@ class DesktopRepositoryTest : ShellTestCase() { fun setTaskInFullImmersiveState_savedAsInImmersiveState() { assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse() - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue() } @Test fun removeTaskInFullImmersiveState_removedAsInImmersiveState() { - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue() - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = false) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = false) assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse() } @Test fun removeTaskInFullImmersiveState_otherWasImmersive_otherRemainsImmersive() { - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 2, immersive = false) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 2, immersive = false) assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue() } @Test fun setTaskInFullImmersiveState_sameDisplay_overridesExistingFullImmersiveTask() { - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true) - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 2, immersive = true) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 2, immersive = true) assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isFalse() assertThat(repo.isTaskInFullImmersiveState(taskId = 2)).isTrue() @@ -1038,8 +1062,10 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun setTaskInFullImmersiveState_differentDisplay_bothAreImmersive() { - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true) - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1, taskId = 2, immersive = true) + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) + repo.setTaskInFullImmersiveState(SECOND_DISPLAY, taskId = 2, immersive = true) assertThat(repo.isTaskInFullImmersiveState(taskId = 1)).isTrue() assertThat(repo.isTaskInFullImmersiveState(taskId = 2)).isTrue() @@ -1061,11 +1087,13 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun getTaskInFullImmersiveState_byDisplay() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID, taskId = 1, immersive = true) - repo.setTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1, taskId = 2, immersive = true) + repo.setTaskInFullImmersiveState(SECOND_DISPLAY, taskId = 2, immersive = true) assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID)).isEqualTo(1) - assertThat(repo.getTaskInFullImmersiveState(DEFAULT_DESKTOP_ID + 1)).isEqualTo(2) + assertThat(repo.getTaskInFullImmersiveState(SECOND_DISPLAY)).isEqualTo(2) } @Test @@ -1089,11 +1117,13 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun setTaskInPip_multipleDisplays_bothAreInPip() { + repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.setTaskInPip(DEFAULT_DESKTOP_ID, taskId = 1, enterPip = true) - repo.setTaskInPip(DEFAULT_DESKTOP_ID + 1, taskId = 2, enterPip = true) + repo.setTaskInPip(SECOND_DISPLAY, taskId = 2, enterPip = true) assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID, taskId = 1)).isTrue() - assertThat(repo.isTaskMinimizedPipInDisplay(DEFAULT_DESKTOP_ID + 1, taskId = 2)).isTrue() + assertThat(repo.isTaskMinimizedPipInDisplay(SECOND_DISPLAY, taskId = 2)).isTrue() } @Test @@ -1169,5 +1199,10 @@ class DesktopRepositoryTest : ShellTestCase() { const val THIRD_DISPLAY = 345 private const val DEFAULT_USER_ID = 1000 private const val DEFAULT_DESKTOP_ID = 0 + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> = + FlagsParameterization.allCombinationsOf(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) } } 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 fffaab36c9ad..0c1fd732f23e 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 @@ -48,8 +48,8 @@ import android.os.IBinder import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.platform.test.flag.junit.SetFlagsRule -import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.Gravity @@ -117,6 +117,7 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCR import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler +import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer @@ -184,6 +185,8 @@ import org.mockito.kotlin.capture import org.mockito.kotlin.eq import org.mockito.kotlin.whenever import org.mockito.quality.Strictness +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters /** * Test class for {@link DesktopTasksController} @@ -191,12 +194,12 @@ import org.mockito.quality.Strictness * Usage: atest WMShellUnitTests:DesktopTasksControllerTest */ @SmallTest -@RunWith(AndroidTestingRunner::class) +@RunWith(ParameterizedAndroidJunit4::class) @ExperimentalCoroutinesApi @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) -class DesktopTasksControllerTest : ShellTestCase() { +class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() + @JvmField @Rule val setFlagsRule = SetFlagsRule(flags) @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var shellCommandHandler: ShellCommandHandler @@ -247,6 +250,7 @@ class DesktopTasksControllerTest : ShellTestCase() { DesktopWallpaperActivityTokenProvider @Mock private lateinit var overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver + @Mock private lateinit var desksOrganizer: DesksOrganizer private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit @@ -358,6 +362,8 @@ class DesktopTasksControllerTest : ShellTestCase() { assumeTrue(ENABLE_SHELL_TRANSITIONS) taskRepository = userRepositories.current + taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DISPLAY) + taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DISPLAY) } private fun createController() = @@ -395,6 +401,7 @@ class DesktopTasksControllerTest : ShellTestCase() { desktopWallpaperActivityTokenProvider, Optional.of(bubbleController), overviewToDesktopTransitionObserver, + desksOrganizer, ) @After @@ -613,7 +620,12 @@ class DesktopTasksControllerTest : ShellTestCase() { Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, ) + @DisableFlags( + /** TODO: b/362720497 - re-enable when activation is implemented. */ + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND + ) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_shouldShowWallpaper() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTask = setUpHomeTask(SECOND_DISPLAY) val task1 = setUpFreeformTask(SECOND_DISPLAY) val task2 = setUpFreeformTask(SECOND_DISPLAY) @@ -634,8 +646,13 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags(Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, + /** TODO: b/362720497 - re-enable when activation is implemented. */ + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTask = setUpHomeTask(SECOND_DISPLAY) val task1 = setUpFreeformTask(SECOND_DISPLAY) val task2 = setUpFreeformTask(SECOND_DISPLAY) @@ -674,8 +691,13 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + /** TODO: b/362720497 - re-enable when activation is implemented. */ + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTask = setUpHomeTask(SECOND_DISPLAY) val task1 = setUpFreeformTask(SECOND_DISPLAY) val task2 = setUpFreeformTask(SECOND_DISPLAY) @@ -777,6 +799,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) setUpHomeTask(SECOND_DISPLAY) @@ -797,6 +820,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) setUpHomeTask(SECOND_DISPLAY) @@ -883,6 +907,8 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun visibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) setUpHomeTask() setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible) setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible) @@ -1476,6 +1502,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) markTaskHidden(freeformTaskDefault) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY) val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY) markTaskHidden(freeformTaskSecond) @@ -1673,6 +1700,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() { val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY) controller.moveToFullscreen(taskDefaultDisplay.taskId, transitionSource = UNKNOWN) @@ -1853,6 +1881,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test fun moveToNextDisplay_moveFromFirstToSecondDisplay() { // Set up two display ids + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) // Create a mock for the target display area: second display @@ -1882,6 +1911,7 @@ class DesktopTasksControllerTest : ShellTestCase() { whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) .thenReturn(defaultDisplayArea) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val task = setUpFreeformTask(displayId = SECOND_DISPLAY) controller.moveToNextDisplay(task.taskId) @@ -1901,6 +1931,7 @@ class DesktopTasksControllerTest : ShellTestCase() { ) fun moveToNextDisplay_wallpaperOnSystemUser_reorderWallpaperToBack() { // Set up two display ids + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) // Create a mock for the target display area: second display @@ -1925,6 +1956,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) fun moveToNextDisplay_wallpaperNotOnSystemUser_removeWallpaper() { // Set up two display ids + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) // Create a mock for the target display area: second display @@ -2049,6 +2081,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT) fun moveToNextDisplay_defaultBoundsWhenDestinationTooSmall() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) // Set up two display ids whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) @@ -2090,6 +2123,7 @@ class DesktopTasksControllerTest : ShellTestCase() { FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, ) fun moveToNextDisplay_destinationGainGlobalFocus() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) // Set up two display ids whenever(rootTaskDisplayAreaOrganizer.displayIds) .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) @@ -3158,6 +3192,8 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_closeTransition_singleTaskNoToken_secondaryDisplay_launchesHome() { + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + taskRepository.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val task = setUpFreeformTask(displayId = SECOND_DISPLAY) whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) @@ -4933,7 +4969,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() { - val triggerTask = setUpFullscreenTask(displayId = 5) + val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) taskRepository.setTaskInFullImmersiveState( displayId = triggerTask.displayId, taskId = triggerTask.taskId, @@ -4951,7 +4987,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_notOpening_doesNotPlay() { - val triggerTask = setUpFreeformTask(displayId = 5) + val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY) taskRepository.setTaskInFullImmersiveState( displayId = triggerTask.displayId, taskId = triggerTask.taskId, @@ -4969,7 +5005,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_notImmersive_doesNotPlay() { - val triggerTask = setUpFreeformTask(displayId = 5) + val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY) taskRepository.setTaskInFullImmersiveState( displayId = triggerTask.displayId, taskId = triggerTask.taskId, @@ -4988,8 +5024,8 @@ class DesktopTasksControllerTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_fullscreenEntersDesktop_plays() { // At least one freeform task to be in a desktop. - val existingTask = setUpFreeformTask(displayId = 5) - val triggerTask = setUpFullscreenTask(displayId = 5) + val existingTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue() taskRepository.setTaskInFullImmersiveState( displayId = existingTask.displayId, @@ -5008,7 +5044,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() { - val triggerTask = setUpFullscreenTask(displayId = 5) + val triggerTask = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() assertThat( @@ -5023,8 +5059,8 @@ class DesktopTasksControllerTest : ShellTestCase() { @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_freeformStaysInDesktop_plays() { // At least one freeform task to be in a desktop. - val existingTask = setUpFreeformTask(displayId = 5) - val triggerTask = setUpFreeformTask(displayId = 5, active = false) + val existingTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, active = false) assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue() taskRepository.setTaskInFullImmersiveState( displayId = existingTask.displayId, @@ -5043,7 +5079,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() { - val triggerTask = setUpFreeformTask(displayId = 5, active = false) + val triggerTask = setUpFreeformTask(displayId = DEFAULT_DISPLAY, active = false) assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() assertThat( @@ -5054,6 +5090,19 @@ class DesktopTasksControllerTest : ShellTestCase() { .isFalse() } + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun testCreateDesk() { + val currentDeskCount = taskRepository.getNumberOfDesks(DEFAULT_DISPLAY) + whenever(desksOrganizer.createDesk(eq(DEFAULT_DISPLAY), any())).thenAnswer { invocation -> + (invocation.arguments[1] as DesksOrganizer.OnCreateCallback).onCreated(deskId = 5) + } + + controller.createDesk(DEFAULT_DISPLAY) + + assertThat(taskRepository.getNumberOfDesks(DEFAULT_DISPLAY)).isEqualTo(currentDeskCount + 1) + } + private class RunOnStartTransitionCallback : ((IBinder) -> Unit) { var invocations = 0 private set @@ -5387,6 +5436,11 @@ class DesktopTasksControllerTest : ShellTestCase() { val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) const val MAX_TASK_LIMIT = 6 private const val TASKBAR_FRAME_HEIGHT = 200 + + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> = + FlagsParameterization.allCombinationsOf(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) } } 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 index e85901bbd9d4..554b09f130bd 100644 --- 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 @@ -180,6 +180,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun addPendingMinimizeTransition_taskIsNotMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask() markTaskHidden(task) @@ -190,6 +192,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_noPendingTransition_taskIsNotMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask() markTaskHidden(task) @@ -203,6 +207,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_differentPendingTransition_taskIsNotMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val pendingTransition = Binder() val taskTransition = Binder() val task = setUpFreeformTask() @@ -219,6 +225,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_pendingTransition_noTaskChange_taskVisible_taskIsNotMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val transition = Binder() val task = setUpFreeformTask() markTaskVisible(task) @@ -232,6 +240,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_pendingTransition_noTaskChange_taskInvisible_taskIsMinimized() { val transition = Binder() + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task = setUpFreeformTask() markTaskHidden(task) addPendingMinimizeChange(transition, taskId = task.taskId) @@ -243,6 +253,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_pendingTransition_changeTaskToBack_taskIsMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val transition = Binder() val task = setUpFreeformTask() addPendingMinimizeChange(transition, taskId = task.taskId) @@ -257,6 +269,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_pendingTransition_changeTaskToBack_boundsSaved() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val bounds = Rect(0, 0, 200, 200) val transition = Binder() val task = setUpFreeformTask() @@ -280,6 +294,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun onTransitionReady_transitionMergedFromPending_taskIsMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val mergedTransition = Binder() val newTransition = Binder() val task = setUpFreeformTask() @@ -302,6 +318,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun removeLeftoverMinimizedTasks_activeNonMinimizedTasksStillAround_doesNothing() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTaskRepo.addTask(displayId = DEFAULT_DISPLAY, taskId = 1, isVisible = true) desktopTaskRepo.addTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true) desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) @@ -318,6 +336,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun removeLeftoverMinimizedTasks_noMinimizedTasks_doesNothing() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val wct = WindowContainerTransaction() desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks( DEFAULT_DISPLAY, @@ -330,6 +350,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun removeLeftoverMinimizedTasks_onlyMinimizedTasksLeft_removesAllMinimizedTasks() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task1.taskId) @@ -351,6 +373,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun removeLeftoverMinimizedTasks_onlyMinimizedTasksLeft_backNavEnabled_doesNothing() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task1.taskId) @@ -364,6 +388,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun addAndGetMinimizeTaskChanges_tasksWithinLimit_noTaskMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } val wct = WindowContainerTransaction() @@ -380,6 +406,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun addAndGetMinimizeTaskChanges_tasksAboveLimit_backTaskMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) // The following list will be ordered bottom -> top, as the last task is moved to top last. val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } @@ -399,6 +427,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun addAndGetMinimizeTaskChanges_nonMinimizedTasksWithinLimit_noTaskMinimized() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = tasks[0].taskId) @@ -416,6 +446,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimize_tasksWithinLimit_returnsNull() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } val minimizedTask = @@ -426,6 +458,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimize_tasksAboveLimit_returnsBackTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val tasks = (1..MAX_TASK_LIMIT + 1).map { setUpFreeformTask() } val minimizedTask = @@ -437,6 +471,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimize_tasksAboveLimit_otherLimit_returnsBackTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) desktopTasksLimiter = DesktopTasksLimiter( transitions, @@ -458,6 +494,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimize_withNewTask_tasksAboveLimit_returnsBackTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } val minimizedTask = @@ -472,6 +510,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimize_tasksAtLimit_newIntentReturnsBackTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize( @@ -486,6 +526,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun minimizeTransitionReadyAndFinished_logsJankInstrumentationBeginAndEnd() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } val transition = Binder() val task = setUpFreeformTask() @@ -510,6 +552,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun minimizeTransitionReadyAndAborted_logsJankInstrumentationBeginAndCancel() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } val transition = Binder() val task = setUpFreeformTask() @@ -534,6 +578,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun minimizeTransitionReadyAndMerged_logsJankInstrumentationBeginAndEnd() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) (1..<MAX_TASK_LIMIT).forEach { _ -> setUpFreeformTask() } val mergedTransition = Binder() val newTransition = Binder() @@ -566,6 +612,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getMinimizingTask_pendingTaskTransition_returnsTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val transition = Binder() val task = setUpFreeformTask() addPendingMinimizeChange( @@ -582,6 +630,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getMinimizingTask_activeTaskTransition_returnsTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val transition = Binder() val task = setUpFreeformTask() addPendingMinimizeChange( @@ -613,6 +663,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getUnminimizingTask_pendingTaskTransition_returnsTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val transition = Binder() val task = setUpFreeformTask() addPendingUnminimizeChange( @@ -632,6 +684,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getUnminimizingTask_activeTaskTransition_returnsTask() { + desktopTaskRepo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 0) + desktopTaskRepo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 0) val transition = Binder() val task = setUpFreeformTask() addPendingMinimizeChange( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt index aee8821a63f6..5f8991e81968 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTestHelpers.kt @@ -35,6 +35,7 @@ object DesktopTestHelpers { ): RunningTaskInfo = TestRunningTaskInfoBuilder() .setDisplayId(displayId) + .setParentTaskId(displayId) .setToken(MockToken().token()) .setActivityType(ACTIVITY_TYPE_STANDARD) .setWindowingMode(WINDOWING_MODE_FREEFORM) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt new file mode 100644 index 000000000000..a07203d86b75 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2025 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.multidesks + +import android.testing.AndroidTestingRunner +import android.view.Display +import android.view.SurfaceControl +import android.window.TransitionInfo +import android.window.WindowContainerTransaction +import android.window.WindowContainerTransaction.HierarchyOp +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.sysui.ShellCommandHandler +import com.android.wm.shell.sysui.ShellInit +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** + * Tests for [RootTaskDesksOrganizer]. + * + * Usage: atest WMShellUnitTests:RootTaskDesksOrganizerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class RootTaskDesksOrganizerTest : ShellTestCase() { + + private val testExecutor = TestShellExecutor() + private val testShellInit = ShellInit(testExecutor) + private val mockShellCommandHandler = mock<ShellCommandHandler>() + private val mockShellTaskOrganizer = mock<ShellTaskOrganizer>() + + private lateinit var organizer: RootTaskDesksOrganizer + + @Before + fun setUp() { + organizer = + RootTaskDesksOrganizer(testShellInit, mockShellCommandHandler, mockShellTaskOrganizer) + } + + @Test + fun testCreateDesk_callsBack() { + val callback = FakeOnCreateCallback() + organizer.createDesk(Display.DEFAULT_DISPLAY, callback) + + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + assertThat(callback.created).isTrue() + assertEquals(freeformRoot.taskId, callback.deskId) + } + + @Test + fun testOnTaskAppeared_withoutRequest_throws() { + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + + assertThrows(Exception::class.java) { + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + } + } + + @Test + fun testOnTaskAppeared_withRequestOnlyInAnotherDisplay_throws() { + organizer.createDesk(displayId = 2, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask(Display.DEFAULT_DISPLAY).apply { parentTaskId = -1 } + + assertThrows(Exception::class.java) { + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + } + } + + @Test + fun testOnTaskAppeared_duplicateRoot_throws() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + assertThrows(Exception::class.java) { + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + } + } + + @Test + fun testOnTaskVanished_removesRoot() { + val callback = FakeOnCreateCallback() + organizer.createDesk(Display.DEFAULT_DISPLAY, callback) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + organizer.onTaskVanished(freeformRoot) + + assertThat(organizer.roots.contains(freeformRoot.taskId)).isFalse() + } + + @Test + fun testDesktopWindowAppearsInDesk() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + + organizer.onTaskAppeared(child, SurfaceControl()) + + assertThat(organizer.roots[freeformRoot.taskId].children).contains(child.taskId) + } + + @Test + fun testDesktopWindowDisappearsFromDesk() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + + organizer.onTaskAppeared(child, SurfaceControl()) + organizer.onTaskVanished(child) + + assertThat(organizer.roots[freeformRoot.taskId].children).doesNotContain(child.taskId) + } + + @Test + fun testRemoveDesk() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val wct = WindowContainerTransaction() + organizer.removeDesk(wct, freeformRoot.taskId) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK && + hop.container == freeformRoot.token.asBinder() + } + ) + .isTrue() + } + + @Test + fun testRemoveDesk_didNotExist_throws() { + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + + val wct = WindowContainerTransaction() + assertThrows(Exception::class.java) { organizer.removeDesk(wct, freeformRoot.taskId) } + } + + @Test + fun testActivateDesk() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val wct = WindowContainerTransaction() + organizer.activateDesk(wct, freeformRoot.taskId) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REORDER && + hop.toTop && + hop.container == freeformRoot.token.asBinder() + } + ) + .isTrue() + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT && + hop.container == freeformRoot.token.asBinder() + } + ) + .isTrue() + } + + @Test + fun testActivateDesk_didNotExist_throws() { + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + + val wct = WindowContainerTransaction() + assertThrows(Exception::class.java) { organizer.activateDesk(wct, freeformRoot.taskId) } + } + + @Test + fun testMoveTaskToDesk() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val desktopTask = createFreeformTask().apply { parentTaskId = -1 } + val wct = WindowContainerTransaction() + organizer.moveTaskToDesk(wct, freeformRoot.taskId, desktopTask) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.isReparent && + hop.toTop && + hop.container == desktopTask.token.asBinder() && + hop.newParent == freeformRoot.token.asBinder() + } + ) + .isTrue() + } + + @Test + fun testMoveTaskToDesk_didNotExist_throws() { + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + + val desktopTask = createFreeformTask().apply { parentTaskId = -1 } + val wct = WindowContainerTransaction() + assertThrows(Exception::class.java) { + organizer.moveTaskToDesk(wct, freeformRoot.taskId, desktopTask) + } + } + + @Test + fun testGetDeskAtEnd() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val task = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + val endDesk = + organizer.getDeskAtEnd( + TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } + ) + + assertThat(endDesk).isEqualTo(freeformRoot.taskId) + } + + private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback { + var deskId: Int? = null + val created: Boolean + get() = deskId != null + + override fun onCreated(deskId: Int) { + this.deskId = deskId + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt index a3c441698905..9a8f264e98a4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt @@ -17,6 +17,7 @@ package com.android.wm.shell.desktopmode.persistence import android.os.UserManager +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner @@ -24,6 +25,7 @@ import android.view.Display.DEFAULT_DISPLAY import androidx.test.filters.SmallTest import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_HSUM import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopUserRepositories @@ -85,7 +87,9 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_DESKTOP_WINDOWING_HSUM) - fun initWithPersistence_multipleUsers_addedCorrectly() = + /** TODO: b/362720497 - add multi-desk version when implemented. */ + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun initWithPersistence_multipleUsers_addedCorrectly_multiDesksDisabled() = runTest(StandardTestDispatcher()) { whenever(persistentRepository.getUserDesktopRepositoryMap()) .thenReturn( @@ -145,7 +149,9 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) - fun initWithPersistence_singleUser_addedCorrectly() = + /** TODO: b/362720497 - add multi-desk version when implemented. */ + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun initWithPersistence_singleUser_addedCorrectly_multiDesksDisabled() = runTest(StandardTestDispatcher()) { whenever(persistentRepository.getUserDesktopRepositoryMap()) .thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1)) @@ -156,24 +162,24 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { repositoryInitializer.initialize(desktopUserRepositories) - // Desktop Repository currently returns all tasks across desktops for a specific user - // since the repository currently doesn't handle desktops. This test logic should be - // updated - // once the repository handles multiple desktops. assertThat( - desktopUserRepositories.getProfile(USER_ID_1).getActiveTasks(DEFAULT_DISPLAY) + desktopUserRepositories + .getProfile(USER_ID_1) + .getActiveTaskIdsInDesk(deskId = DEFAULT_DISPLAY) ) .containsExactly(1, 3, 4, 5) .inOrder() assertThat( desktopUserRepositories .getProfile(USER_ID_1) - .getExpandedTasksOrdered(DEFAULT_DISPLAY) + .getExpandedTasksIdsInDeskOrdered(deskId = DEFAULT_DISPLAY) ) .containsExactly(5, 1) .inOrder() assertThat( - desktopUserRepositories.getProfile(USER_ID_1).getMinimizedTasks(DEFAULT_DISPLAY) + desktopUserRepositories + .getProfile(USER_ID_1) + .getMinimizedTaskIdsInDesk(deskId = DEFAULT_DISPLAY) ) .containsExactly(3, 4) .inOrder() @@ -195,6 +201,7 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { val desktop1: Desktop = Desktop.newBuilder() .setDesktopId(DESKTOP_ID_1) + .setDisplayId(DEFAULT_DISPLAY) .addAllZOrderedTasks(freeformTasksInZOrder1) .putTasksByTaskId( 1, @@ -216,6 +223,7 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { val desktop2: Desktop = Desktop.newBuilder() .setDesktopId(DESKTOP_ID_2) + .setDisplayId(DEFAULT_DISPLAY) .addAllZOrderedTasks(freeformTasksInZOrder2) .putTasksByTaskId( 4, @@ -237,6 +245,7 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { val desktop3: Desktop = Desktop.newBuilder() .setDesktopId(DESKTOP_ID_3) + .setDisplayId(DEFAULT_DISPLAY) .addAllZOrderedTasks(freeformTasksInZOrder3) .putTasksByTaskId( 7, |