diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java index b9d2be280efbc..546ce62fda597 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeController.java @@ -508,5 +508,15 @@ public class DesktopModeController implements RemoteCallable = ArraySet(), val visibleTasks: ArraySet = ArraySet(), + var stashed: Boolean = false ) // Tasks currently in freeform mode, ordered from top to bottom (top is at index 0). @@ -311,6 +312,33 @@ class DesktopModeTaskRepository { } } + /** + * Update stashed status on display with id [displayId] + */ + fun setStashed(displayId: Int, stashed: Boolean) { + val data = displayData.getOrCreate(displayId) + val oldValue = data.stashed + data.stashed = stashed + if (oldValue != stashed) { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTaskRepo: mark stashed=%b displayId=%d", + stashed, + displayId + ) + visibleTasksListeners.forEach { (listener, executor) -> + executor.execute { listener.onStashedChanged(displayId, stashed) } + } + } + } + + /** + * Check if display with id [displayId] has desktop tasks stashed + */ + fun isStashed(displayId: Int): Boolean { + return displayData[displayId]?.stashed ?: false + } + /** * Defines interface for classes that can listen to changes for active tasks in desktop mode. */ @@ -331,5 +359,11 @@ class DesktopModeTaskRepository { */ @JvmDefault fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {} + + /** + * Called when the desktop stashed status changes. + */ + @JvmDefault + fun onStashedChanged(displayId: Int, stashed: Boolean) {} } } 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 91bb155d9d01e..66f486dbca583 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 @@ -46,6 +46,7 @@ import com.android.wm.shell.common.ExecutorUtils import com.android.wm.shell.common.ExternalInterfaceBinder import com.android.wm.shell.common.RemoteCallable import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.common.SingleInstanceRemoteListener import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.common.annotations.ExternalThread import com.android.wm.shell.common.annotations.ShellMainThread @@ -118,6 +119,18 @@ class DesktopTasksController( } } + /** + * Stash desktop tasks on display with id [displayId]. + * + * When desktop tasks are stashed, launcher home screen icons are fully visible. New apps + * launched in this state will be added to the desktop. Existing desktop tasks will be brought + * back to front during the launch. + */ + fun stashDesktopApps(displayId: Int) { + KtProtoLog.v(WM_SHELL_DESKTOP_MODE, "DesktopTasksController: stashDesktopApps") + desktopModeTaskRepository.setStashed(displayId, true) + } + /** Get number of tasks that are marked as visible */ fun getVisibleTaskCount(displayId: Int): Int { return desktopModeTaskRepository.getVisibleTaskCount(displayId) @@ -397,6 +410,11 @@ class DesktopTasksController( transition: IBinder, request: TransitionRequestInfo ): WindowContainerTransaction? { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: handleRequest request=%s", + request + ) // Check if we should skip handling this transition val shouldHandleRequest = when { @@ -418,43 +436,63 @@ class DesktopTasksController( } val task: RunningTaskInfo = request.triggerTask - val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) - // Check if we should switch a fullscreen task to freeform - if (task.windowingMode == WINDOWING_MODE_FULLSCREEN) { - // If there are any visible desktop tasks, switch the task to freeform - if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { - KtProtoLog.d( - WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: switch fullscreen task to freeform on transition" + - " taskId=%d", - task.taskId - ) - return WindowContainerTransaction().also { wct -> - addMoveToDesktopChanges(wct, task.token) - } - } + return when { + // If display has tasks stashed, handle as stashed launch + desktopModeTaskRepository.isStashed(task.displayId) -> handleStashedTaskLaunch(task) + // Check if fullscreen task should be updated + task.windowingMode == WINDOWING_MODE_FULLSCREEN -> handleFullscreenTaskLaunch(task) + // Check if freeform task should be updated + task.windowingMode == WINDOWING_MODE_FREEFORM -> handleFreeformTaskLaunch(task) + else -> null } + } - // CHeck if we should switch a freeform task to fullscreen - if (task.windowingMode == WINDOWING_MODE_FREEFORM) { - // If no visible desktop tasks, switch this task to freeform as the transition came - // outside of this controller - if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) { - KtProtoLog.d( + private fun handleFreeformTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) + if (activeTasks.none { desktopModeTaskRepository.isVisibleTask(it) }) { + KtProtoLog.d( WM_SHELL_DESKTOP_MODE, "DesktopTasksController: switch freeform task to fullscreen oon transition" + - " taskId=%d", + " taskId=%d", task.taskId - ) - return WindowContainerTransaction().also { wct -> - addMoveToFullscreenChanges(wct, task.token) - } + ) + return WindowContainerTransaction().also { wct -> + addMoveToFullscreenChanges(wct, task.token) } } return null } + private fun handleFullscreenTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction? { + val activeTasks = desktopModeTaskRepository.getActiveTasks(task.displayId) + if (activeTasks.any { desktopModeTaskRepository.isVisibleTask(it) }) { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: switch fullscreen task to freeform on transition" + + " taskId=%d", + task.taskId + ) + return WindowContainerTransaction().also { wct -> + addMoveToDesktopChanges(wct, task.token) + } + } + return null + } + + private fun handleStashedTaskLaunch(task: RunningTaskInfo): WindowContainerTransaction { + KtProtoLog.d( + WM_SHELL_DESKTOP_MODE, + "DesktopTasksController: launch apps with stashed on transition taskId=%d", + task.taskId + ) + val wct = WindowContainerTransaction() + bringDesktopAppsToFront(task.displayId, wct) + addMoveToDesktopChanges(wct, task.token) + desktopModeTaskRepository.setStashed(task.displayId, false) + return wct + } + private fun addMoveToDesktopChanges( wct: WindowContainerTransaction, token: WindowContainerToken @@ -658,8 +696,46 @@ class DesktopTasksController( @BinderThread private class IDesktopModeImpl(private var controller: DesktopTasksController?) : IDesktopMode.Stub(), ExternalInterfaceBinder { + + private lateinit var remoteListener: + SingleInstanceRemoteListener + + private val listener: VisibleTasksListener = object : VisibleTasksListener { + override fun onVisibilityChanged(displayId: Int, visible: Boolean) { + // TODO(b/261234402): move visibility from sysui state to listener + remoteListener.call { l -> l.onVisibilityChanged(displayId, visible) } + } + + override fun onStashedChanged(displayId: Int, stashed: Boolean) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "IDesktopModeImpl: onStashedChanged stashed=%b display=%d", + stashed, + displayId + ) + remoteListener.call { l -> l.onStashedChanged(displayId, stashed) } + } + } + + init { + remoteListener = + SingleInstanceRemoteListener( + controller, + { c -> + c.desktopModeTaskRepository.addVisibleTasksListener( + listener, + c.mainExecutor + ) + }, + { c -> + c.desktopModeTaskRepository.removeVisibleTasksListener(listener) + } + ) + } + /** Invalidates this instance, preventing future calls from updating the controller. */ override fun invalidate() { + remoteListener.unregister() controller = null } @@ -670,6 +746,13 @@ class DesktopTasksController( ) { c -> c.showDesktopApps(displayId) } } + override fun stashDesktopApps(displayId: Int) { + ExecutorUtils.executeRemoteCallWithTaskPermission( + controller, + "stashDesktopApps" + ) { c -> c.stashDesktopApps(displayId) } + } + override fun getVisibleTaskCount(displayId: Int): Int { val result = IntArray(1) ExecutorUtils.executeRemoteCallWithTaskPermission( @@ -680,6 +763,18 @@ class DesktopTasksController( ) return result[0] } + + override fun setTaskListener(listener: IDesktopTaskListener?) { + KtProtoLog.v( + WM_SHELL_DESKTOP_MODE, + "IDesktopModeImpl: set task listener=%s", + listener ?: "null" + ) + ExecutorUtils.executeRemoteCallWithTaskPermission( + controller, + "setTaskListener" + ) { _ -> listener?.let { remoteListener.register(it) } ?: remoteListener.unregister() } + } } companion object { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl index 899d67267e690..e7d353f56e30d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopMode.aidl @@ -16,6 +16,8 @@ package com.android.wm.shell.desktopmode; +import com.android.wm.shell.desktopmode.IDesktopTaskListener; + /** * Interface that is exposed to remote callers to manipulate desktop mode features. */ @@ -24,6 +26,12 @@ interface IDesktopMode { /** Show apps on the desktop on the given display */ void showDesktopApps(int displayId); + /** Stash apps on the desktop to allow launching another app from home screen */ + void stashDesktopApps(int displayId); + /** Get count of visible desktop tasks on the given display */ int getVisibleTaskCount(int displayId); + + /** Set listener that will receive callbacks about updates to desktop tasks */ + oneway void setTaskListener(IDesktopTaskListener listener); } \ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl new file mode 100644 index 0000000000000..39128a863ec96 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 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; + +/** + * Allows external processes to register a listener in WMShell to get updates about desktop task + * state. + */ +interface IDesktopTaskListener { + + /** Desktop task visibility has change. Visible if at least 1 task is visible. */ + oneway void onVisibilityChanged(int displayId, boolean visible); + + /** Desktop task stashed status has changed. */ + oneway void onStashedChanged(int displayId, boolean stashed); +} \ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 3bc2f0e8674e4..17c0463309d71 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -313,6 +313,65 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { assertThat(tasks.first()).isEqualTo(6) } + @Test + fun setStashed_stateIsUpdatedForTheDisplay() { + repo.setStashed(DEFAULT_DISPLAY, true) + assertThat(repo.isStashed(DEFAULT_DISPLAY)).isTrue() + assertThat(repo.isStashed(SECOND_DISPLAY)).isFalse() + + repo.setStashed(DEFAULT_DISPLAY, false) + assertThat(repo.isStashed(DEFAULT_DISPLAY)).isFalse() + } + + @Test + fun setStashed_notifyListener() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + repo.setStashed(DEFAULT_DISPLAY, true) + executor.flushAll() + assertThat(listener.stashedOnDefaultDisplay).isTrue() + assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1) + + repo.setStashed(DEFAULT_DISPLAY, false) + executor.flushAll() + assertThat(listener.stashedOnDefaultDisplay).isFalse() + assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(2) + } + + @Test + fun setStashed_secondCallDoesNotNotify() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + repo.setStashed(DEFAULT_DISPLAY, true) + repo.setStashed(DEFAULT_DISPLAY, true) + executor.flushAll() + assertThat(listener.stashedChangesOnDefaultDisplay).isEqualTo(1) + } + + @Test + fun setStashed_tracksPerDisplay() { + val listener = TestVisibilityListener() + val executor = TestShellExecutor() + repo.addVisibleTasksListener(listener, executor) + + repo.setStashed(DEFAULT_DISPLAY, true) + executor.flushAll() + assertThat(listener.stashedOnDefaultDisplay).isTrue() + assertThat(listener.stashedOnSecondaryDisplay).isFalse() + + repo.setStashed(SECOND_DISPLAY, true) + executor.flushAll() + assertThat(listener.stashedOnDefaultDisplay).isTrue() + assertThat(listener.stashedOnSecondaryDisplay).isTrue() + + repo.setStashed(DEFAULT_DISPLAY, false) + executor.flushAll() + assertThat(listener.stashedOnDefaultDisplay).isFalse() + assertThat(listener.stashedOnSecondaryDisplay).isTrue() + } + class TestListener : DesktopModeTaskRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 @@ -332,6 +391,12 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { var visibleChangesOnDefaultDisplay = 0 var visibleChangesOnSecondaryDisplay = 0 + var stashedOnDefaultDisplay = false + var stashedOnSecondaryDisplay = false + + var stashedChangesOnDefaultDisplay = 0 + var stashedChangesOnSecondaryDisplay = 0 + override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) { when (displayId) { DEFAULT_DISPLAY -> { @@ -345,6 +410,20 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { else -> fail("Visible task listener received unexpected display id: $displayId") } } + + override fun onStashedChanged(displayId: Int, stashed: Boolean) { + when (displayId) { + DEFAULT_DISPLAY -> { + stashedOnDefaultDisplay = stashed + stashedChangesOnDefaultDisplay++ + } + SECOND_DISPLAY -> { + stashedOnSecondaryDisplay = stashed + stashedChangesOnDefaultDisplay++ + } + else -> fail("Visible task listener received unexpected display id: $displayId") + } + } } companion object { 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 1335ebf105a67..dbd8a469c5d18 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 @@ -450,6 +450,27 @@ class DesktopTasksControllerTest : ShellTestCase() { assertThat(result).isNull() } + @Test + fun handleRequest_fullscreenTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY) + markTaskHidden(stashedFreeformTask) + + val fullscreenTask = createFullscreenTask(DEFAULT_DISPLAY) + + controller.stashDesktopApps(DEFAULT_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + assertThat(result).isNotNull() + result!!.assertReorderSequence(stashedFreeformTask, fullscreenTask) + assertThat(result.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + + // Stashed state should be cleared + assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() + } + @Test fun handleRequest_freeformTask_freeformVisible_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -500,6 +521,25 @@ class DesktopTasksControllerTest : ShellTestCase() { .isEqualTo(WINDOWING_MODE_FULLSCREEN) } + @Test + fun handleRequest_freeformTask_desktopStashed_returnWCTWithAllAppsBroughtToFront() { + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + val stashedFreeformTask = setUpFreeformTask(DEFAULT_DISPLAY) + markTaskHidden(stashedFreeformTask) + + val freeformTask = createFreeformTask(DEFAULT_DISPLAY) + + controller.stashDesktopApps(DEFAULT_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(freeformTask)) + assertThat(result).isNotNull() + result?.assertReorderSequence(stashedFreeformTask, freeformTask) + + // Stashed state should be cleared + assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isFalse() + } + @Test fun handleRequest_notOpenOrToFrontTransition_returnNull() { assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -539,6 +579,14 @@ class DesktopTasksControllerTest : ShellTestCase() { assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() } + @Test + fun stashDesktopApps_stateUpdates() { + controller.stashDesktopApps(DEFAULT_DISPLAY) + + assertThat(desktopModeTaskRepository.isStashed(DEFAULT_DISPLAY)).isTrue() + assertThat(desktopModeTaskRepository.isStashed(SECOND_DISPLAY)).isFalse() + } + private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createFreeformTask(displayId) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task)