diff options
| author | 2024-07-22 10:28:54 +0000 | |
|---|---|---|
| committer | 2024-08-01 16:50:33 +0000 | |
| commit | e2832fdc0c98a3c50d0f2b1be9a5de83a04f08fc (patch) | |
| tree | 20c95fed354420e704cff656ffa8b913ec8fd3ed | |
| parent | 2e7481d198e55525e7b8d4bcce24eb03fa7aa024 (diff) | |
Snap desktop window to valid area on display rotate
Updated DesktopModeWindowDecorViewModel to listen to display rotations, and update the all task bounds if the task would end up unreachable on the rotated display bounds
Bug: 348389314
Test: atest DesktopModeWindowDecorViewModelTests
Test: manual testing
Flag: EXEMPT bugfix
Change-Id: Ia8ec5782d75bce4d94a44c9d8a8575b2889c9282
3 files changed, 179 insertions, 10 deletions
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 81942e8ef59b..d68c018267a6 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 @@ -89,6 +89,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser; +import com.android.wm.shell.common.DisplayChangeController; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.DisplayLayout; @@ -175,6 +176,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private boolean mInImmersiveMode; private final String mSysUIPackageName; + private final DisplayChangeController.OnDisplayChangingListener mOnDisplayChangingListener; private final ISystemGestureExclusionListener mGestureExclusionListener = new ISystemGestureExclusionListener.Stub() { @Override @@ -287,6 +289,31 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mSysUIPackageName = mContext.getResources().getString( com.android.internal.R.string.config_systemUi); mInteractionJankMonitor = interactionJankMonitor; + mOnDisplayChangingListener = (displayId, fromRotation, toRotation, displayAreaInfo, t) -> { + DesktopModeWindowDecoration decoration; + RunningTaskInfo taskInfo; + for (int i = 0; i < mWindowDecorByTaskId.size(); i++) { + decoration = mWindowDecorByTaskId.valueAt(i); + if (decoration == null) { + continue; + } else { + taskInfo = decoration.mTaskInfo; + } + + // Check if display has been rotated between portrait & landscape + if (displayId == taskInfo.displayId && taskInfo.isFreeform() + && (fromRotation % 2 != toRotation % 2)) { + // Check if the task bounds on the rotated display will be out of bounds. + // If so, then update task bounds to be within reachable area. + final Rect taskBounds = new Rect( + taskInfo.configuration.windowConfiguration.getBounds()); + if (DragPositioningCallbackUtility.snapTaskBoundsIfNecessary( + taskBounds, decoration.calculateValidDragArea())) { + t.setBounds(taskInfo.token, taskBounds); + } + } + } + }; shellInit.addInitCallback(this::onInit, this); } @@ -298,6 +325,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { new DesktopModeOnInsetsChangedListener()); mDesktopTasksController.setOnTaskResizeAnimationListener( new DesktopModeOnTaskResizeAnimationListener()); + mDisplayController.addDisplayChangingController(mOnDisplayChangingListener); try { mWindowManager.registerSystemGestureExclusionListener(mGestureExclusionListener, mContext.getDisplayId()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 24fb97197f39..d70e225b9a9a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -312,8 +312,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // transaction (that applies task crop) is synced with the buffer transaction (that draws // the View). Both will be shown on screen at the same, whereas applying them independently // causes flickering. See b/270202228. - final boolean applyTransactionOnDraw = - taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; + final boolean applyTransactionOnDraw = taskInfo.isFreeform(); relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop); if (!applyTransactionOnDraw) { t.apply(); @@ -324,7 +323,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop) { Trace.beginSection("DesktopModeWindowDecoration#relayout"); - if (taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + if (taskInfo.isFreeform()) { // The Task is in Freeform mode -> show its header in sync since it's an integral part // of the window itself - a delayed header might cause bad UX. relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw, @@ -524,9 +523,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo) { - final boolean isFreeform = - taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM; - return isFreeform && taskInfo.isResizeable; + return taskInfo.isFreeform() && taskInfo.isResizeable; } private void updateMaximizeMenu(SurfaceControl.Transaction startT) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index 61c7080789bf..bbf42b5fb1a1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -21,6 +21,7 @@ import android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.app.WindowConfiguration.WINDOWING_MODE_PINNED import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.app.WindowConfiguration.WindowingMode import android.content.ComponentName @@ -51,11 +52,13 @@ import android.view.InputMonitor import android.view.InsetsSource import android.view.InsetsState import android.view.KeyEvent +import android.view.Surface import android.view.SurfaceControl import android.view.SurfaceView import android.view.View import android.view.WindowInsets.Type.navigationBars import android.view.WindowInsets.Type.statusBars +import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession @@ -69,6 +72,7 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser +import com.android.wm.shell.common.DisplayChangeController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayInsetsController import com.android.wm.shell.common.DisplayLayout @@ -110,6 +114,7 @@ import org.mockito.kotlin.argThat import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doNothing import org.mockito.kotlin.eq +import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -166,6 +171,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { private lateinit var mockitoSession: StaticMockitoSession private lateinit var shellInit: ShellInit private lateinit var desktopModeOnInsetsChangedListener: DesktopModeOnInsetsChangedListener + private lateinit var displayChangingListener: DisplayChangeController.OnDisplayChangingListener private lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel @Before @@ -174,6 +180,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockitoSession() .strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java) + .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(Mockito.any()) } @@ -218,10 +225,17 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { shellInit.init() - val listenerCaptor = - argumentCaptor<DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener>() - verify(displayInsetsController).addInsetsChangedListener(anyInt(), listenerCaptor.capture()) - desktopModeOnInsetsChangedListener = listenerCaptor.firstValue + val insetListenerCaptor = + argumentCaptor<DesktopModeWindowDecorViewModel.DesktopModeOnInsetsChangedListener>() + verify(displayInsetsController) + .addInsetsChangedListener(anyInt(), insetListenerCaptor.capture()) + desktopModeOnInsetsChangedListener = insetListenerCaptor.firstValue + + val displayChangingListenerCaptor = + argumentCaptor<DisplayChangeController.OnDisplayChangingListener>() + verify(mockDisplayController) + .addDisplayChangingController(displayChangingListenerCaptor.capture()) + displayChangingListener = displayChangingListenerCaptor.firstValue } @After @@ -786,6 +800,135 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { }) } + @Test + fun testOnDisplayRotation_tasksOutOfValidArea_taskBoundsUpdated() { + val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) + val secondTask = + createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM) + val thirdTask = + createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM) + + doReturn(true).`when` { + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any()) + } + setUpMockDecorationsForTasks(task, secondTask, thirdTask) + + onTaskOpening(task) + onTaskOpening(secondTask) + onTaskOpening(thirdTask) + + val wct = mock<WindowContainerTransaction>() + + displayChangingListener.onDisplayChange( + task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct + ) + + verify(wct).setBounds(eq(task.token), any()) + verify(wct).setBounds(eq(secondTask.token), any()) + verify(wct).setBounds(eq(thirdTask.token), any()) + } + + @Test + fun testOnDisplayRotation_taskInValidArea_taskBoundsNotUpdated() { + val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) + val secondTask = + createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM) + val thirdTask = + createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM) + + doReturn(false).`when` { + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any()) + } + setUpMockDecorationsForTasks(task, secondTask, thirdTask) + + onTaskOpening(task) + onTaskOpening(secondTask) + onTaskOpening(thirdTask) + + val wct = mock<WindowContainerTransaction>() + displayChangingListener.onDisplayChange( + task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct + ) + + verify(wct, never()).setBounds(eq(task.token), any()) + verify(wct, never()).setBounds(eq(secondTask.token), any()) + verify(wct, never()).setBounds(eq(thirdTask.token), any()) + } + + @Test + fun testOnDisplayRotation_sameOrientationRotation_taskBoundsNotUpdated() { + val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) + val secondTask = + createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM) + val thirdTask = + createTask(displayId = task.displayId, windowingMode = WINDOWING_MODE_FREEFORM) + + setUpMockDecorationsForTasks(task, secondTask, thirdTask) + + onTaskOpening(task) + onTaskOpening(secondTask) + onTaskOpening(thirdTask) + + val wct = mock<WindowContainerTransaction>() + displayChangingListener.onDisplayChange( + task.displayId, Surface.ROTATION_0, Surface.ROTATION_180, null, wct + ) + + verify(wct, never()).setBounds(eq(task.token), any()) + verify(wct, never()).setBounds(eq(secondTask.token), any()) + verify(wct, never()).setBounds(eq(thirdTask.token), any()) + } + + @Test + fun testOnDisplayRotation_differentDisplayId_taskBoundsNotUpdated() { + val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) + val secondTask = createTask(displayId = -2, windowingMode = WINDOWING_MODE_FREEFORM) + val thirdTask = createTask(displayId = -3, windowingMode = WINDOWING_MODE_FREEFORM) + + doReturn(true).`when` { + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any()) + } + setUpMockDecorationsForTasks(task, secondTask, thirdTask) + + onTaskOpening(task) + onTaskOpening(secondTask) + onTaskOpening(thirdTask) + + val wct = mock<WindowContainerTransaction>() + displayChangingListener.onDisplayChange( + task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct + ) + + verify(wct).setBounds(eq(task.token), any()) + verify(wct, never()).setBounds(eq(secondTask.token), any()) + verify(wct, never()).setBounds(eq(thirdTask.token), any()) + } + + @Test + fun testOnDisplayRotation_nonFreeformTask_taskBoundsNotUpdated() { + val task = createTask(focused = true, windowingMode = WINDOWING_MODE_FREEFORM) + val secondTask = createTask(displayId = -2, windowingMode = WINDOWING_MODE_FULLSCREEN) + val thirdTask = createTask(displayId = -3, windowingMode = WINDOWING_MODE_PINNED) + + doReturn(true).`when` { + DragPositioningCallbackUtility.snapTaskBoundsIfNecessary(any(), any()) + } + setUpMockDecorationsForTasks(task, secondTask, thirdTask) + + onTaskOpening(task) + onTaskOpening(secondTask) + onTaskOpening(thirdTask) + + val wct = mock<WindowContainerTransaction>() + displayChangingListener.onDisplayChange( + task.displayId, Surface.ROTATION_0, Surface.ROTATION_90, null, wct + ) + + verify(wct).setBounds(eq(task.token), any()) + verify(wct, never()).setBounds(eq(secondTask.token), any()) + verify(wct, never()).setBounds(eq(thirdTask.token), any()) + } + private fun createOpenTaskDecoration( @WindowingMode windowingMode: Int, onMaxOrRestoreListenerCaptor: ArgumentCaptor<Function0<Unit>> = @@ -864,6 +1007,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { whenever(mockSplitScreenController.isTaskInSplitScreen(task.taskId)) .thenReturn(true) } + whenever(decoration.calculateValidDragArea()).thenReturn(Rect(0, 60, 2560, 1600)) return decoration } |