Loading libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +59 −8 Original line number Diff line number Diff line Loading @@ -71,6 +71,7 @@ import android.view.InputEventReceiver; import android.view.InputMonitor; import android.view.InsetsState; import android.view.MotionEvent; import android.view.PointerIcon; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.view.View; Loading Loading @@ -1037,7 +1038,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopTilingDecorViewModel.onDeskRemoved(deskId); } private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener @VisibleForTesting public class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { private static final long APP_HANDLE_HOLD_TO_DRAG_DURATION_MS = 100; Loading @@ -1051,6 +1053,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final GestureDetector mGestureDetector; private final int mDisplayId; private final Rect mOnDragStartInitialBounds = new Rect(); private final Rect mCurrentBounds = new Rect(); /** * Whether to pilfer the next motion event to send cancellations to the windows below. Loading @@ -1067,6 +1070,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private boolean mLongClickDisabled; private int mDragPointerId = -1; private MotionEvent mMotionEvent; private int mCurrentPointerIconType = PointerIcon.TYPE_ARROW; private DesktopModeTouchEventListener( RunningTaskInfo taskInfo, Loading Loading @@ -1357,6 +1361,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, e.getRawY(0)); updateDragStatus(decoration, e); mOnDragStartInitialBounds.set(initialBounds); mCurrentBounds.set(initialBounds); } // Do not consume input event if a button is touched, otherwise it would // prevent the button's ripple effect from showing. Loading @@ -1372,17 +1377,36 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDragPointerId = e.getPointerId(0); } final int dragPointerIdx = e.findPointerIndex(mDragPointerId); final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningMove( if (DesktopExperienceFlags .ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX.isTrue()) { final boolean inDesktopModeDisplay = isDisplayInDesktopMode( e.getDisplayId()); // TODO: b/418651425 - Use a more specific pointer icon when available. updatePointerIcon(e, dragPointerIdx, v.getViewRootImpl().getInputToken(), inDesktopModeDisplay ? PointerIcon.TYPE_ARROW : PointerIcon.TYPE_NO_DROP); // Allow bounds update only when cursor is on desktop-mode displays. // Otherwise, ignore the MOVE event and the window holds its current bounds. if (inDesktopModeDisplay) { mCurrentBounds.set(mDragPositioningCallback.onDragPositioningMove( e.getDisplayId(), e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx))); } } else { mCurrentBounds.set(mDragPositioningCallback.onDragPositioningMove( e.getDisplayId(), e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx))); } mDesktopTasksController.onDragPositioningMove(taskInfo, decoration.mTaskSurface, decoration.getLeash(), e.getDisplayId(), e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx), newTaskBounds); mCurrentBounds); // Flip mIsDragging only if the bounds actually changed. if (mIsDragging || !newTaskBounds.equals(mOnDragStartInitialBounds)) { if (mIsDragging || !mCurrentBounds.equals(mOnDragStartInitialBounds)) { updateDragStatus(decoration, e); } return true; Loading @@ -1402,11 +1426,23 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd( e.getDisplayId(), e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); if (DesktopExperienceFlags .ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX.isTrue()) { updatePointerIcon(e, dragPointerIdx, v.getViewRootImpl().getInputToken(), PointerIcon.TYPE_ARROW); // If the cursor ends on a non-desktop-mode display, revert the window // to its initial bounds prior to the drag starting. if (!isDisplayInDesktopMode(e.getDisplayId())) { newTaskBounds.set(mOnDragStartInitialBounds); } } // Tasks bounds haven't actually been updated (only its leash), so pass to // DesktopTasksController to allow secondary transformations (i.e. snap resizing // or transforming to fullscreen) before setting new task bounds. mDesktopTasksController.onDragPositioningEnd( taskInfo, decoration.mTaskSurface, taskInfo, decoration.getLeash(), e.getDisplayId(), new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)), newTaskBounds, decoration.calculateValidDragArea(), Loading @@ -1425,6 +1461,21 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, return true; } private void updatePointerIcon(MotionEvent e, int dragPointerIdx, IBinder inputToken, int iconType) { if (mCurrentPointerIconType == iconType) { return; } mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, iconType), e.getDisplayId(), e.getDeviceId(), e.getPointerId(dragPointerIdx), inputToken); mCurrentPointerIconType = iconType; } private boolean isDisplayInDesktopMode(int displayId) { return mDesktopState.isDesktopModeSupportedOnDisplay(displayId) && mDesktopTasksController.getActiveDeskId(displayId) != null; } private void updateDragStatus(DesktopModeWindowDecoration decor, MotionEvent e) { switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +201 −5 Original line number Diff line number Diff line Loading @@ -29,12 +29,14 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.Intent.ACTION_MAIN import android.graphics.PointF import android.graphics.Rect import android.graphics.Region import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.hardware.input.InputManager import android.net.Uri import android.os.IBinder import android.os.SystemClock import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags Loading @@ -46,6 +48,7 @@ import android.view.InsetsSource import android.view.InsetsState import android.view.KeyEvent import android.view.MotionEvent import android.view.PointerIcon import android.view.Surface import android.view.SurfaceControl import android.view.SurfaceView Loading Loading @@ -74,14 +77,13 @@ import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import junit.framework.Assert.fail import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull Loading @@ -90,13 +92,15 @@ import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doNothing import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.isNotNull import org.mockito.kotlin.isNull import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.stub import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.quality.Strictness import org.mockito.kotlin.isNotNull import org.mockito.kotlin.isNull import org.mockito.kotlin.whenever import org.mockito.quality.Strictness /** * Tests of [DesktopModeWindowDecorViewModel] Loading Loading @@ -1320,6 +1324,194 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest verify(mockDecoration).close() } @Test @EnableFlags(Flags.FLAG_ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX) fun testOnFreeformWindowDragEnd_toDesktopModeDisplay_updateBounds() { val onTouchListenerCaptor = argumentCaptor<View.OnTouchListener>() val decor = createOpenTaskDecoration( windowingMode = WINDOWING_MODE_FREEFORM, onCaptionButtonTouchListener = onTouchListenerCaptor, ) val touchListener = onTouchListenerCaptor.firstValue if (touchListener is DesktopModeWindowDecorViewModel.DesktopModeTouchEventListener) { val taskInfo = decor.mTaskInfo mockDesktopTasksController.stub { on { getActiveDeskId(DEFAULT_DISPLAY) } doReturn 1 } mockDesktopTasksController.stub { on { getActiveDeskId(SECOND_DISPLAY) } doReturn 2 } val mockInputToken = mock<IBinder>() val mockViewRootImpl = mock<ViewRootImpl> { on { inputToken } doReturn mockInputToken } val view = mock<View> { on { getViewRootImpl() } doReturn mockViewRootImpl } mockTaskPositioner.stub { on { onDragPositioningStart(any(), any(), any(), any()) } doReturn INITIAL_BOUNDS on { onDragPositioningMove(any(), any(), any()) } doReturn BOUNDS_AFTER_FIRST_MOVE on { onDragPositioningEnd(any(), any(), any()) } doReturn BOUNDS_ON_DRAG_END_DESKTOP_ACCEPTED } touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0).apply { displayId = DEFAULT_DISPLAY }, ) // ACTION_MOVE on desktop-mode display touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 1L, MotionEvent.ACTION_MOVE, 10f, 10f, 0).apply { displayId = SECOND_DISPLAY }, ) // Verify point icon does not change and bounds changes verify(mockInputManager, never()).setPointerIcon(any(), any(), any(), any(), any()) verify(mockDesktopTasksController) .onDragPositioningMove( eq(taskInfo), any<SurfaceControl>(), eq(SECOND_DISPLAY), eq(10f), eq(10f), eq(BOUNDS_AFTER_FIRST_MOVE), ) // ACTION_UP on desktop-mode display touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 2L, MotionEvent.ACTION_UP, 20f, 20f, 0).apply { displayId = SECOND_DISPLAY }, ) // Verify point icon does not change and bounds changes verify(mockInputManager, never()).setPointerIcon(any(), any(), any(), any(), any()) verify(mockDesktopTasksController) .onDragPositioningEnd( eq(taskInfo), any<SurfaceControl>(), eq(SECOND_DISPLAY), eq(PointF(20f, 20f)), eq(BOUNDS_ON_DRAG_END_DESKTOP_ACCEPTED), any<Rect>(), any<Rect>(), any<MotionEvent>(), ) } else { fail("touchListener was not a DesktopModeTouchEventListener as expected.") } } @Test @EnableFlags(Flags.FLAG_ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX) fun testOnFreeformWindowDragMove_toNonDesktopModeDisplay_setsNoDropIconAndKeepsBounds() { val onTouchListenerCaptor = argumentCaptor<View.OnTouchListener>() val decor = createOpenTaskDecoration( windowingMode = WINDOWING_MODE_FREEFORM, onCaptionButtonTouchListener = onTouchListenerCaptor, ) val touchListener = onTouchListenerCaptor.firstValue if (touchListener is DesktopModeWindowDecorViewModel.DesktopModeTouchEventListener) { val taskInfo = decor.mTaskInfo mockDesktopTasksController.stub { on { getActiveDeskId(DEFAULT_DISPLAY) } doReturn 1 } mockDesktopTasksController.stub { on { getActiveDeskId(SECOND_DISPLAY) } doReturn null } val mockInputToken = mock<IBinder>() val mockViewRootImpl = mock<ViewRootImpl> { on { inputToken } doReturn mockInputToken } val view = mock<View> { on { getViewRootImpl() } doReturn mockViewRootImpl } mockTaskPositioner.stub { on { onDragPositioningStart(any(), any(), any(), any()) } doReturn INITIAL_BOUNDS on { onDragPositioningMove(any(), any(), any()) } doReturn BOUNDS_AFTER_FIRST_MOVE on { onDragPositioningEnd(any(), any(), any()) } doReturn BOUNDS_IGNORED_ON_NON_DESKTOP } touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0).apply { displayId = DEFAULT_DISPLAY }, ) // ACTION_MOVE on desktop-mode display touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 1L, MotionEvent.ACTION_MOVE, 10f, 10f, 0).apply { displayId = DEFAULT_DISPLAY }, ) // Verify point icon does not change and bounds changes verify(mockInputManager, never()).setPointerIcon(any(), any(), any(), any(), any()) verify(mockDesktopTasksController) .onDragPositioningMove( eq(taskInfo), any(), eq(DEFAULT_DISPLAY), eq(10f), eq(10f), eq(BOUNDS_AFTER_FIRST_MOVE), ) // ACTION_MOVE to non-desktop-mode display touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 2L, MotionEvent.ACTION_MOVE, 20f, 20f, 0).apply { displayId = SECOND_DISPLAY }, ) // Verify point icon changes and bounds stays the same verify(mockInputManager) .setPointerIcon( argThat { icon -> icon.type == PointerIcon.TYPE_NO_DROP }, eq(SECOND_DISPLAY), any(), eq(0), eq(mockInputToken), ) verify(mockDesktopTasksController) .onDragPositioningMove( eq(taskInfo), any(), eq(SECOND_DISPLAY), eq(20f), eq(20f), eq(BOUNDS_AFTER_FIRST_MOVE), ) // ACTION_UP on non-desktop-mode display touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 2L, MotionEvent.ACTION_UP, 30f, 30f, 0).apply { displayId = SECOND_DISPLAY }, ) // Verify point icon changes and bounds resets to initial bounds verify(mockInputManager) .setPointerIcon( argThat { icon -> icon.type == PointerIcon.TYPE_ARROW }, eq(SECOND_DISPLAY), any(), eq(0), eq(mockInputToken), ) verify(mockDesktopTasksController) .onDragPositioningEnd( eq(taskInfo), any<SurfaceControl>(), eq(SECOND_DISPLAY), eq(PointF(30f, 30f)), eq(INITIAL_BOUNDS), any<Rect>(), any<Rect>(), any<MotionEvent>(), ) } else { fail("touchListener was not a DesktopModeTouchEventListener as expected.") } } private fun createOpenTaskDecoration( @WindowingMode windowingMode: Int, taskSurface: SurfaceControl = SurfaceControl(), Loading @@ -1338,6 +1530,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest windowDecorationActions ) onTaskOpening(decor.mTaskInfo, taskSurface) decor.stub { on { leash } doReturn taskSurface } verify(decor).setCaptionListeners( onCaptionButtonClickListener.capture(), onCaptionButtonTouchListener.capture(), any(), any()) Loading @@ -1363,5 +1556,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest private companion object { const val SECOND_DISPLAY = 2 private val BOUNDS_AFTER_FIRST_MOVE = Rect(10, 10, 110, 110) private val BOUNDS_IGNORED_ON_NON_DESKTOP = Rect(20, 20, 120, 120) private val BOUNDS_ON_DRAG_END_DESKTOP_ACCEPTED = Rect(50, 50, 150, 150) } } Loading
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +59 −8 Original line number Diff line number Diff line Loading @@ -71,6 +71,7 @@ import android.view.InputEventReceiver; import android.view.InputMonitor; import android.view.InsetsState; import android.view.MotionEvent; import android.view.PointerIcon; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.view.View; Loading Loading @@ -1037,7 +1038,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopTilingDecorViewModel.onDeskRemoved(deskId); } private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener @VisibleForTesting public class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { private static final long APP_HANDLE_HOLD_TO_DRAG_DURATION_MS = 100; Loading @@ -1051,6 +1053,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final GestureDetector mGestureDetector; private final int mDisplayId; private final Rect mOnDragStartInitialBounds = new Rect(); private final Rect mCurrentBounds = new Rect(); /** * Whether to pilfer the next motion event to send cancellations to the windows below. Loading @@ -1067,6 +1070,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private boolean mLongClickDisabled; private int mDragPointerId = -1; private MotionEvent mMotionEvent; private int mCurrentPointerIconType = PointerIcon.TYPE_ARROW; private DesktopModeTouchEventListener( RunningTaskInfo taskInfo, Loading Loading @@ -1357,6 +1361,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, e.getRawY(0)); updateDragStatus(decoration, e); mOnDragStartInitialBounds.set(initialBounds); mCurrentBounds.set(initialBounds); } // Do not consume input event if a button is touched, otherwise it would // prevent the button's ripple effect from showing. Loading @@ -1372,17 +1377,36 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDragPointerId = e.getPointerId(0); } final int dragPointerIdx = e.findPointerIndex(mDragPointerId); final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningMove( if (DesktopExperienceFlags .ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX.isTrue()) { final boolean inDesktopModeDisplay = isDisplayInDesktopMode( e.getDisplayId()); // TODO: b/418651425 - Use a more specific pointer icon when available. updatePointerIcon(e, dragPointerIdx, v.getViewRootImpl().getInputToken(), inDesktopModeDisplay ? PointerIcon.TYPE_ARROW : PointerIcon.TYPE_NO_DROP); // Allow bounds update only when cursor is on desktop-mode displays. // Otherwise, ignore the MOVE event and the window holds its current bounds. if (inDesktopModeDisplay) { mCurrentBounds.set(mDragPositioningCallback.onDragPositioningMove( e.getDisplayId(), e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx))); } } else { mCurrentBounds.set(mDragPositioningCallback.onDragPositioningMove( e.getDisplayId(), e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx))); } mDesktopTasksController.onDragPositioningMove(taskInfo, decoration.mTaskSurface, decoration.getLeash(), e.getDisplayId(), e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx), newTaskBounds); mCurrentBounds); // Flip mIsDragging only if the bounds actually changed. if (mIsDragging || !newTaskBounds.equals(mOnDragStartInitialBounds)) { if (mIsDragging || !mCurrentBounds.equals(mOnDragStartInitialBounds)) { updateDragStatus(decoration, e); } return true; Loading @@ -1402,11 +1426,23 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd( e.getDisplayId(), e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)); if (DesktopExperienceFlags .ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX.isTrue()) { updatePointerIcon(e, dragPointerIdx, v.getViewRootImpl().getInputToken(), PointerIcon.TYPE_ARROW); // If the cursor ends on a non-desktop-mode display, revert the window // to its initial bounds prior to the drag starting. if (!isDisplayInDesktopMode(e.getDisplayId())) { newTaskBounds.set(mOnDragStartInitialBounds); } } // Tasks bounds haven't actually been updated (only its leash), so pass to // DesktopTasksController to allow secondary transformations (i.e. snap resizing // or transforming to fullscreen) before setting new task bounds. mDesktopTasksController.onDragPositioningEnd( taskInfo, decoration.mTaskSurface, taskInfo, decoration.getLeash(), e.getDisplayId(), new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)), newTaskBounds, decoration.calculateValidDragArea(), Loading @@ -1425,6 +1461,21 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, return true; } private void updatePointerIcon(MotionEvent e, int dragPointerIdx, IBinder inputToken, int iconType) { if (mCurrentPointerIconType == iconType) { return; } mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, iconType), e.getDisplayId(), e.getDeviceId(), e.getPointerId(dragPointerIdx), inputToken); mCurrentPointerIconType = iconType; } private boolean isDisplayInDesktopMode(int displayId) { return mDesktopState.isDesktopModeSupportedOnDisplay(displayId) && mDesktopTasksController.getActiveDeskId(displayId) != null; } private void updateDragStatus(DesktopModeWindowDecoration decor, MotionEvent e) { switch (e.getActionMasked()) { case MotionEvent.ACTION_DOWN: Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +201 −5 Original line number Diff line number Diff line Loading @@ -29,12 +29,14 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.Intent.ACTION_MAIN import android.graphics.PointF import android.graphics.Rect import android.graphics.Region import android.hardware.display.DisplayManager import android.hardware.display.VirtualDisplay import android.hardware.input.InputManager import android.net.Uri import android.os.IBinder import android.os.SystemClock import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags Loading @@ -46,6 +48,7 @@ import android.view.InsetsSource import android.view.InsetsState import android.view.KeyEvent import android.view.MotionEvent import android.view.PointerIcon import android.view.Surface import android.view.SurfaceControl import android.view.SurfaceView Loading Loading @@ -74,14 +77,13 @@ import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import junit.framework.Assert.fail import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.Assume.assumeTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.kotlin.KArgumentCaptor import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull Loading @@ -90,13 +92,15 @@ import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doNothing import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.isNotNull import org.mockito.kotlin.isNull import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.stub import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.quality.Strictness import org.mockito.kotlin.isNotNull import org.mockito.kotlin.isNull import org.mockito.kotlin.whenever import org.mockito.quality.Strictness /** * Tests of [DesktopModeWindowDecorViewModel] Loading Loading @@ -1320,6 +1324,194 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest verify(mockDecoration).close() } @Test @EnableFlags(Flags.FLAG_ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX) fun testOnFreeformWindowDragEnd_toDesktopModeDisplay_updateBounds() { val onTouchListenerCaptor = argumentCaptor<View.OnTouchListener>() val decor = createOpenTaskDecoration( windowingMode = WINDOWING_MODE_FREEFORM, onCaptionButtonTouchListener = onTouchListenerCaptor, ) val touchListener = onTouchListenerCaptor.firstValue if (touchListener is DesktopModeWindowDecorViewModel.DesktopModeTouchEventListener) { val taskInfo = decor.mTaskInfo mockDesktopTasksController.stub { on { getActiveDeskId(DEFAULT_DISPLAY) } doReturn 1 } mockDesktopTasksController.stub { on { getActiveDeskId(SECOND_DISPLAY) } doReturn 2 } val mockInputToken = mock<IBinder>() val mockViewRootImpl = mock<ViewRootImpl> { on { inputToken } doReturn mockInputToken } val view = mock<View> { on { getViewRootImpl() } doReturn mockViewRootImpl } mockTaskPositioner.stub { on { onDragPositioningStart(any(), any(), any(), any()) } doReturn INITIAL_BOUNDS on { onDragPositioningMove(any(), any(), any()) } doReturn BOUNDS_AFTER_FIRST_MOVE on { onDragPositioningEnd(any(), any(), any()) } doReturn BOUNDS_ON_DRAG_END_DESKTOP_ACCEPTED } touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0).apply { displayId = DEFAULT_DISPLAY }, ) // ACTION_MOVE on desktop-mode display touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 1L, MotionEvent.ACTION_MOVE, 10f, 10f, 0).apply { displayId = SECOND_DISPLAY }, ) // Verify point icon does not change and bounds changes verify(mockInputManager, never()).setPointerIcon(any(), any(), any(), any(), any()) verify(mockDesktopTasksController) .onDragPositioningMove( eq(taskInfo), any<SurfaceControl>(), eq(SECOND_DISPLAY), eq(10f), eq(10f), eq(BOUNDS_AFTER_FIRST_MOVE), ) // ACTION_UP on desktop-mode display touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 2L, MotionEvent.ACTION_UP, 20f, 20f, 0).apply { displayId = SECOND_DISPLAY }, ) // Verify point icon does not change and bounds changes verify(mockInputManager, never()).setPointerIcon(any(), any(), any(), any(), any()) verify(mockDesktopTasksController) .onDragPositioningEnd( eq(taskInfo), any<SurfaceControl>(), eq(SECOND_DISPLAY), eq(PointF(20f, 20f)), eq(BOUNDS_ON_DRAG_END_DESKTOP_ACCEPTED), any<Rect>(), any<Rect>(), any<MotionEvent>(), ) } else { fail("touchListener was not a DesktopModeTouchEventListener as expected.") } } @Test @EnableFlags(Flags.FLAG_ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX) fun testOnFreeformWindowDragMove_toNonDesktopModeDisplay_setsNoDropIconAndKeepsBounds() { val onTouchListenerCaptor = argumentCaptor<View.OnTouchListener>() val decor = createOpenTaskDecoration( windowingMode = WINDOWING_MODE_FREEFORM, onCaptionButtonTouchListener = onTouchListenerCaptor, ) val touchListener = onTouchListenerCaptor.firstValue if (touchListener is DesktopModeWindowDecorViewModel.DesktopModeTouchEventListener) { val taskInfo = decor.mTaskInfo mockDesktopTasksController.stub { on { getActiveDeskId(DEFAULT_DISPLAY) } doReturn 1 } mockDesktopTasksController.stub { on { getActiveDeskId(SECOND_DISPLAY) } doReturn null } val mockInputToken = mock<IBinder>() val mockViewRootImpl = mock<ViewRootImpl> { on { inputToken } doReturn mockInputToken } val view = mock<View> { on { getViewRootImpl() } doReturn mockViewRootImpl } mockTaskPositioner.stub { on { onDragPositioningStart(any(), any(), any(), any()) } doReturn INITIAL_BOUNDS on { onDragPositioningMove(any(), any(), any()) } doReturn BOUNDS_AFTER_FIRST_MOVE on { onDragPositioningEnd(any(), any(), any()) } doReturn BOUNDS_IGNORED_ON_NON_DESKTOP } touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0).apply { displayId = DEFAULT_DISPLAY }, ) // ACTION_MOVE on desktop-mode display touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 1L, MotionEvent.ACTION_MOVE, 10f, 10f, 0).apply { displayId = DEFAULT_DISPLAY }, ) // Verify point icon does not change and bounds changes verify(mockInputManager, never()).setPointerIcon(any(), any(), any(), any(), any()) verify(mockDesktopTasksController) .onDragPositioningMove( eq(taskInfo), any(), eq(DEFAULT_DISPLAY), eq(10f), eq(10f), eq(BOUNDS_AFTER_FIRST_MOVE), ) // ACTION_MOVE to non-desktop-mode display touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 2L, MotionEvent.ACTION_MOVE, 20f, 20f, 0).apply { displayId = SECOND_DISPLAY }, ) // Verify point icon changes and bounds stays the same verify(mockInputManager) .setPointerIcon( argThat { icon -> icon.type == PointerIcon.TYPE_NO_DROP }, eq(SECOND_DISPLAY), any(), eq(0), eq(mockInputToken), ) verify(mockDesktopTasksController) .onDragPositioningMove( eq(taskInfo), any(), eq(SECOND_DISPLAY), eq(20f), eq(20f), eq(BOUNDS_AFTER_FIRST_MOVE), ) // ACTION_UP on non-desktop-mode display touchListener.handleMotionEvent( view, MotionEvent.obtain(0L, 2L, MotionEvent.ACTION_UP, 30f, 30f, 0).apply { displayId = SECOND_DISPLAY }, ) // Verify point icon changes and bounds resets to initial bounds verify(mockInputManager) .setPointerIcon( argThat { icon -> icon.type == PointerIcon.TYPE_ARROW }, eq(SECOND_DISPLAY), any(), eq(0), eq(mockInputToken), ) verify(mockDesktopTasksController) .onDragPositioningEnd( eq(taskInfo), any<SurfaceControl>(), eq(SECOND_DISPLAY), eq(PointF(30f, 30f)), eq(INITIAL_BOUNDS), any<Rect>(), any<Rect>(), any<MotionEvent>(), ) } else { fail("touchListener was not a DesktopModeTouchEventListener as expected.") } } private fun createOpenTaskDecoration( @WindowingMode windowingMode: Int, taskSurface: SurfaceControl = SurfaceControl(), Loading @@ -1338,6 +1530,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest windowDecorationActions ) onTaskOpening(decor.mTaskInfo, taskSurface) decor.stub { on { leash } doReturn taskSurface } verify(decor).setCaptionListeners( onCaptionButtonClickListener.capture(), onCaptionButtonTouchListener.capture(), any(), any()) Loading @@ -1363,5 +1556,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest private companion object { const val SECOND_DISPLAY = 2 private val BOUNDS_AFTER_FIRST_MOVE = Rect(10, 10, 110, 110) private val BOUNDS_IGNORED_ON_NON_DESKTOP = Rect(20, 20, 120, 120) private val BOUNDS_ON_DRAG_END_DESKTOP_ACCEPTED = Rect(50, 50, 150, 150) } }