From 44fdf07f6573d3cbefd52091352ce33f28fda26f Mon Sep 17 00:00:00 2001 From: Ats Jenk Date: Fri, 28 Apr 2023 11:19:28 -0700 Subject: [PATCH] Add support to move task to another display Create an API in DesktopTasksController to move a task to another display. It will rotate through the existing displays and move the task to the next one. If the task is already on the last display, it will move it back to the first. Display list is ordered by display ids. If there is only one display, the method call is a no-op. Adding a dev option to window caption menu that is behind a flag. This dev option can be used to move the window. Bug: 278084491 Test: atest DesktopTasksControllerTest Test: use a virtual device with multiple displays, use select button to move an app between displays Change-Id: If30b48d04dfa86b64b80e39e63e365bf921a6f92 --- .../shell/RootTaskDisplayAreaOrganizer.java | 11 ++++ .../shell/desktopmode/DesktopModeStatus.java | 6 ++ .../desktopmode/DesktopTasksController.kt | 63 +++++++++++++++++++ .../DesktopModeWindowDecorViewModel.java | 7 +++ .../wm/shell/windowdecor/HandleMenu.java | 2 + .../desktopmode/DesktopTasksControllerTest.kt | 52 +++++++++++++++ 6 files changed, 141 insertions(+) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java index 544d75739547e..410ae78dba1bc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/RootTaskDisplayAreaOrganizer.java @@ -180,6 +180,17 @@ public class RootTaskDisplayAreaOrganizer extends DisplayAreaOrganizer { applyConfigChangesToContext(displayAreaInfo); } + /** + * Returns the list of display ids that are tracked by a {@link DisplayAreaInfo} + */ + public int[] getDisplayIds() { + int[] displayIds = new int[mDisplayAreasInfo.size()]; + for (int i = 0; i < mDisplayAreasInfo.size(); i++) { + displayIds[i] = mDisplayAreasInfo.keyAt(i); + } + return displayIds; + } + /** * Returns the {@link DisplayAreaInfo} of the {@link DisplayAreaInfo#displayId}. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java index d1760ed755475..76ca68bbfa75d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeStatus.java @@ -48,6 +48,12 @@ public class DesktopModeStatus { private static final boolean IS_VEILED_RESIZE_ENABLED = SystemProperties.getBoolean( "persist.wm.debug.desktop_veiled_resizing", true); + /** + * Flag to indicate is moving task to another display is enabled. + */ + public static final boolean IS_DISPLAY_CHANGE_ENABLED = SystemProperties.getBoolean( + "persist.wm.debug.desktop_change_display", false); + /** * Return {@code true} if desktop mode support is enabled */ 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 c814fe575e811..73a0e362a7445 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 @@ -234,6 +234,69 @@ class DesktopTasksController( } } + /** + * Move task to the next display. + * + * Queries all current known display ids and sorts them in ascending order. Then iterates + * through the list and looks for the display id that is larger than the display id for + * the passed in task. If a display with a higher id is not found, iterates through the list and + * finds the first display id that is not the display id for the passed in task. + * + * If a display matching the above criteria is found, re-parents the task to that display. + * No-op if no such display is found. + */ + fun moveToNextDisplay(taskId: Int) { + val task = shellTaskOrganizer.getRunningTaskInfo(taskId) + if (task == null) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d not found", taskId) + return + } + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: taskId=%d taskDisplayId=%d", + taskId, task.displayId) + + val displayIds = rootTaskDisplayAreaOrganizer.displayIds.sorted() + // Get the first display id that is higher than current task display id + var newDisplayId = displayIds.firstOrNull { displayId -> displayId > task.displayId } + if (newDisplayId == null) { + // No display with a higher id, get the first display id that is not the task display id + newDisplayId = displayIds.firstOrNull { displayId -> displayId < task.displayId } + } + if (newDisplayId == null) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToNextDisplay: next display not found") + return + } + moveToDisplay(task, newDisplayId) + } + + /** + * Move [task] to display with [displayId]. + * + * No-op if task is already on that display per [RunningTaskInfo.displayId]. + */ + private fun moveToDisplay(task: RunningTaskInfo, displayId: Int) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "moveToDisplay: taskId=%d displayId=%d", + task.taskId, displayId) + + if (task.displayId == displayId) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "moveToDisplay: task already on display") + return + } + + val displayAreaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(displayId) + if (displayAreaInfo == null) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "moveToDisplay: display not found") + return + } + + val wct = WindowContainerTransaction() + wct.reparent(task.token, displayAreaInfo.token, true /* onTop */) + if (Transitions.ENABLE_SHELL_TRANSITIONS) { + transitions.startTransition(TRANSIT_CHANGE, wct, null /* handler */) + } else { + shellTaskOrganizer.applyTransaction(wct) + } + } + /** * Get windowing move for a given `taskId` * diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 4ef3350d06a46..ff60130a07481 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -333,6 +333,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { decoration.closeHandleMenu(); } else if (id == R.id.collapse_menu_button) { decoration.closeHandleMenu(); + } else if (id == R.id.select_button) { + if (DesktopModeStatus.IS_DISPLAY_CHANGE_ENABLED) { + // TODO(b/278084491): dev option to enable display switching + // remove when select is implemented + mDesktopTasksController.ifPresent(c -> c.moveToNextDisplay(mTaskId)); + decoration.closeHandleMenu(); + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java index ed3cca0780844..ac4a597c15d17 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.java @@ -187,6 +187,8 @@ class HandleMenu { final View moreActionsPillView = mMoreActionsPill.mWindowViewHost.getView(); final Button closeBtn = moreActionsPillView.findViewById(R.id.close_button); closeBtn.setOnClickListener(mOnClickListener); + final Button selectBtn = moreActionsPillView.findViewById(R.id.select_button); + selectBtn.setOnClickListener(mOnClickListener); } /** 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 f506969f51df2..1335ebf105a67 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 @@ -31,6 +31,7 @@ import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_NONE import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_FRONT +import android.window.DisplayAreaInfo import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER @@ -343,6 +344,57 @@ class DesktopTasksControllerTest : ShellTestCase() { } } + @Test + fun moveToNextDisplay_noOtherDisplays() { + whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + controller.moveToNextDisplay(task.taskId) + verifyWCTNotExecuted() + } + + @Test + fun moveToNextDisplay_moveFromFirstToSecondDisplay() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + controller.moveToNextDisplay(task.taskId) + with(getLatestWct(expectTransition = TRANSIT_CHANGE)) { + assertThat(hierarchyOps).hasSize(1) + assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) + assertThat(hierarchyOps[0].isReparent).isTrue() + assertThat(hierarchyOps[0].newParent).isEqualTo(secondDisplayArea.token.asBinder()) + assertThat(hierarchyOps[0].toTop).isTrue() + } + } + + @Test + fun moveToNextDisplay_moveFromSecondToFirstDisplay() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: default display + val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .thenReturn(defaultDisplayArea) + + val task = setUpFreeformTask(displayId = SECOND_DISPLAY) + controller.moveToNextDisplay(task.taskId) + + with(getLatestWct(expectTransition = TRANSIT_CHANGE)) { + assertThat(hierarchyOps).hasSize(1) + assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) + assertThat(hierarchyOps[0].isReparent).isTrue() + assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder()) + assertThat(hierarchyOps[0].toTop).isTrue() + } + } + @Test fun getTaskWindowingMode() { val fullscreenTask = setUpFullscreenTask()