Merge "Add an API to desktop mode to stash desktop apps" into udc-qpr-dev

This commit is contained in:
Ats Jenk
2023-05-17 00:16:04 +00:00
committed by Android (Google) Code Review
7 changed files with 330 additions and 26 deletions

View File

@@ -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
}
}
}

View File

@@ -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) {}
}
}

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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)