Merge "Adjust bounds of a task moving to another display" into main
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 609ac0a..d966aaa 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
@@ -863,7 +863,11 @@
}
val wct = WindowContainerTransaction()
- if (!task.isFreeform) addMoveToDesktopChanges(wct, task, displayId)
+ if (!task.isFreeform) {
+ addMoveToDesktopChanges(wct, task, displayId)
+ } else if (Flags.enableMoveToNextDisplayShortcut()) {
+ applyFreeformDisplayChange(wct, task, displayId)
+ }
wct.reparent(task.token, displayAreaInfo.token, true /* onTop */)
if (Flags.enablePerDisplayDesktopWallpaperActivity()) {
@@ -1909,6 +1913,50 @@
}
}
+ /**
+ * Apply changes to move a freeform task from one display to another, which includes handling
+ * density changes between displays.
+ */
+ private fun applyFreeformDisplayChange(
+ wct: WindowContainerTransaction,
+ taskInfo: RunningTaskInfo,
+ destDisplayId: Int,
+ ) {
+ val sourceLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return
+ val destLayout = displayController.getDisplayLayout(destDisplayId) ?: return
+ val bounds = taskInfo.configuration.windowConfiguration.bounds
+ val scaledWidth = bounds.width() * destLayout.densityDpi() / sourceLayout.densityDpi()
+ val scaledHeight = bounds.height() * destLayout.densityDpi() / sourceLayout.densityDpi()
+ val sourceWidthMargin = sourceLayout.width() - bounds.width()
+ val sourceHeightMargin = sourceLayout.height() - bounds.height()
+ val destWidthMargin = destLayout.width() - scaledWidth
+ val destHeightMargin = destLayout.height() - scaledHeight
+ val scaledLeft =
+ if (sourceWidthMargin != 0) {
+ bounds.left * destWidthMargin / sourceWidthMargin
+ } else {
+ destWidthMargin / 2
+ }
+ val scaledTop =
+ if (sourceHeightMargin != 0) {
+ bounds.top * destHeightMargin / sourceHeightMargin
+ } else {
+ destHeightMargin / 2
+ }
+ val boundsWithinDisplay =
+ if (destWidthMargin >= 0 && destHeightMargin >= 0) {
+ Rect(0, 0, scaledWidth, scaledHeight).apply {
+ offsetTo(
+ scaledLeft.coerceIn(0, destWidthMargin),
+ scaledTop.coerceIn(0, destHeightMargin),
+ )
+ }
+ } else {
+ getInitialBounds(destLayout, taskInfo, destDisplayId)
+ }
+ wct.setBounds(taskInfo.token, boundsWithinDisplay)
+ }
+
private fun getInitialBounds(
displayLayout: DisplayLayout,
taskInfo: RunningTaskInfo,
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 0eb88e3..0283c02 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
@@ -83,6 +83,7 @@
import com.android.window.flags.Flags
import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE
import com.android.window.flags.Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP
+import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT
import com.android.window.flags.Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY
import com.android.wm.shell.MockToken
import com.android.wm.shell.R
@@ -297,6 +298,7 @@
whenever(displayLayout.getStableBounds(any())).thenAnswer { i ->
(i.arguments.first() as Rect).set(STABLE_BOUNDS)
}
+ whenever(displayLayout.densityDpi()).thenReturn(160)
whenever(runBlocking { persistentRepository.readDesktop(any(), any()) })
.thenReturn(Desktop.getDefaultInstance())
doReturn(mockToast).`when` { Toast.makeText(any(), anyInt(), anyInt()) }
@@ -1746,6 +1748,154 @@
}
@Test
+ @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT)
+ fun moveToNextDisplay_sizeInDpPreserved() {
+ // 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)
+ // Two displays have different density
+ whenever(displayLayout.densityDpi()).thenReturn(320)
+ whenever(displayLayout.width()).thenReturn(2400)
+ whenever(displayLayout.height()).thenReturn(1600)
+ val secondaryLayout = mock(DisplayLayout::class.java)
+ whenever(displayController.getDisplayLayout(SECOND_DISPLAY)).thenReturn(secondaryLayout)
+ whenever(secondaryLayout.densityDpi()).thenReturn(160)
+ whenever(secondaryLayout.width()).thenReturn(1280)
+ whenever(secondaryLayout.height()).thenReturn(720)
+
+ // Place a task with a size of 640x480 at a position where the ratio of the left margin to
+ // the right margin is 1:3 and the ratio of top margin to the bottom margin is 1:2.
+ val task =
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, bounds = Rect(440, 374, 1080, 854))
+
+ controller.moveToNextDisplay(task.taskId)
+
+ with(getLatestWct(type = TRANSIT_CHANGE)) {
+ val taskChange = changes[task.token.asBinder()]
+ assertThat(taskChange).isNotNull()
+ // To preserve DP size, pixel size is changed to 320x240. The ratio of the left margin
+ // to the right margin and the ratio of the top margin to bottom margin are also
+ // preserved.
+ assertThat(taskChange!!.configuration.windowConfiguration.bounds)
+ .isEqualTo(Rect(240, 160, 560, 400))
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT)
+ fun moveToNextDisplay_shiftWithinDestinationDisplayBounds() {
+ // 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)
+ // Two displays have different density
+ whenever(displayLayout.densityDpi()).thenReturn(320)
+ whenever(displayLayout.width()).thenReturn(2400)
+ whenever(displayLayout.height()).thenReturn(1600)
+ val secondaryLayout = mock(DisplayLayout::class.java)
+ whenever(displayController.getDisplayLayout(SECOND_DISPLAY)).thenReturn(secondaryLayout)
+ whenever(secondaryLayout.densityDpi()).thenReturn(160)
+ whenever(secondaryLayout.width()).thenReturn(1280)
+ whenever(secondaryLayout.height()).thenReturn(720)
+
+ // Place a task with a size of 640x480 at a position where the bottom-right corner of the
+ // window is outside the source display bounds. The destination display still has enough
+ // space to place the window within its bounds.
+ val task =
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, bounds = Rect(2000, 1200, 2640, 1680))
+
+ controller.moveToNextDisplay(task.taskId)
+
+ with(getLatestWct(type = TRANSIT_CHANGE)) {
+ val taskChange = changes[task.token.asBinder()]
+ assertThat(taskChange).isNotNull()
+ assertThat(taskChange!!.configuration.windowConfiguration.bounds)
+ .isEqualTo(Rect(960, 480, 1280, 720))
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT)
+ fun moveToNextDisplay_maximizedTask() {
+ // 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)
+ // Two displays have different density
+ whenever(displayLayout.densityDpi()).thenReturn(320)
+ whenever(displayLayout.width()).thenReturn(1280)
+ whenever(displayLayout.height()).thenReturn(960)
+ val secondaryLayout = mock(DisplayLayout::class.java)
+ whenever(displayController.getDisplayLayout(SECOND_DISPLAY)).thenReturn(secondaryLayout)
+ whenever(secondaryLayout.densityDpi()).thenReturn(160)
+ whenever(secondaryLayout.width()).thenReturn(1280)
+ whenever(secondaryLayout.height()).thenReturn(720)
+
+ // Place a task with a size equals to display size.
+ val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY, bounds = Rect(0, 0, 1280, 960))
+
+ controller.moveToNextDisplay(task.taskId)
+
+ with(getLatestWct(type = TRANSIT_CHANGE)) {
+ val taskChange = changes[task.token.asBinder()]
+ assertThat(taskChange).isNotNull()
+ // DP size is preserved. The window is centered in the destination display.
+ assertThat(taskChange!!.configuration.windowConfiguration.bounds)
+ .isEqualTo(Rect(320, 120, 960, 600))
+ }
+ }
+
+ @Test
+ @EnableFlags(FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT)
+ fun moveToNextDisplay_defaultBoundsWhenDestinationTooSmall() {
+ // 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)
+ // Two displays have different density
+ whenever(displayLayout.densityDpi()).thenReturn(320)
+ whenever(displayLayout.width()).thenReturn(2400)
+ whenever(displayLayout.height()).thenReturn(1600)
+ val secondaryLayout = mock(DisplayLayout::class.java)
+ whenever(displayController.getDisplayLayout(SECOND_DISPLAY)).thenReturn(secondaryLayout)
+ whenever(secondaryLayout.densityDpi()).thenReturn(160)
+ whenever(secondaryLayout.width()).thenReturn(640)
+ whenever(secondaryLayout.height()).thenReturn(480)
+ whenever(secondaryLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i ->
+ (i.arguments.first() as Rect).set(0, 0, 640, 480)
+ }
+
+ // A task with a size of 1800x1200 is being placed. To preserve DP size,
+ // 900x600 pixels are needed, which does not fit in the destination display.
+ val task =
+ setUpFreeformTask(displayId = DEFAULT_DISPLAY, bounds = Rect(300, 200, 2100, 1400))
+
+ controller.moveToNextDisplay(task.taskId)
+
+ with(getLatestWct(type = TRANSIT_CHANGE)) {
+ val taskChange = changes[task.token.asBinder()]
+ assertThat(taskChange).isNotNull()
+ assertThat(taskChange!!.configuration.windowConfiguration.bounds.left).isAtLeast(0)
+ assertThat(taskChange.configuration.windowConfiguration.bounds.top).isAtLeast(0)
+ assertThat(taskChange.configuration.windowConfiguration.bounds.right).isAtMost(640)
+ assertThat(taskChange.configuration.windowConfiguration.bounds.bottom).isAtMost(480)
+ }
+ }
+
+ @Test
fun getTaskWindowingMode() {
val fullscreenTask = setUpFullscreenTask()
val freeformTask = setUpFreeformTask()