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
This commit is contained in:
Ats Jenk
2023-04-28 11:19:28 -07:00
parent e08458ee6a
commit 44fdf07f65
6 changed files with 141 additions and 0 deletions

View File

@@ -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}.
*/

View File

@@ -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
*/

View File

@@ -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`
*

View File

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

View File

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

View File

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