Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +56 −17 Original line number Diff line number Diff line Loading @@ -82,6 +82,7 @@ import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.recents.RecentTasksController Loading Loading @@ -562,10 +563,20 @@ class DesktopTasksController( ) } /** Move a task to the front */ fun moveTaskToFront(taskId: Int) { /** * Move a task to the front, using [remoteTransition]. * * Note: beyond moving a task to the front, this method will minimize a task if we reach the * Desktop task limit, so [remoteTransition] should also handle any such minimize change. */ @JvmOverloads fun moveTaskToFront(taskId: Int, remoteTransition: RemoteTransition? = null) { val task = shellTaskOrganizer.getRunningTaskInfo(taskId) if (task == null) moveBackgroundTaskToFront(taskId) else moveTaskToFront(task) if (task == null) { moveBackgroundTaskToFront(taskId, remoteTransition) } else { moveTaskToFront(task, remoteTransition) } } /** Loading @@ -573,12 +584,10 @@ class DesktopTasksController( * desktop. If outside of desktop and want to launch a background task in desktop, use * [moveBackgroundTaskToDesktop] instead. */ private fun moveBackgroundTaskToFront(taskId: Int) { private fun moveBackgroundTaskToFront(taskId: Int, remoteTransition: RemoteTransition?) { logV("moveBackgroundTaskToFront taskId=%s", taskId) val wct = WindowContainerTransaction() // TODO: b/342378842 - Instead of using default display, support multiple displays val taskToMinimize: RunningTaskInfo? = addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId) val runOnTransit = immersiveTransitionHandler .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY) wct.startTask( Loading @@ -587,26 +596,56 @@ class DesktopTasksController( launchWindowingMode = WINDOWING_MODE_FREEFORM }.toBundle(), ) val transition = transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */) addPendingMinimizeTransition(transition, taskToMinimize) val transition = startLaunchTransition(TRANSIT_OPEN, wct, taskId, remoteTransition) runOnTransit?.invoke(transition) } /** Move a task to the front */ fun moveTaskToFront(taskInfo: RunningTaskInfo) { /** * Move a task to the front, using [remoteTransition]. * * Note: beyond moving a task to the front, this method will minimize a task if we reach the * Desktop task limit, so [remoteTransition] should also handle any such minimize change. */ @JvmOverloads fun moveTaskToFront(taskInfo: RunningTaskInfo, remoteTransition: RemoteTransition? = null) { logV("moveTaskToFront taskId=%s", taskInfo.taskId) val wct = WindowContainerTransaction() wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */) val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable( wct, taskInfo.displayId) val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo.taskId) val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */) addPendingMinimizeTransition(transition, taskToMinimize) val transition = startLaunchTransition(TRANSIT_TO_FRONT, wct, taskInfo.taskId, remoteTransition) runOnTransit?.invoke(transition) } private fun startLaunchTransition( transitionType: Int, wct: WindowContainerTransaction, taskId: Int, remoteTransition: RemoteTransition?, ): IBinder { val taskToMinimize: RunningTaskInfo? = addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId) if (remoteTransition == null) { val t = transitions.startTransition(transitionType, wct, null /* handler */) addPendingMinimizeTransition(t, taskToMinimize) return t } if (taskToMinimize == null) { val remoteTransitionHandler = OneShotRemoteHandler(mainExecutor, remoteTransition) val t = transitions.startTransition(transitionType, wct, remoteTransitionHandler) remoteTransitionHandler.setTransition(t) return t } val remoteTransitionHandler = DesktopWindowLimitRemoteHandler( mainExecutor, rootTaskDisplayAreaOrganizer, remoteTransition, taskToMinimize.taskId) val t = transitions.startTransition(transitionType, wct, remoteTransitionHandler) remoteTransitionHandler.setTransition(t) addPendingMinimizeTransition(t, taskToMinimize) return t } /** * Move task to the next display. * Loading Loading @@ -2031,9 +2070,9 @@ class DesktopTasksController( } } override fun showDesktopApp(taskId: Int) { override fun showDesktopApp(taskId: Int, remoteTransition: RemoteTransition?) { executeRemoteCallWithTaskPermission(controller, "showDesktopApp") { c -> c.moveTaskToFront(taskId) c.moveTaskToFront(taskId, remoteTransition) } } Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl +7 −2 Original line number Diff line number Diff line Loading @@ -35,8 +35,13 @@ interface IDesktopMode { /** @deprecated this is no longer supported. */ void hideStashedDesktopApps(int displayId); /** Bring task with the given id to front */ oneway void showDesktopApp(int taskId); /** * Bring task with the given id to front, using the given remote transition. * * <p> Note: beyond moving a task to the front, this method will minimize a task if we reach the * Desktop task limit, so {@code remoteTransition} should also handle any such minimize change. */ oneway void showDesktopApp(int taskId, in @nullable RemoteTransition remoteTransition); /** Get count of visible desktop tasks on the given display */ int getVisibleTaskCount(int displayId); Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/minimize/DesktopWindowLimitRemoteHandler.kt 0 → 100644 +100 −0 Original line number Diff line number Diff line /* * 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.minimize import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_BACK import android.window.RemoteTransition import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.transition.OneShotRemoteHandler import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionHandler /** * Handles transitions that result in hitting the Desktop window limit, by performing some * preparation work and then delegating to [remoteTransition]. * * This transition handler prepares the leash of the minimizing change referenced by * [taskIdToMinimize], and then delegates to [remoteTransition] to perform the actual transition. */ class DesktopWindowLimitRemoteHandler( mainExecutor: ShellExecutor, private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, remoteTransition: RemoteTransition, private val taskIdToMinimize: Int, ) : TransitionHandler { private val oneShotRemoteHandler = OneShotRemoteHandler(mainExecutor, remoteTransition) private var transition: IBinder? = null /** Sets the transition that will be handled - this must be called before [startAnimation]. */ fun setTransition(transition: IBinder) { this.transition = transition oneShotRemoteHandler.setTransition(transition) } override fun handleRequest( transition: IBinder, request: TransitionRequestInfo ): WindowContainerTransaction? { this.transition = transition return oneShotRemoteHandler.handleRequest(transition, request) } override fun startAnimation( transition: IBinder, info: TransitionInfo, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback ): Boolean { if (transition != this.transition) return false val minimizeChange = findMinimizeChange(info, taskIdToMinimize) ?: return false // Reparent the minimize change back to the root task display area, so the minimizing window // isn't shown in front of other windows. We do this here in Shell since Launcher doesn't // have access to RootTaskDisplayAreaOrganizer. applyMinimizeChangeReparenting(info, minimizeChange, startTransaction) return oneShotRemoteHandler.startAnimation( transition, info, startTransaction, finishTransaction, finishCallback) } private fun applyMinimizeChangeReparenting( info: TransitionInfo, minimizeChange: Change, startTransaction: SurfaceControl.Transaction, ) { val taskInfo = minimizeChange.taskInfo ?: return if (taskInfo.isFreeform && TransitionUtil.isOpeningMode(info.type)) { rootTaskDisplayAreaOrganizer.reparentToDisplayArea( taskInfo.displayId, minimizeChange.leash, startTransaction) } } private fun findMinimizeChange( info: TransitionInfo, taskIdToMinimize: Int, ): Change? = info.changes.find { change -> change.taskInfo?.taskId == taskIdToMinimize && change.mode == TRANSIT_TO_BACK } } libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +39 −8 Original line number Diff line number Diff line Loading @@ -97,6 +97,7 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreef import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplitScreenTask import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.draganddrop.DragAndDropController Loading @@ -122,6 +123,7 @@ import java.util.function.Consumer import java.util.Optional import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlinx.coroutines.CoroutineScope Loading Loading @@ -1338,7 +1340,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val task1 = setUpFreeformTask() setUpFreeformTask() controller.moveTaskToFront(task1) controller.moveTaskToFront(task1, remoteTransition = null) val wct = getLatestWct(type = TRANSIT_TO_FRONT) assertThat(wct.hierarchyOps).hasSize(1) Loading @@ -1350,7 +1352,7 @@ class DesktopTasksControllerTest : ShellTestCase() { setUpHomeTask() val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } controller.moveTaskToFront(freeformTasks[0]) controller.moveTaskToFront(freeformTasks[0], remoteTransition = null) val wct = getLatestWct(type = TRANSIT_TO_FRONT) assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize Loading @@ -1358,12 +1360,41 @@ class DesktopTasksControllerTest : ShellTestCase() { wct.assertReorderAt(1, freeformTasks[1], toTop = false) } @Test fun moveTaskToFront_remoteTransition_usesOneshotHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() } val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) whenever( transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()) ).thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) } @Test fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() } val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) whenever( transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()) ).thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) assertThat(transitionHandlerArgCaptor.value) .isInstanceOf(DesktopWindowLimitRemoteHandler::class.java) } @Test fun moveTaskToFront_backgroundTask_launchesTask() { val task = createTaskInfo(1) whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) controller.moveTaskToFront(task.taskId) controller.moveTaskToFront(task.taskId, remoteTransition = null) val wct = getLatestWct(type = TRANSIT_OPEN) assertThat(wct.hierarchyOps).hasSize(1) Loading @@ -1376,12 +1407,12 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = createTaskInfo(1001) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) controller.moveTaskToFront(task.taskId) controller.moveTaskToFront(task.taskId, remoteTransition = null) val wct = getLatestWct(type = TRANSIT_OPEN) assertThat(wct.hierarchyOps.size).isEqualTo(2) // launch + minimize wct.assertReorderAt(0, freeformTasks[0], toTop = false) wct.assertLaunchTaskAt(1, task.taskId, WINDOWING_MODE_FREEFORM) wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) wct.assertReorderAt(1, freeformTasks[0], toTop = false) } @Test Loading Loading @@ -3372,7 +3403,7 @@ class DesktopTasksControllerTest : ShellTestCase() { .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit) whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition) controller.moveTaskToFront(task.taskId) controller.moveTaskToFront(task.taskId, remoteTransition = null) verify(mockDesktopFullImmersiveTransitionHandler) .exitImmersiveIfApplicable(any(), eq(task.displayId)) Loading @@ -3388,7 +3419,7 @@ class DesktopTasksControllerTest : ShellTestCase() { .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit) whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition) controller.moveTaskToFront(task.taskId) controller.moveTaskToFront(task.taskId, remoteTransition = null) verify(mockDesktopFullImmersiveTransitionHandler) .exitImmersiveIfApplicable(any(), eq(task.displayId)) Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/minimize/DesktopWindowLimitRemoteHandlerTest.kt 0 → 100644 +167 −0 Original line number Diff line number Diff line /* * 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.minimize import android.app.ActivityManager import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.os.Binder import android.os.IBinder import android.testing.AndroidTestingRunner import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.IRemoteTransition import android.window.RemoteTransition import androidx.test.filters.SmallTest import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.transition.TransitionInfoBuilder import com.android.wm.shell.transition.Transitions import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.mock import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidTestingRunner::class) class DesktopWindowLimitRemoteHandlerTest { private val shellExecutor = TestShellExecutor() private val transition: IBinder = Binder() private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() private val remoteTransition = mock<RemoteTransition>() private val iRemoteTransition = mock<IRemoteTransition>() private val startT = mock<Transaction>() private val finishT = mock<Transaction>() private val finishCallback = mock<Transitions.TransitionFinishCallback>() @Before fun setUp() { whenever(remoteTransition.remoteTransition).thenReturn(iRemoteTransition) whenever(iRemoteTransition.asBinder()).thenReturn(mock(IBinder::class.java)) } private fun createRemoteHandler(taskIdToMinimize: Int) = DesktopWindowLimitRemoteHandler( shellExecutor, rootTaskDisplayAreaOrganizer, remoteTransition, taskIdToMinimize) @Test fun startAnimation_dontSetTransition_returnsFalse() { val minimizeTask = createDesktopTask() val remoteHandler = createRemoteHandler(taskIdToMinimize = minimizeTask.taskId) assertThat(remoteHandler.startAnimation(transition, createMinimizeTransitionInfo(minimizeTask), startT, finishT, finishCallback) ).isFalse() } @Test fun startAnimation_noMinimizeChange_returnsFalse() { val remoteHandler = createRemoteHandler(taskIdToMinimize = 1) remoteHandler.setTransition(transition) val info = createToFrontTransitionInfo() assertThat( remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) ).isFalse() } @Test fun startAnimation_correctTransition_returnsTrue() { val minimizeTask = createDesktopTask() val remoteHandler = createRemoteHandler(taskIdToMinimize = minimizeTask.taskId) remoteHandler.setTransition(transition) val info = createMinimizeTransitionInfo(minimizeTask) assertThat( remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) ).isTrue() } @Test fun startAnimation_noMinimizeChange_doesNotReparentMinimizeChange() { val remoteHandler = createRemoteHandler(taskIdToMinimize = 1) remoteHandler.setTransition(transition) val info = createToFrontTransitionInfo() remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) verify(rootTaskDisplayAreaOrganizer, times(0)) .reparentToDisplayArea(anyInt(), any(), any()) } @Test fun startAnimation_hasMinimizeChange_reparentsMinimizeChange() { val minimizeTask = createDesktopTask() val remoteHandler = createRemoteHandler(taskIdToMinimize = minimizeTask.taskId) remoteHandler.setTransition(transition) val info = createMinimizeTransitionInfo(minimizeTask) remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) verify(rootTaskDisplayAreaOrganizer).reparentToDisplayArea(anyInt(), any(), any()) } @Test fun startAnimation_noMinimizeChange_doesNotStartRemoteAnimation() { val minimizeTask = createDesktopTask() val remoteHandler = createRemoteHandler(taskIdToMinimize = minimizeTask.taskId) remoteHandler.setTransition(transition) val info = createToFrontTransitionInfo() remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) verify(iRemoteTransition, times(0)).startAnimation(any(), any(), any(), any()) } @Test fun startAnimation_hasMinimizeChange_startsRemoteAnimation() { val minimizeTask = createDesktopTask() val remoteHandler = createRemoteHandler(taskIdToMinimize = minimizeTask.taskId) remoteHandler.setTransition(transition) val info = createMinimizeTransitionInfo(minimizeTask) remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) verify(iRemoteTransition).startAnimation(any(), any(), any(), any()) } private fun createDesktopTask() = TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build() private fun createToFrontTransitionInfo() = TransitionInfoBuilder(TRANSIT_TO_FRONT) .addChange(TRANSIT_TO_FRONT, TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build()) .build() private fun createMinimizeTransitionInfo(minimizeTask: ActivityManager.RunningTaskInfo) = TransitionInfoBuilder(TRANSIT_TO_FRONT) .addChange(TRANSIT_TO_FRONT, TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build()) .addChange(TRANSIT_TO_BACK, minimizeTask) .build() } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +56 −17 Original line number Diff line number Diff line Loading @@ -82,6 +82,7 @@ import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType import com.android.wm.shell.desktopmode.DragToDesktopTransitionHandler.DragToDesktopStateListener import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.recents.RecentTasksController Loading Loading @@ -562,10 +563,20 @@ class DesktopTasksController( ) } /** Move a task to the front */ fun moveTaskToFront(taskId: Int) { /** * Move a task to the front, using [remoteTransition]. * * Note: beyond moving a task to the front, this method will minimize a task if we reach the * Desktop task limit, so [remoteTransition] should also handle any such minimize change. */ @JvmOverloads fun moveTaskToFront(taskId: Int, remoteTransition: RemoteTransition? = null) { val task = shellTaskOrganizer.getRunningTaskInfo(taskId) if (task == null) moveBackgroundTaskToFront(taskId) else moveTaskToFront(task) if (task == null) { moveBackgroundTaskToFront(taskId, remoteTransition) } else { moveTaskToFront(task, remoteTransition) } } /** Loading @@ -573,12 +584,10 @@ class DesktopTasksController( * desktop. If outside of desktop and want to launch a background task in desktop, use * [moveBackgroundTaskToDesktop] instead. */ private fun moveBackgroundTaskToFront(taskId: Int) { private fun moveBackgroundTaskToFront(taskId: Int, remoteTransition: RemoteTransition?) { logV("moveBackgroundTaskToFront taskId=%s", taskId) val wct = WindowContainerTransaction() // TODO: b/342378842 - Instead of using default display, support multiple displays val taskToMinimize: RunningTaskInfo? = addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId) val runOnTransit = immersiveTransitionHandler .exitImmersiveIfApplicable(wct, DEFAULT_DISPLAY) wct.startTask( Loading @@ -587,26 +596,56 @@ class DesktopTasksController( launchWindowingMode = WINDOWING_MODE_FREEFORM }.toBundle(), ) val transition = transitions.startTransition(TRANSIT_OPEN, wct, null /* handler */) addPendingMinimizeTransition(transition, taskToMinimize) val transition = startLaunchTransition(TRANSIT_OPEN, wct, taskId, remoteTransition) runOnTransit?.invoke(transition) } /** Move a task to the front */ fun moveTaskToFront(taskInfo: RunningTaskInfo) { /** * Move a task to the front, using [remoteTransition]. * * Note: beyond moving a task to the front, this method will minimize a task if we reach the * Desktop task limit, so [remoteTransition] should also handle any such minimize change. */ @JvmOverloads fun moveTaskToFront(taskInfo: RunningTaskInfo, remoteTransition: RemoteTransition? = null) { logV("moveTaskToFront taskId=%s", taskInfo.taskId) val wct = WindowContainerTransaction() wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */) val runOnTransit = immersiveTransitionHandler.exitImmersiveIfApplicable( wct, taskInfo.displayId) val taskToMinimize = addAndGetMinimizeChangesIfNeeded(taskInfo.displayId, wct, taskInfo.taskId) val transition = transitions.startTransition(TRANSIT_TO_FRONT, wct, null /* handler */) addPendingMinimizeTransition(transition, taskToMinimize) val transition = startLaunchTransition(TRANSIT_TO_FRONT, wct, taskInfo.taskId, remoteTransition) runOnTransit?.invoke(transition) } private fun startLaunchTransition( transitionType: Int, wct: WindowContainerTransaction, taskId: Int, remoteTransition: RemoteTransition?, ): IBinder { val taskToMinimize: RunningTaskInfo? = addAndGetMinimizeChangesIfNeeded(DEFAULT_DISPLAY, wct, taskId) if (remoteTransition == null) { val t = transitions.startTransition(transitionType, wct, null /* handler */) addPendingMinimizeTransition(t, taskToMinimize) return t } if (taskToMinimize == null) { val remoteTransitionHandler = OneShotRemoteHandler(mainExecutor, remoteTransition) val t = transitions.startTransition(transitionType, wct, remoteTransitionHandler) remoteTransitionHandler.setTransition(t) return t } val remoteTransitionHandler = DesktopWindowLimitRemoteHandler( mainExecutor, rootTaskDisplayAreaOrganizer, remoteTransition, taskToMinimize.taskId) val t = transitions.startTransition(transitionType, wct, remoteTransitionHandler) remoteTransitionHandler.setTransition(t) addPendingMinimizeTransition(t, taskToMinimize) return t } /** * Move task to the next display. * Loading Loading @@ -2031,9 +2070,9 @@ class DesktopTasksController( } } override fun showDesktopApp(taskId: Int) { override fun showDesktopApp(taskId: Int, remoteTransition: RemoteTransition?) { executeRemoteCallWithTaskPermission(controller, "showDesktopApp") { c -> c.moveTaskToFront(taskId) c.moveTaskToFront(taskId, remoteTransition) } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl +7 −2 Original line number Diff line number Diff line Loading @@ -35,8 +35,13 @@ interface IDesktopMode { /** @deprecated this is no longer supported. */ void hideStashedDesktopApps(int displayId); /** Bring task with the given id to front */ oneway void showDesktopApp(int taskId); /** * Bring task with the given id to front, using the given remote transition. * * <p> Note: beyond moving a task to the front, this method will minimize a task if we reach the * Desktop task limit, so {@code remoteTransition} should also handle any such minimize change. */ oneway void showDesktopApp(int taskId, in @nullable RemoteTransition remoteTransition); /** Get count of visible desktop tasks on the given display */ int getVisibleTaskCount(int displayId); Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/minimize/DesktopWindowLimitRemoteHandler.kt 0 → 100644 +100 −0 Original line number Diff line number Diff line /* * 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.minimize import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_TO_BACK import android.window.RemoteTransition import android.window.TransitionInfo import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.transition.OneShotRemoteHandler import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionHandler /** * Handles transitions that result in hitting the Desktop window limit, by performing some * preparation work and then delegating to [remoteTransition]. * * This transition handler prepares the leash of the minimizing change referenced by * [taskIdToMinimize], and then delegates to [remoteTransition] to perform the actual transition. */ class DesktopWindowLimitRemoteHandler( mainExecutor: ShellExecutor, private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, remoteTransition: RemoteTransition, private val taskIdToMinimize: Int, ) : TransitionHandler { private val oneShotRemoteHandler = OneShotRemoteHandler(mainExecutor, remoteTransition) private var transition: IBinder? = null /** Sets the transition that will be handled - this must be called before [startAnimation]. */ fun setTransition(transition: IBinder) { this.transition = transition oneShotRemoteHandler.setTransition(transition) } override fun handleRequest( transition: IBinder, request: TransitionRequestInfo ): WindowContainerTransaction? { this.transition = transition return oneShotRemoteHandler.handleRequest(transition, request) } override fun startAnimation( transition: IBinder, info: TransitionInfo, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback ): Boolean { if (transition != this.transition) return false val minimizeChange = findMinimizeChange(info, taskIdToMinimize) ?: return false // Reparent the minimize change back to the root task display area, so the minimizing window // isn't shown in front of other windows. We do this here in Shell since Launcher doesn't // have access to RootTaskDisplayAreaOrganizer. applyMinimizeChangeReparenting(info, minimizeChange, startTransaction) return oneShotRemoteHandler.startAnimation( transition, info, startTransaction, finishTransaction, finishCallback) } private fun applyMinimizeChangeReparenting( info: TransitionInfo, minimizeChange: Change, startTransaction: SurfaceControl.Transaction, ) { val taskInfo = minimizeChange.taskInfo ?: return if (taskInfo.isFreeform && TransitionUtil.isOpeningMode(info.type)) { rootTaskDisplayAreaOrganizer.reparentToDisplayArea( taskInfo.displayId, minimizeChange.leash, startTransaction) } } private fun findMinimizeChange( info: TransitionInfo, taskIdToMinimize: Int, ): Change? = info.changes.find { change -> change.taskInfo?.taskId == taskIdToMinimize && change.mode == TRANSIT_TO_BACK } }
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +39 −8 Original line number Diff line number Diff line Loading @@ -97,6 +97,7 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreef import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createHomeTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplitScreenTask import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.draganddrop.DragAndDropController Loading @@ -122,6 +123,7 @@ import java.util.function.Consumer import java.util.Optional import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlinx.coroutines.CoroutineScope Loading Loading @@ -1338,7 +1340,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val task1 = setUpFreeformTask() setUpFreeformTask() controller.moveTaskToFront(task1) controller.moveTaskToFront(task1, remoteTransition = null) val wct = getLatestWct(type = TRANSIT_TO_FRONT) assertThat(wct.hierarchyOps).hasSize(1) Loading @@ -1350,7 +1352,7 @@ class DesktopTasksControllerTest : ShellTestCase() { setUpHomeTask() val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } controller.moveTaskToFront(freeformTasks[0]) controller.moveTaskToFront(freeformTasks[0], remoteTransition = null) val wct = getLatestWct(type = TRANSIT_TO_FRONT) assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize Loading @@ -1358,12 +1360,41 @@ class DesktopTasksControllerTest : ShellTestCase() { wct.assertReorderAt(1, freeformTasks[1], toTop = false) } @Test fun moveTaskToFront_remoteTransition_usesOneshotHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() } val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) whenever( transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()) ).thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) } @Test fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() } val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) whenever( transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()) ).thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) assertThat(transitionHandlerArgCaptor.value) .isInstanceOf(DesktopWindowLimitRemoteHandler::class.java) } @Test fun moveTaskToFront_backgroundTask_launchesTask() { val task = createTaskInfo(1) whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) controller.moveTaskToFront(task.taskId) controller.moveTaskToFront(task.taskId, remoteTransition = null) val wct = getLatestWct(type = TRANSIT_OPEN) assertThat(wct.hierarchyOps).hasSize(1) Loading @@ -1376,12 +1407,12 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = createTaskInfo(1001) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) controller.moveTaskToFront(task.taskId) controller.moveTaskToFront(task.taskId, remoteTransition = null) val wct = getLatestWct(type = TRANSIT_OPEN) assertThat(wct.hierarchyOps.size).isEqualTo(2) // launch + minimize wct.assertReorderAt(0, freeformTasks[0], toTop = false) wct.assertLaunchTaskAt(1, task.taskId, WINDOWING_MODE_FREEFORM) wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) wct.assertReorderAt(1, freeformTasks[0], toTop = false) } @Test Loading Loading @@ -3372,7 +3403,7 @@ class DesktopTasksControllerTest : ShellTestCase() { .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit) whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition) controller.moveTaskToFront(task.taskId) controller.moveTaskToFront(task.taskId, remoteTransition = null) verify(mockDesktopFullImmersiveTransitionHandler) .exitImmersiveIfApplicable(any(), eq(task.displayId)) Loading @@ -3388,7 +3419,7 @@ class DesktopTasksControllerTest : ShellTestCase() { .exitImmersiveIfApplicable(any(), eq(task.displayId))).thenReturn(runOnStartTransit) whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition) controller.moveTaskToFront(task.taskId) controller.moveTaskToFront(task.taskId, remoteTransition = null) verify(mockDesktopFullImmersiveTransitionHandler) .exitImmersiveIfApplicable(any(), eq(task.displayId)) Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/minimize/DesktopWindowLimitRemoteHandlerTest.kt 0 → 100644 +167 −0 Original line number Diff line number Diff line /* * 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.minimize import android.app.ActivityManager import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.os.Binder import android.os.IBinder import android.testing.AndroidTestingRunner import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.IRemoteTransition import android.window.RemoteTransition import androidx.test.filters.SmallTest import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.transition.TransitionInfoBuilder import com.android.wm.shell.transition.Transitions import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.mock import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidTestingRunner::class) class DesktopWindowLimitRemoteHandlerTest { private val shellExecutor = TestShellExecutor() private val transition: IBinder = Binder() private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() private val remoteTransition = mock<RemoteTransition>() private val iRemoteTransition = mock<IRemoteTransition>() private val startT = mock<Transaction>() private val finishT = mock<Transaction>() private val finishCallback = mock<Transitions.TransitionFinishCallback>() @Before fun setUp() { whenever(remoteTransition.remoteTransition).thenReturn(iRemoteTransition) whenever(iRemoteTransition.asBinder()).thenReturn(mock(IBinder::class.java)) } private fun createRemoteHandler(taskIdToMinimize: Int) = DesktopWindowLimitRemoteHandler( shellExecutor, rootTaskDisplayAreaOrganizer, remoteTransition, taskIdToMinimize) @Test fun startAnimation_dontSetTransition_returnsFalse() { val minimizeTask = createDesktopTask() val remoteHandler = createRemoteHandler(taskIdToMinimize = minimizeTask.taskId) assertThat(remoteHandler.startAnimation(transition, createMinimizeTransitionInfo(minimizeTask), startT, finishT, finishCallback) ).isFalse() } @Test fun startAnimation_noMinimizeChange_returnsFalse() { val remoteHandler = createRemoteHandler(taskIdToMinimize = 1) remoteHandler.setTransition(transition) val info = createToFrontTransitionInfo() assertThat( remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) ).isFalse() } @Test fun startAnimation_correctTransition_returnsTrue() { val minimizeTask = createDesktopTask() val remoteHandler = createRemoteHandler(taskIdToMinimize = minimizeTask.taskId) remoteHandler.setTransition(transition) val info = createMinimizeTransitionInfo(minimizeTask) assertThat( remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) ).isTrue() } @Test fun startAnimation_noMinimizeChange_doesNotReparentMinimizeChange() { val remoteHandler = createRemoteHandler(taskIdToMinimize = 1) remoteHandler.setTransition(transition) val info = createToFrontTransitionInfo() remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) verify(rootTaskDisplayAreaOrganizer, times(0)) .reparentToDisplayArea(anyInt(), any(), any()) } @Test fun startAnimation_hasMinimizeChange_reparentsMinimizeChange() { val minimizeTask = createDesktopTask() val remoteHandler = createRemoteHandler(taskIdToMinimize = minimizeTask.taskId) remoteHandler.setTransition(transition) val info = createMinimizeTransitionInfo(minimizeTask) remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) verify(rootTaskDisplayAreaOrganizer).reparentToDisplayArea(anyInt(), any(), any()) } @Test fun startAnimation_noMinimizeChange_doesNotStartRemoteAnimation() { val minimizeTask = createDesktopTask() val remoteHandler = createRemoteHandler(taskIdToMinimize = minimizeTask.taskId) remoteHandler.setTransition(transition) val info = createToFrontTransitionInfo() remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) verify(iRemoteTransition, times(0)).startAnimation(any(), any(), any(), any()) } @Test fun startAnimation_hasMinimizeChange_startsRemoteAnimation() { val minimizeTask = createDesktopTask() val remoteHandler = createRemoteHandler(taskIdToMinimize = minimizeTask.taskId) remoteHandler.setTransition(transition) val info = createMinimizeTransitionInfo(minimizeTask) remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) verify(iRemoteTransition).startAnimation(any(), any(), any(), any()) } private fun createDesktopTask() = TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build() private fun createToFrontTransitionInfo() = TransitionInfoBuilder(TRANSIT_TO_FRONT) .addChange(TRANSIT_TO_FRONT, TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build()) .build() private fun createMinimizeTransitionInfo(minimizeTask: ActivityManager.RunningTaskInfo) = TransitionInfoBuilder(TRANSIT_TO_FRONT) .addChange(TRANSIT_TO_FRONT, TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build()) .addChange(TRANSIT_TO_BACK, minimizeTask) .build() }