diff options
11 files changed, 408 insertions, 29 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 c81838f56a74..0f232d50f2fc 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 @@ -111,6 +111,7 @@ 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.DesksTransitionObserver; import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer; import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository; import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer; @@ -760,6 +761,7 @@ public abstract class WMShellModule { Optional<BubbleController> bubbleController, OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver, DesksOrganizer desksOrganizer, + DesksTransitionObserver desksTransitionObserver, UserProfileContexts userProfileContexts, DesktopModeCompatPolicy desktopModeCompatPolicy) { return new DesktopTasksController( @@ -797,6 +799,7 @@ public abstract class WMShellModule { bubbleController, overviewToDesktopTransitionObserver, desksOrganizer, + desksTransitionObserver, userProfileContexts, desktopModeCompatPolicy); } @@ -1134,6 +1137,7 @@ public abstract class WMShellModule { Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler, Optional<BackAnimationController> backAnimationController, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, + @NonNull DesksTransitionObserver desksTransitionObserver, ShellInit shellInit) { return desktopUserRepositories.flatMap( repository -> @@ -1146,11 +1150,20 @@ public abstract class WMShellModule { desktopMixedTransitionHandler.get(), backAnimationController.get(), desktopWallpaperActivityTokenProvider, + desksTransitionObserver, shellInit))); } @WMSingleton @Provides + static DesksTransitionObserver provideDesksTransitionObserver( + @NonNull @DynamicOverride DesktopUserRepositories desktopUserRepositories + ) { + return new DesksTransitionObserver(desktopUserRepositories); + } + + @WMSingleton + @Provides static Optional<DesktopMixedTransitionHandler> provideDesktopMixedTransitionHandler( Context context, Transitions transitions, 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 164d04bbde65..b93d2e396402 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 @@ -152,8 +152,8 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: desk id should be an integer") return false } - pw.println("Not implemented.") - return false + controller.removeDesk(deskId) + return true } private fun runRemoveAllDesks(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 4ff1a5f1be31..043b353ba380 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 @@ -174,6 +174,9 @@ class DesktopRepository( /** Returns the number of desks in the given display. */ fun getNumberOfDesks(displayId: Int) = desktopData.getNumberOfDesks(displayId) + /** Returns the display the given desk is in. */ + fun getDisplayForDesk(deskId: Int) = desktopData.getDisplayForDesk(deskId) + /** Adds [regionListener] to inform about changes to exclusion regions for all Desktop tasks. */ fun setExclusionRegionListener(regionListener: Consumer<Region>, executor: Executor) { desktopGestureExclusionListener = regionListener @@ -207,6 +210,14 @@ class DesktopRepository( desktopData.createDesk(displayId, deskId) } + /** Returns the ids of the existing desks in the given display. */ + @VisibleForTesting + fun getDeskIds(displayId: Int): Set<Int> = + desktopData.desksSequence(displayId).map { desk -> desk.deskId }.toSet() + + /** Returns the id of the default desk in the given display. */ + fun getDefaultDeskId(displayId: Int): Int? = getDefaultDesk(displayId)?.deskId + /** Returns the default desk in the given display. */ private fun getDefaultDesk(displayId: Int): Desk? = desktopData.getDefaultDesk(displayId) @@ -716,17 +727,13 @@ class DesktopRepository( } } - /** - * Removes the active desk for the given [displayId] and returns the active tasks on that desk. - * - * TODO: b/389960283 - add explicit [deskId] argument. - */ - fun removeDesk(displayId: Int): ArraySet<Int> { - val desk = desktopData.getActiveDesk(displayId) - if (desk == null) { - logW("Could not find desk to remove: displayId=%d", displayId) - return ArraySet() - } + /** Removes the given desk and returns the active tasks in that desk. */ + fun removeDesk(deskId: Int): Set<Int> { + val desk = + desktopData.getDesk(deskId) + ?: return emptySet<Int>().also { + logW("Could not find desk to remove: deskId=%d", deskId) + } val activeTasks = ArraySet(desk.activeTasks) desktopData.remove(desk.deskId) return activeTasks @@ -1066,7 +1073,7 @@ class DesktopRepository( } override fun getDisplayForDesk(deskId: Int): Int = - getAllActiveDesks().find { it.deskId == deskId }?.displayId + desksSequence().find { it.deskId == deskId }?.displayId ?: error("Display for desk=$deskId not found") } 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 06a64f32f0bc..3f88e7bddd34 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 @@ -103,7 +103,9 @@ 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.DeskTransition import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter @@ -185,6 +187,7 @@ class DesktopTasksController( private val bubbleController: Optional<BubbleController>, private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver, private val desksOrganizer: DesksOrganizer, + private val desksTransitionObserver: DesksTransitionObserver, private val userProfileContexts: UserProfileContexts, private val desktopModeCompatPolicy: DesktopModeCompatPolicy, ) : @@ -2374,20 +2377,62 @@ class DesktopTasksController( ) } - fun removeDesktop(displayId: Int) { + /** Removes the default desk in the given display. */ + @Deprecated("Deprecated with multi-desks.", ReplaceWith("removeDesk()")) + fun removeDefaultDeskInDisplay(displayId: Int) { + val deskId = + checkNotNull(taskRepository.getDefaultDeskId(displayId)) { + "Expected a default desk to exist" + } + removeDesk(displayId = displayId, deskId = deskId) + } + + /** Removes the given desk. */ + fun removeDesk(deskId: Int) { + val displayId = taskRepository.getDisplayForDesk(deskId) + removeDesk(displayId = displayId, deskId = deskId) + } + + private fun removeDesk(displayId: Int, deskId: Int) { if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) return + logV("removeDesk deskId=%d from displayId=%d", deskId, displayId) - val tasksToRemove = taskRepository.removeDesk(displayId) - val wct = WindowContainerTransaction() - tasksToRemove.forEach { - val task = shellTaskOrganizer.getRunningTaskInfo(it) - if (task != null) { - wct.removeTask(task.token) + val tasksToRemove = + if (Flags.enableMultipleDesktopsBackend()) { + taskRepository.getActiveTaskIdsInDesk(deskId) } else { - recentTasksController?.removeBackgroundTask(it) + // TODO: 362720497 - make sure minimized windows are also removed in WM + // and the repository. + taskRepository.removeDesk(deskId) + } + + val wct = WindowContainerTransaction() + if (!Flags.enableMultipleDesktopsBackend()) { + tasksToRemove.forEach { + val task = shellTaskOrganizer.getRunningTaskInfo(it) + if (task != null) { + wct.removeTask(task.token) + } else { + recentTasksController?.removeBackgroundTask(it) + } } + } else { + // TODO: 362720497 - double check background tasks are also removed. + desksOrganizer.removeDesk(wct, deskId) + } + if (!Flags.enableMultipleDesktopsBackend() && wct.isEmpty) return + val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null) + if (Flags.enableMultipleDesktopsBackend()) { + desksTransitionObserver.addPendingTransition( + DeskTransition.RemoveDesk( + token = transition, + displayId = displayId, + deskId = deskId, + tasks = tasksToRemove, + onDeskRemovedListener = onDeskRemovedListener, + ) + ) } - if (!wct.isEmpty) transitions.startTransition(TRANSIT_CLOSE, wct, null) } /** Enter split by using the focused desktop task in given `displayId`. */ @@ -3091,7 +3136,7 @@ class DesktopTasksController( override fun removeDesktop(displayId: Int) { executeRemoteCallWithTaskPermission(controller, "removeDesktop") { c -> - c.removeDesktop(displayId) + c.removeDefaultDeskInDisplay(displayId) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index b3648699ed0b..3ada988ba2a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -37,6 +37,7 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.back.BackAnimationController import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isExitDesktopModeTransition import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.desktopmode.DesktopModeStatus @@ -58,6 +59,7 @@ class DesktopTasksTransitionObserver( private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler, private val backAnimationController: BackAnimationController, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, + private val desksTransitionObserver: DesksTransitionObserver, shellInit: ShellInit, ) : Transitions.TransitionObserver { @@ -87,6 +89,7 @@ class DesktopTasksTransitionObserver( finishTransaction: SurfaceControl.Transaction, ) { // TODO: b/332682201 Update repository state + desksTransitionObserver.onTransitionReady(transition, info) if ( DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt new file mode 100644 index 000000000000..47088c0b545a --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt @@ -0,0 +1,33 @@ +/* + * 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.os.IBinder + +/** Represents shell-started transitions involving desks. */ +sealed class DeskTransition { + /** The transition token. */ + abstract val token: IBinder + + /** A transition to remove a desk and its tasks from a display. */ + data class RemoveDesk( + override val token: IBinder, + val displayId: Int, + val deskId: Int, + val tasks: Set<Int>, + val onDeskRemovedListener: OnDeskRemovedListener?, + ) : DeskTransition() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt new file mode 100644 index 000000000000..3e49b8a4538b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt @@ -0,0 +1,58 @@ +/* + * 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.os.IBinder +import android.view.WindowManager.TRANSIT_CLOSE +import android.window.TransitionInfo +import com.android.window.flags.Flags +import com.android.wm.shell.desktopmode.DesktopUserRepositories + +/** + * Observer of desk-related transitions, such as adding, removing or activating a whole desk. It + * tracks pending transitions and updates repository state once they finish. + */ +class DesksTransitionObserver(private val desktopUserRepositories: DesktopUserRepositories) { + private val deskTransitions = mutableMapOf<IBinder, DeskTransition>() + + /** Adds a pending desk transition to be tracked. */ + fun addPendingTransition(transition: DeskTransition) { + if (!Flags.enableMultipleDesktopsBackend()) return + deskTransitions[transition.token] = transition + } + + /** + * Called when any transition is ready, which may include transitions not tracked by this + * observer. + */ + fun onTransitionReady(transition: IBinder, info: TransitionInfo) { + if (!Flags.enableMultipleDesktopsBackend()) return + val deskTransition = deskTransitions.remove(transition) ?: return + val desktopRepository = desktopUserRepositories.current + when (deskTransition) { + is DeskTransition.RemoveDesk -> { + check(info.type == TRANSIT_CLOSE) { "Expected close transition for desk removal" } + // TODO: b/362720497 - consider verifying the desk was actually removed through the + // DesksOrganizer. The transition info won't have changes if the desk was not + // visible, such as when dismissing from Overview. + val deskId = deskTransition.deskId + val displayId = deskTransition.displayId + desktopRepository.removeDesk(deskTransition.deskId) + deskTransition.onDeskRemovedListener?.onDeskRemoved(displayId, deskId) + } + } + } +} 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 90f342f91a38..6a343c56d364 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 @@ -27,6 +27,7 @@ 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.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.ShellExecutor @@ -35,6 +36,7 @@ import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat import junit.framework.Assert.fail +import kotlin.test.assertEquals import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -1076,13 +1078,37 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) - val tasksBeforeRemoval = repo.removeDesk(displayId = DEFAULT_DISPLAY) + val tasksBeforeRemoval = repo.removeDesk(deskId = DEFAULT_DISPLAY) assertThat(tasksBeforeRemoval).containsExactly(1, 2, 3).inOrder() assertThat(repo.getActiveTasks(displayId = DEFAULT_DISPLAY)).isEmpty() } @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleDesks_active_removes() { + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2) + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + repo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + + repo.removeDesk(deskId = 3) + + assertThat(repo.getDeskIds(displayId = DEFAULT_DISPLAY)).doesNotContain(3) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleDesks_inactive_removes() { + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2) + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + repo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + + repo.removeDesk(deskId = 2) + + assertThat(repo.getDeskIds(displayId = DEFAULT_DISPLAY)).doesNotContain(2) + } + + @Test fun getTaskInFullImmersiveState_byDisplay() { repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) @@ -1164,6 +1190,26 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { assertThat(repo.getActiveTaskIdsInDesk(999)).contains(6) } + @Test + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun getDisplayForDesk() { + repo.addDesk(SECOND_DISPLAY, SECOND_DISPLAY) + + assertEquals(SECOND_DISPLAY, repo.getDisplayForDesk(deskId = SECOND_DISPLAY)) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun getDisplayForDesk_multipleDesks() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addDesk(DEFAULT_DISPLAY, deskId = 7) + repo.addDesk(SECOND_DISPLAY, deskId = 8) + repo.addDesk(SECOND_DISPLAY, deskId = 9) + + assertEquals(DEFAULT_DISPLAY, repo.getDisplayForDesk(deskId = 7)) + assertEquals(SECOND_DISPLAY, repo.getDisplayForDesk(deskId = 8)) + } + class TestListener : DesktopRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 05ecd5d4b72f..8e7545c2a99c 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 @@ -118,7 +118,9 @@ 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.DeskTransition import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer @@ -180,6 +182,7 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argThat import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeastOnce import org.mockito.kotlin.capture @@ -251,6 +254,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() private lateinit var overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver @Mock private lateinit var desksOrganizer: DesksOrganizer @Mock private lateinit var userProfileContexts: UserProfileContexts + @Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit @@ -406,6 +410,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Optional.of(bubbleController), overviewToDesktopTransitionObserver, desksOrganizer, + desksTransitionsObserver, userProfileContexts, desktopModeCompatPolicy, ) @@ -3619,13 +3624,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun removeDesktop_multipleTasks_removesAll() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleTasks_removesAll() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() val task3 = setUpFreeformTask() taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) - controller.removeDesktop(displayId = DEFAULT_DISPLAY) + controller.removeDefaultDeskInDisplay(displayId = DEFAULT_DISPLAY) val wct = getLatestWct(TRANSIT_CLOSE) assertThat(wct.hierarchyOps).hasSize(3) @@ -3636,14 +3642,15 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun removeDesktop_multipleTasksWithBackgroundTask_removesAll() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleTasksWithBackgroundTask_removesAll() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() val task3 = setUpFreeformTask() taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) whenever(shellTaskOrganizer.getRunningTaskInfo(task3.taskId)).thenReturn(null) - controller.removeDesktop(displayId = DEFAULT_DISPLAY) + controller.removeDefaultDeskInDisplay(displayId = DEFAULT_DISPLAY) val wct = getLatestWct(TRANSIT_CLOSE) assertThat(wct.hierarchyOps).hasSize(2) @@ -3653,6 +3660,30 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun removeDesk_multipleDesks_addsPendingTransition() { + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_CLOSE), any(), anyOrNull())) + .thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 2) + + controller.removeDesk(deskId = 2) + + verify(desksOrganizer).removeDesk(any(), eq(2)) + verify(desksTransitionsObserver) + .addPendingTransition( + argThat { + this is DeskTransition.RemoveDesk && + this.token == transition && + this.deskId == 2 + } + ) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { val spyController = spy(controller) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index 091159c67db5..c29edece5537 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -47,11 +47,13 @@ import com.android.wm.shell.back.BackAnimationController import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP +import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Before @@ -92,6 +94,7 @@ class DesktopTasksTransitionObserverTest { private val backAnimationController = mock<BackAnimationController>() private val desktopWallpaperActivityTokenProvider = mock<DesktopWallpaperActivityTokenProvider>() + private val desksTransitionObserver = mock<DesksTransitionObserver>() private val wallpaperToken = MockToken().token() private lateinit var transitionObserver: DesktopTasksTransitionObserver @@ -115,6 +118,7 @@ class DesktopTasksTransitionObserverTest { mixedHandler, backAnimationController, desktopWallpaperActivityTokenProvider, + desksTransitionObserver, shellInit, ) } @@ -411,6 +415,21 @@ class DesktopTasksTransitionObserverTest { verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) } + @Test + fun onTransitionReady_forwardsToDesksTransitionObserver() { + val transition = Binder() + val info = TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0) + + transitionObserver.onTransitionReady( + transition = transition, + info = info, + StubTransaction(), + StubTransaction(), + ) + + verify(desksTransitionObserver).onTransitionReady(transition, info) + } + private fun createBackNavigationTransition( task: RunningTaskInfo?, type: Int = TRANSIT_TO_BACK, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt new file mode 100644 index 000000000000..bfbaa84e9312 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.desktopmode.multidesks + +import android.os.Binder +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.AndroidTestingRunner +import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager.TRANSIT_CLOSE +import android.window.TransitionInfo +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.desktopmode.DesktopRepository +import com.android.wm.shell.desktopmode.DesktopUserRepositories +import com.android.wm.shell.sysui.ShellInit +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +/** + * Tests for [DesksTransitionObserver]. + * + * Build/Install/Run: atest WMShellUnitTests:DesksTransitionObserverTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesksTransitionObserverTest : ShellTestCase() { + + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + private lateinit var desktopUserRepositories: DesktopUserRepositories + private lateinit var observer: DesksTransitionObserver + + private val repository: DesktopRepository + get() = desktopUserRepositories.current + + @Before + fun setUp() { + desktopUserRepositories = + DesktopUserRepositories( + context, + ShellInit(TestShellExecutor()), + /* shellController= */ mock(), + /* persistentRepository= */ mock(), + /* repositoryInitializer= */ mock(), + /* mainCoroutineScope= */ mock(), + /* userManager= */ mock(), + ) + observer = DesksTransitionObserver(desktopUserRepositories) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_removeDesk_removesFromRepository() { + val transition = Binder() + val removeTransition = + DeskTransition.RemoveDesk( + transition, + displayId = DEFAULT_DISPLAY, + deskId = 5, + tasks = setOf(10, 11), + onDeskRemovedListener = null, + ) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(removeTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0), + ) + + assertThat(repository.getDeskIds(DEFAULT_DISPLAY)).doesNotContain(5) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_removeDesk_invokesOnRemoveListener() { + class FakeOnDeskRemovedListener : OnDeskRemovedListener { + var lastDeskRemoved: Int? = null + + override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) { + lastDeskRemoved = deskId + } + } + val transition = Binder() + val removeListener = FakeOnDeskRemovedListener() + val removeTransition = + DeskTransition.RemoveDesk( + transition, + displayId = DEFAULT_DISPLAY, + deskId = 5, + tasks = setOf(10, 11), + onDeskRemovedListener = removeListener, + ) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(removeTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0), + ) + + assertThat(removeListener.lastDeskRemoved).isEqualTo(5) + } +} |