Add an API to desktop mode to stash desktop apps
Keep track of the state that apps are stashed. When apps are stashed, always launch new apps on that display on desktop. Also bring other desktop tasks to front at the same time. Bug: 261234402 Test: atest DesktopTasksControllerTest DesktopModeTaskRepositoryTest Change-Id: Ia2ee633a779d0cb438490e90fc84f1cfda10e1fc
This commit is contained in:
@@ -508,5 +508,15 @@ public class DesktopModeController implements RemoteCallable<DesktopModeControll
|
||||
);
|
||||
return result[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stashDesktopApps(int displayId) throws RemoteException {
|
||||
// Stashing of desktop apps not needed. Apps always launch on desktop
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTaskListener(IDesktopTaskListener listener) throws RemoteException {
|
||||
// TODO(b/261234402): move visibility from sysui state to listener
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class DesktopModeTaskRepository {
|
||||
*/
|
||||
val activeTasks: ArraySet<Int> = ArraySet(),
|
||||
val visibleTasks: ArraySet<Int> = 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) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<DesktopTasksController, IDesktopTaskListener>
|
||||
|
||||
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<DesktopTasksController, IDesktopTaskListener>(
|
||||
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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user