Loading libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +49 −1 Original line number Diff line number Diff line Loading @@ -863,7 +863,11 @@ class DesktopTasksController( } 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()) { Loading Loading @@ -1909,6 +1913,50 @@ class DesktopTasksController( } } /** * 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, Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +150 −0 Original line number Diff line number Diff line Loading @@ -83,6 +83,7 @@ import com.android.internal.jank.InteractionJankMonitor 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 Loading Loading @@ -297,6 +298,7 @@ class DesktopTasksControllerTest : ShellTestCase() { 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()) } Loading Loading @@ -1745,6 +1747,154 @@ class DesktopTasksControllerTest : ShellTestCase() { } } @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() Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +49 −1 Original line number Diff line number Diff line Loading @@ -863,7 +863,11 @@ class DesktopTasksController( } 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()) { Loading Loading @@ -1909,6 +1913,50 @@ class DesktopTasksController( } } /** * 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, Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +150 −0 Original line number Diff line number Diff line Loading @@ -83,6 +83,7 @@ import com.android.internal.jank.InteractionJankMonitor 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 Loading Loading @@ -297,6 +298,7 @@ class DesktopTasksControllerTest : ShellTestCase() { 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()) } Loading Loading @@ -1745,6 +1747,154 @@ class DesktopTasksControllerTest : ShellTestCase() { } } @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() Loading