diff options
4 files changed, 920 insertions, 1 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 429e0564dd2c..055bc8f5f092 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 @@ -2014,7 +2014,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, Supplier<SurfaceControl.Transaction> transactionFactory, Handler handler) { final TaskPositioner taskPositioner = DesktopModeStatus.isVeiledResizeEnabled() - ? new VeiledResizeTaskPositioner( + // TODO(b/383632995): Update when the flag is launched. + ? (Flags.enableConnectedDisplaysWindowDrag() + ? new MultiDisplayVeiledResizeTaskPositioner( taskOrganizer, windowDecoration, displayController, @@ -2022,6 +2024,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, transitions, interactionJankMonitor, handler) + : new VeiledResizeTaskPositioner( + taskOrganizer, + windowDecoration, + displayController, + dragEventListener, + transitions, + interactionJankMonitor, + handler)) : new FluidResizeTaskPositioner( taskOrganizer, transitions, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt new file mode 100644 index 000000000000..8dc921c986ce --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor + +import android.graphics.PointF +import android.graphics.Rect +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.view.Choreographer +import android.view.Surface +import android.view.SurfaceControl +import android.view.WindowManager +import android.window.TransitionInfo +import android.window.TransitionRequestInfo +import android.window.WindowContainerTransaction +import com.android.internal.jank.Cuj +import com.android.internal.jank.InteractionJankMonitor +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.transition.Transitions +import java.util.concurrent.TimeUnit +import java.util.function.Supplier + +/** + * A task positioner that also takes into account resizing a + * [com.android.wm.shell.windowdecor.ResizeVeil] and dragging move across multiple displays. + * - If the drag is resizing the task, we resize the veil instead. + * - If the drag is repositioning, we consider multi-display topology if needed, and update in the + * typical manner. + */ +class MultiDisplayVeiledResizeTaskPositioner( + private val taskOrganizer: ShellTaskOrganizer, + private val desktopWindowDecoration: DesktopModeWindowDecoration, + private val displayController: DisplayController, + dragEventListener: DragPositioningCallbackUtility.DragEventListener, + private val transactionSupplier: Supplier<SurfaceControl.Transaction>, + private val transitions: Transitions, + private val interactionJankMonitor: InteractionJankMonitor, + @ShellMainThread private val handler: Handler, +) : TaskPositioner, Transitions.TransitionHandler { + private val dragEventListeners = + mutableListOf<DragPositioningCallbackUtility.DragEventListener>() + private val stableBounds = Rect() + private val taskBoundsAtDragStart = Rect() + private val repositionStartPoint = PointF() + private val repositionTaskBounds = Rect() + private val isResizing: Boolean + get() = + (ctrlType and DragPositioningCallback.CTRL_TYPE_TOP) != 0 || + (ctrlType and DragPositioningCallback.CTRL_TYPE_BOTTOM) != 0 || + (ctrlType and DragPositioningCallback.CTRL_TYPE_LEFT) != 0 || + (ctrlType and DragPositioningCallback.CTRL_TYPE_RIGHT) != 0 + + @DragPositioningCallback.CtrlType private var ctrlType = 0 + private var isResizingOrAnimatingResize = false + @Surface.Rotation private var rotation = 0 + + constructor( + taskOrganizer: ShellTaskOrganizer, + windowDecoration: DesktopModeWindowDecoration, + displayController: DisplayController, + dragEventListener: DragPositioningCallbackUtility.DragEventListener, + transitions: Transitions, + interactionJankMonitor: InteractionJankMonitor, + @ShellMainThread handler: Handler, + ) : this( + taskOrganizer, + windowDecoration, + displayController, + dragEventListener, + Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() }, + transitions, + interactionJankMonitor, + handler, + ) + + init { + dragEventListeners.add(dragEventListener) + } + + override fun onDragPositioningStart(ctrlType: Int, displayId: Int, x: Float, y: Float): Rect { + this.ctrlType = ctrlType + taskBoundsAtDragStart.set( + desktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.bounds + ) + repositionStartPoint[x] = y + if (isResizing) { + // Capture CUJ for re-sizing window in DW mode. + interactionJankMonitor.begin( + createLongTimeoutJankConfigBuilder(Cuj.CUJ_DESKTOP_MODE_RESIZE_WINDOW) + ) + if (!desktopWindowDecoration.mHasGlobalFocus) { + val wct = WindowContainerTransaction() + wct.reorder( + desktopWindowDecoration.mTaskInfo.token, + /* onTop= */ true, + /* includingParents= */ true, + ) + taskOrganizer.applyTransaction(wct) + } + } + for (dragEventListener in dragEventListeners) { + dragEventListener.onDragStart(desktopWindowDecoration.mTaskInfo.taskId) + } + repositionTaskBounds.set(taskBoundsAtDragStart) + val rotation = + desktopWindowDecoration.mTaskInfo.configuration.windowConfiguration.displayRotation + if (stableBounds.isEmpty || this.rotation != rotation) { + this.rotation = rotation + displayController + .getDisplayLayout(desktopWindowDecoration.mDisplay.displayId)!! + .getStableBounds(stableBounds) + } + return Rect(repositionTaskBounds) + } + + override fun onDragPositioningMove(displayId: Int, x: Float, y: Float): Rect { + check(Looper.myLooper() == handler.looper) { + "This method must run on the shell main thread." + } + val delta = DragPositioningCallbackUtility.calculateDelta(x, y, repositionStartPoint) + if ( + isResizing && + DragPositioningCallbackUtility.changeBounds( + ctrlType, + repositionTaskBounds, + taskBoundsAtDragStart, + stableBounds, + delta, + displayController, + desktopWindowDecoration, + ) + ) { + if (!isResizingOrAnimatingResize) { + for (dragEventListener in dragEventListeners) { + dragEventListener.onDragMove(desktopWindowDecoration.mTaskInfo.taskId) + } + desktopWindowDecoration.showResizeVeil(repositionTaskBounds) + isResizingOrAnimatingResize = true + } else { + desktopWindowDecoration.updateResizeVeil(repositionTaskBounds) + } + } else if (ctrlType == DragPositioningCallback.CTRL_TYPE_UNDEFINED) { + // Begin window drag CUJ instrumentation only when drag position moves. + interactionJankMonitor.begin( + createLongTimeoutJankConfigBuilder(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW) + ) + val t = transactionSupplier.get() + DragPositioningCallbackUtility.setPositionOnDrag( + desktopWindowDecoration, + repositionTaskBounds, + taskBoundsAtDragStart, + repositionStartPoint, + t, + x, + y, + ) + t.setFrameTimeline(Choreographer.getInstance().vsyncId) + t.apply() + } + return Rect(repositionTaskBounds) + } + + override fun onDragPositioningEnd(displayId: Int, x: Float, y: Float): Rect { + val delta = DragPositioningCallbackUtility.calculateDelta(x, y, repositionStartPoint) + if (isResizing) { + if (taskBoundsAtDragStart != repositionTaskBounds) { + DragPositioningCallbackUtility.changeBounds( + ctrlType, + repositionTaskBounds, + taskBoundsAtDragStart, + stableBounds, + delta, + displayController, + desktopWindowDecoration, + ) + desktopWindowDecoration.updateResizeVeil(repositionTaskBounds) + val wct = WindowContainerTransaction() + wct.setBounds(desktopWindowDecoration.mTaskInfo.token, repositionTaskBounds) + transitions.startTransition(WindowManager.TRANSIT_CHANGE, wct, this) + } else { + // If bounds haven't changed, perform necessary veil reset here as startAnimation + // won't be called. + resetVeilIfVisible() + } + interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_RESIZE_WINDOW) + } else { + DragPositioningCallbackUtility.updateTaskBounds( + repositionTaskBounds, + taskBoundsAtDragStart, + repositionStartPoint, + x, + y, + ) + interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW) + } + + ctrlType = DragPositioningCallback.CTRL_TYPE_UNDEFINED + taskBoundsAtDragStart.setEmpty() + repositionStartPoint[0f] = 0f + return Rect(repositionTaskBounds) + } + + private fun resetVeilIfVisible() { + if (isResizingOrAnimatingResize) { + desktopWindowDecoration.hideResizeVeil() + isResizingOrAnimatingResize = false + } + } + + private fun createLongTimeoutJankConfigBuilder(@Cuj.CujType cujType: Int) = + InteractionJankMonitor.Configuration.Builder.withSurface( + cujType, + desktopWindowDecoration.mContext, + desktopWindowDecoration.mTaskSurface, + handler, + ) + .setTimeout(LONG_CUJ_TIMEOUT_MS) + + override fun startAnimation( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: Transitions.TransitionFinishCallback, + ): Boolean { + for (change in info.changes) { + val sc = change.leash + val endBounds = change.endAbsBounds + val endPosition = change.endRelOffset + startTransaction + .setWindowCrop(sc, endBounds.width(), endBounds.height()) + .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat()) + finishTransaction + .setWindowCrop(sc, endBounds.width(), endBounds.height()) + .setPosition(sc, endPosition.x.toFloat(), endPosition.y.toFloat()) + } + + startTransaction.apply() + resetVeilIfVisible() + ctrlType = DragPositioningCallback.CTRL_TYPE_UNDEFINED + finishCallback.onTransitionFinished(null /* wct */) + isResizingOrAnimatingResize = false + interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_DRAG_WINDOW) + return true + } + + /** + * We should never reach this as this handler's transitions are only started from shell + * explicitly. + */ + override fun handleRequest( + transition: IBinder, + request: TransitionRequestInfo, + ): WindowContainerTransaction? { + return null + } + + override fun isResizingOrAnimating() = isResizingOrAnimatingResize + + override fun addDragEventListener( + dragEventListener: DragPositioningCallbackUtility.DragEventListener + ) { + dragEventListeners.add(dragEventListener) + } + + override fun removeDragEventListener( + dragEventListener: DragPositioningCallbackUtility.DragEventListener + ) { + dragEventListeners.remove(dragEventListener) + } + + companion object { + // Timeout used for resize and drag CUJs, this is longer than the default timeout to avoid + // timing out in the middle of a resize or drag action. + private val LONG_CUJ_TIMEOUT_MS = TimeUnit.SECONDS.toMillis(/* duration= */ 10L) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java index e011cc08903b..d2c79d76e6c1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/VeiledResizeTaskPositioner.java @@ -53,7 +53,12 @@ import java.util.function.Supplier; * {@link com.android.wm.shell.windowdecor.ResizeVeil}. * If the drag is resizing the task, we resize the veil instead. * If the drag is repositioning, we update in the typical manner. + * <p> + * @deprecated This class will be replaced by + * {@link com.android.wm.shell.windowdecor.MultiDisplayVeiledResizeTaskPositioner}. + * TODO(b/383632995): Remove this class after MultiDisplayVeiledResizeTaskPositioner is launched. */ +@Deprecated public class VeiledResizeTaskPositioner implements TaskPositioner, Transitions.TransitionHandler { // Timeout used for resize and drag CUJs, this is longer than the default timeout to avoid // timing out in the middle of a resize or drag action. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt new file mode 100644 index 000000000000..f179cac32244 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt @@ -0,0 +1,611 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor + +import android.app.ActivityManager +import android.app.WindowConfiguration +import android.content.Context +import android.content.res.Resources +import android.graphics.Point +import android.graphics.Rect +import android.os.Handler +import android.os.IBinder +import android.os.Looper +import android.testing.AndroidTestingRunner +import android.view.Display +import android.view.Surface.ROTATION_0 +import android.view.Surface.ROTATION_270 +import android.view.Surface.ROTATION_90 +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import android.view.WindowManager.TRANSIT_CHANGE +import android.window.TransitionInfo +import android.window.WindowContainerToken +import androidx.test.filters.SmallTest +import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread +import com.android.internal.jank.InteractionJankMonitor +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TransitionFinishCallback +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP +import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED +import java.util.function.Supplier +import junit.framework.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.argThat +import org.mockito.Mockito.eq +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +/** + * Tests for [MultiDisplayVeiledResizeTaskPositioner]. + * + * Build/Install/Run: atest WMShellUnitTests:MultiDisplayVeiledResizeTaskPositionerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { + + @Mock private lateinit var mockShellTaskOrganizer: ShellTaskOrganizer + @Mock private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration + @Mock + private lateinit var mockDragEventListener: DragPositioningCallbackUtility.DragEventListener + + @Mock private lateinit var taskToken: WindowContainerToken + @Mock private lateinit var taskBinder: IBinder + + @Mock private lateinit var mockDisplayController: DisplayController + @Mock private lateinit var mockDisplayLayout: DisplayLayout + @Mock private lateinit var mockDisplay: Display + @Mock private lateinit var mockTransactionFactory: Supplier<SurfaceControl.Transaction> + @Mock private lateinit var mockTransaction: SurfaceControl.Transaction + @Mock private lateinit var mockTransitionBinder: IBinder + @Mock private lateinit var mockTransitionInfo: TransitionInfo + @Mock private lateinit var mockFinishCallback: TransitionFinishCallback + @Mock private lateinit var mockTransitions: Transitions + @Mock private lateinit var mockContext: Context + @Mock private lateinit var mockResources: Resources + @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + private val mainHandler = Handler(Looper.getMainLooper()) + + private lateinit var taskPositioner: MultiDisplayVeiledResizeTaskPositioner + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + mockDesktopWindowDecoration.mDisplay = mockDisplay + mockDesktopWindowDecoration.mDecorWindowContext = mockContext + whenever(mockContext.getResources()).thenReturn(mockResources) + whenever(taskToken.asBinder()).thenReturn(taskBinder) + whenever(mockDisplayController.getDisplayLayout(DISPLAY_ID)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.densityDpi()).thenReturn(DENSITY_DPI) + whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + if ( + mockDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration + .displayRotation == ROTATION_90 || + mockDesktopWindowDecoration.mTaskInfo.configuration.windowConfiguration + .displayRotation == ROTATION_270 + ) { + (i.arguments.first() as Rect).set(STABLE_BOUNDS_LANDSCAPE) + } else { + (i.arguments.first() as Rect).set(STABLE_BOUNDS_PORTRAIT) + } + } + `when`(mockTransactionFactory.get()).thenReturn(mockTransaction) + mockDesktopWindowDecoration.mTaskInfo = + ActivityManager.RunningTaskInfo().apply { + taskId = TASK_ID + token = taskToken + minWidth = MIN_WIDTH + minHeight = MIN_HEIGHT + defaultMinSize = DEFAULT_MIN + displayId = DISPLAY_ID + configuration.windowConfiguration.setBounds(STARTING_BOUNDS) + configuration.windowConfiguration.displayRotation = ROTATION_90 + isResizeable = true + } + `when`(mockDesktopWindowDecoration.calculateValidDragArea()).thenReturn(VALID_DRAG_AREA) + mockDesktopWindowDecoration.mDisplay = mockDisplay + whenever(mockDisplay.displayId).thenAnswer { DISPLAY_ID } + + taskPositioner = + MultiDisplayVeiledResizeTaskPositioner( + mockShellTaskOrganizer, + mockDesktopWindowDecoration, + mockDisplayController, + mockDragEventListener, + mockTransactionFactory, + mockTransitions, + mockInteractionJankMonitor, + mainHandler, + ) + } + + @Test + fun testDragResize_noMove_doesNotShowResizeVeil() = runOnUiThread { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + verify(mockDesktopWindowDecoration, never()).showResizeVeil(STARTING_BOUNDS) + + taskPositioner.onDragPositioningEnd( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + verify(mockTransitions, never()) + .startTransition( + eq(TRANSIT_CHANGE), + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0 && + change.configuration.windowConfiguration.bounds == STARTING_BOUNDS + } + }, + eq(taskPositioner), + ) + verify(mockDesktopWindowDecoration, never()).hideResizeVeil() + } + + @Test + fun testDragResize_movesTask_doesNotShowResizeVeil() = runOnUiThread { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_UNDEFINED, + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + taskPositioner.onDragPositioningMove( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat() + 60, + STARTING_BOUNDS.top.toFloat() + 100, + ) + val rectAfterMove = Rect(STARTING_BOUNDS) + rectAfterMove.left += 60 + rectAfterMove.right += 60 + rectAfterMove.top += 100 + rectAfterMove.bottom += 100 + verify(mockTransaction) + .setPosition(any(), eq(rectAfterMove.left.toFloat()), eq(rectAfterMove.top.toFloat())) + + val endBounds = + taskPositioner.onDragPositioningEnd( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat() + 70, + STARTING_BOUNDS.top.toFloat() + 20, + ) + val rectAfterEnd = Rect(STARTING_BOUNDS) + rectAfterEnd.left += 70 + rectAfterEnd.right += 70 + rectAfterEnd.top += 20 + rectAfterEnd.bottom += 20 + + verify(mockDesktopWindowDecoration, never()).showResizeVeil(any()) + verify(mockDesktopWindowDecoration, never()).hideResizeVeil() + Assert.assertEquals(rectAfterEnd, endBounds) + } + + @Test + fun testDragResize_resize_boundsUpdateOnEnd() = runOnUiThread { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT or CTRL_TYPE_TOP, + DISPLAY_ID, + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + taskPositioner.onDragPositioningMove( + DISPLAY_ID, + STARTING_BOUNDS.right.toFloat() + 10, + STARTING_BOUNDS.top.toFloat() + 10, + ) + + val rectAfterMove = Rect(STARTING_BOUNDS) + rectAfterMove.right += 10 + rectAfterMove.top += 10 + verify(mockDesktopWindowDecoration).showResizeVeil(rectAfterMove) + verify(mockShellTaskOrganizer, never()) + .applyTransaction( + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0 && + change.configuration.windowConfiguration.bounds == rectAfterMove + } + } + ) + + taskPositioner.onDragPositioningEnd( + DISPLAY_ID, + STARTING_BOUNDS.right.toFloat() + 20, + STARTING_BOUNDS.top.toFloat() + 20, + ) + val rectAfterEnd = Rect(rectAfterMove) + rectAfterEnd.right += 10 + rectAfterEnd.top += 10 + verify(mockDesktopWindowDecoration).updateResizeVeil(any()) + verify(mockTransitions) + .startTransition( + eq(TRANSIT_CHANGE), + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0 && + change.configuration.windowConfiguration.bounds == rectAfterEnd + } + }, + eq(taskPositioner), + ) + } + + @Test + fun testDragResize_noEffectiveMove_skipsTransactionOnEnd() = runOnUiThread { + taskPositioner.onDragPositioningStart( + DISPLAY_ID, + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + taskPositioner.onDragPositioningMove( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + taskPositioner.onDragPositioningEnd( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat() + 10, + STARTING_BOUNDS.top.toFloat() + 10, + ) + + verify(mockTransitions, never()) + .startTransition( + eq(TRANSIT_CHANGE), + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0 && + change.configuration.windowConfiguration.bounds == STARTING_BOUNDS + } + }, + eq(taskPositioner), + ) + + verify(mockShellTaskOrganizer, never()) + .applyTransaction( + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0) + } + } + ) + } + + @Test + fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() = runOnUiThread { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_UNDEFINED, // drag + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + val newX = STARTING_BOUNDS.left.toFloat() + 5 + val newY = DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT.toFloat() - 1 + taskPositioner.onDragPositioningMove(DISPLAY_ID, newX, newY) + + taskPositioner.onDragPositioningEnd(DISPLAY_ID, newX, newY) + + verify(mockShellTaskOrganizer, never()) + .applyTransaction( + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + ((change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0) + } + } + ) + } + + @Test + fun testDragResize_resize_resizingTaskReorderedToTopWhenNotFocused() = runOnUiThread { + mockDesktopWindowDecoration.mHasGlobalFocus = false + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT, // Resize right + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + // Verify task is reordered to top + verify(mockShellTaskOrganizer) + .applyTransaction( + argThat { wct -> + return@argThat wct.hierarchyOps.any { hierarchyOps -> + hierarchyOps.container == taskBinder && hierarchyOps.toTop + } + } + ) + } + + @Test + fun testDragResize_resize_resizingTaskNotReorderedToTopWhenFocused() = runOnUiThread { + mockDesktopWindowDecoration.mHasGlobalFocus = true + taskPositioner.onDragPositioningStart( + CTRL_TYPE_RIGHT, // Resize right + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + // Verify task is not reordered to top + verify(mockShellTaskOrganizer, never()) + .applyTransaction( + argThat { wct -> + return@argThat wct.hierarchyOps.any { hierarchyOps -> + hierarchyOps.container == taskBinder && hierarchyOps.toTop + } + } + ) + } + + @Test + fun testDragResize_drag_draggedTaskNotReorderedToTop() = runOnUiThread { + mockDesktopWindowDecoration.mHasGlobalFocus = false + taskPositioner.onDragPositioningStart( + CTRL_TYPE_UNDEFINED, // drag + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + // Verify task is not reordered to top since task is already brought to top before dragging + // begins + verify(mockShellTaskOrganizer, never()) + .applyTransaction( + argThat { wct -> + return@argThat wct.hierarchyOps.any { hierarchyOps -> + hierarchyOps.container == taskBinder && hierarchyOps.toTop + } + } + ) + } + + @Test + fun testDragResize_drag_updatesStableBoundsOnRotate() = runOnUiThread { + // Test landscape stable bounds + performDrag( + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat(), + STARTING_BOUNDS.right.toFloat() + 2000, + STARTING_BOUNDS.bottom.toFloat() + 2000, + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + ) + val rectAfterDrag = Rect(STARTING_BOUNDS) + rectAfterDrag.right += 2000 + rectAfterDrag.bottom = STABLE_BOUNDS_LANDSCAPE.bottom + // First drag; we should fetch stable bounds. + verify(mockDisplayLayout, times(1)).getStableBounds(any()) + verify(mockTransitions) + .startTransition( + eq(TRANSIT_CHANGE), + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0 && + change.configuration.windowConfiguration.bounds == rectAfterDrag + } + }, + eq(taskPositioner), + ) + // Drag back to starting bounds. + performDrag( + STARTING_BOUNDS.right.toFloat() + 2000, + STARTING_BOUNDS.bottom.toFloat(), + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat(), + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + ) + + // Display did not rotate; we should use previous stable bounds + verify(mockDisplayLayout, times(1)).getStableBounds(any()) + + // Rotate the screen to portrait + mockDesktopWindowDecoration.mTaskInfo.apply { + configuration.windowConfiguration.displayRotation = ROTATION_0 + } + // Test portrait stable bounds + performDrag( + STARTING_BOUNDS.right.toFloat(), + STARTING_BOUNDS.bottom.toFloat(), + STARTING_BOUNDS.right.toFloat() + 2000, + STARTING_BOUNDS.bottom.toFloat() + 2000, + CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM, + ) + rectAfterDrag.right = STABLE_BOUNDS_PORTRAIT.right + rectAfterDrag.bottom = STARTING_BOUNDS.bottom + 2000 + + verify(mockTransitions) + .startTransition( + eq(TRANSIT_CHANGE), + argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + token == taskBinder && + (change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != + 0 && + change.configuration.windowConfiguration.bounds == rectAfterDrag + } + }, + eq(taskPositioner), + ) + // Display has rotated; we expect a new stable bounds. + verify(mockDisplayLayout, times(2)).getStableBounds(any()) + } + + @Test + fun testIsResizingOrAnimatingResizeSet() = runOnUiThread { + Assert.assertFalse(taskPositioner.isResizingOrAnimating) + + taskPositioner.onDragPositioningStart( + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + taskPositioner.onDragPositioningMove( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat() - 20, + STARTING_BOUNDS.top.toFloat() - 20, + ) + + // isResizingOrAnimating should be set to true after move during a resize + Assert.assertTrue(taskPositioner.isResizingOrAnimating) + verify(mockDragEventListener, times(1)).onDragMove(eq(TASK_ID)) + + taskPositioner.onDragPositioningEnd( + DISPLAY_ID, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + // isResizingOrAnimating should be not be set till false until after transition animation + Assert.assertTrue(taskPositioner.isResizingOrAnimating) + } + + @Test + fun testIsResizingOrAnimatingResizeResetAfterStartAnimation() = runOnUiThread { + performDrag( + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + STARTING_BOUNDS.left.toFloat() - 20, + STARTING_BOUNDS.top.toFloat() - 20, + CTRL_TYPE_TOP or CTRL_TYPE_RIGHT, + ) + + taskPositioner.startAnimation( + mockTransitionBinder, + mockTransitionInfo, + mockTransaction, + mockTransaction, + mockFinishCallback, + ) + + // isResizingOrAnimating should be set to false until after transition successfully consumed + Assert.assertFalse(taskPositioner.isResizingOrAnimating) + } + + @Test + fun testStartAnimation_useEndRelOffset() = runOnUiThread { + val changeMock = mock(TransitionInfo.Change::class.java) + val startTransaction = mock(Transaction::class.java) + val finishTransaction = mock(Transaction::class.java) + val point = Point(10, 20) + val bounds = Rect(1, 2, 3, 4) + `when`(changeMock.leash).thenReturn(mock(SurfaceControl::class.java)) + `when`(changeMock.endRelOffset).thenReturn(point) + `when`(changeMock.endAbsBounds).thenReturn(bounds) + `when`(mockTransitionInfo.changes).thenReturn(listOf(changeMock)) + `when`(startTransaction.setWindowCrop(any(), eq(bounds.width()), eq(bounds.height()))) + .thenReturn(startTransaction) + `when`(finishTransaction.setWindowCrop(any(), eq(bounds.width()), eq(bounds.height()))) + .thenReturn(finishTransaction) + + taskPositioner.startAnimation( + mockTransitionBinder, + mockTransitionInfo, + startTransaction, + finishTransaction, + mockFinishCallback, + ) + + verify(startTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(finishTransaction).setPosition(any(), eq(point.x.toFloat()), eq(point.y.toFloat())) + verify(changeMock).endRelOffset + } + + private fun performDrag(startX: Float, startY: Float, endX: Float, endY: Float, ctrlType: Int) { + taskPositioner.onDragPositioningStart(ctrlType, DISPLAY_ID, startX, startY) + taskPositioner.onDragPositioningMove(DISPLAY_ID, endX, endY) + + taskPositioner.onDragPositioningEnd(DISPLAY_ID, endX, endY) + } + + companion object { + private const val TASK_ID = 5 + private const val MIN_WIDTH = 10 + private const val MIN_HEIGHT = 10 + private const val DENSITY_DPI = 20 + private const val DEFAULT_MIN = 40 + private const val DISPLAY_ID = 1 + private const val NAVBAR_HEIGHT = 50 + private const val CAPTION_HEIGHT = 50 + private const val DISALLOWED_AREA_FOR_END_BOUNDS_HEIGHT = 10 + private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) + private val STARTING_BOUNDS = Rect(100, 100, 200, 200) + private val STABLE_BOUNDS_LANDSCAPE = + Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.top + CAPTION_HEIGHT, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT, + ) + private val STABLE_BOUNDS_PORTRAIT = + Rect( + DISPLAY_BOUNDS.top, + DISPLAY_BOUNDS.left + CAPTION_HEIGHT, + DISPLAY_BOUNDS.bottom, + DISPLAY_BOUNDS.right - NAVBAR_HEIGHT, + ) + private val VALID_DRAG_AREA = + Rect( + DISPLAY_BOUNDS.left - 100, + STABLE_BOUNDS_LANDSCAPE.top, + DISPLAY_BOUNDS.right - 100, + DISPLAY_BOUNDS.bottom - 100, + ) + } +} |