diff options
author | 2024-09-15 05:56:37 +0000 | |
---|---|---|
committer | 2024-09-18 22:35:00 +0000 | |
commit | 66112fa6c9d41d54c98cb61dc93fe20127e4c088 (patch) | |
tree | 6de781272dde72b23ed713351df9c5ddf51f7542 | |
parent | 6f62c8c15a197e84f855cfff7710879e048d29b1 (diff) |
Require hold-to-drag for App Handle drags
Adds a holding period functionality to DragDetector, which requires a
hold within the slop region to be maintained for X amount of ms before
ACTION_MOVEs outside the slop are allowed (reported to the event
handler). This functionality is enabled for the App Handle's drag
detector behind a flag, and disable for every other drag detector
(header, resize listener).
Also modifies e2e test to check the type of input before entering
desktop with drag, and simulates a hold-to-drag when the input is from a
touchscreen.
Flag: com.android.window.flags.enable_hold_to_drag_app_handle
Bug: 356409496
Test: atest WMShellUnitTests; atest PlatformScenarioTests
Change-Id: Ib57be0ce8b63aaa17ecc57b70d1629ab88c69787
9 files changed, 340 insertions, 52 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 015139519f1f..431461a27e6f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -44,6 +44,7 @@ import android.view.IWindowManager; import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.View; +import android.view.ViewConfiguration; import android.window.DisplayAreaInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -310,7 +311,6 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { new CaptionTouchEventListener(taskInfo, taskPositioner); windowDecoration.setCaptionListeners(touchEventListener, touchEventListener); windowDecoration.setDragPositioningCallback(taskPositioner); - windowDecoration.setDragDetector(touchEventListener.mDragDetector); windowDecoration.setTaskDragResizer(taskPositioner); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* setTaskCropAndPosition */); @@ -334,7 +334,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel { mTaskId = taskInfo.taskId; mTaskToken = taskInfo.token; mDragPositioningCallback = dragPositioningCallback; - mDragDetector = new DragDetector(this); + mDragDetector = new DragDetector(this, 0 /* holdToDragMinDurationMs */, + ViewConfiguration.get(mContext).getScaledTouchSlop()); mDisplayId = taskInfo.displayId; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 0caa8e93d4eb..1539b5fb34e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -74,7 +74,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL private View.OnTouchListener mOnCaptionTouchListener; private DragPositioningCallback mDragPositioningCallback; private DragResizeInputListener mDragResizeListener; - private DragDetector mDragDetector; private RelayoutParams mRelayoutParams = new RelayoutParams(); private final RelayoutResult<WindowDecorLinearLayout> mResult = @@ -176,12 +175,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL return stableBounds.bottom - requiredEmptySpace; } - - void setDragDetector(DragDetector dragDetector) { - mDragDetector = dragDetector; - mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop()); - } - @Override void relayout(RunningTaskInfo taskInfo) { final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); @@ -288,7 +281,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) .getScaledTouchSlop(); - mDragDetector.setTouchSlop(touchSlop); final Resources res = mResult.mRootView.getResources(); mDragResizeListener.setGeometry(new DragResizeWindowGeometry(0 /* taskCornerRadius */, 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 b311359ae624..3d41870ca159 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 @@ -79,6 +79,7 @@ import android.view.MotionEvent; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.view.View; +import android.view.ViewConfiguration; import android.widget.Toast; import android.window.TaskSnapshot; import android.window.WindowContainerToken; @@ -651,11 +652,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { private 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; + private static final long APP_HEADER_HOLD_TO_DRAG_DURATION_MS = 0; private final int mTaskId; private final WindowContainerToken mTaskToken; private final DragPositioningCallback mDragPositioningCallback; - private final DragDetector mDragDetector; + private final DragDetector mHandleDragDetector; + private final DragDetector mHeaderDragDetector; private final GestureDetector mGestureDetector; private final int mDisplayId; private final Rect mOnDragStartInitialBounds = new Rect(); @@ -677,7 +681,13 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mTaskId = taskInfo.taskId; mTaskToken = taskInfo.token; mDragPositioningCallback = dragPositioningCallback; - mDragDetector = new DragDetector(this); + final int touchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); + final long appHandleHoldToDragDuration = Flags.enableHoldToDragAppHandle() + ? APP_HANDLE_HOLD_TO_DRAG_DURATION_MS : 0; + mHandleDragDetector = new DragDetector(this, appHandleHoldToDragDuration, + touchSlop); + mHeaderDragDetector = new DragDetector(this, APP_HEADER_HOLD_TO_DRAG_DURATION_MS, + touchSlop); mGestureDetector = new GestureDetector(mContext, this); mDisplayId = taskInfo.displayId; } @@ -736,7 +746,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && id != R.id.maximize_window && id != R.id.minimize_window) { return false; } - + final boolean isAppHandle = !getTaskInfo().isFreeform(); final int actionMasked = e.getActionMasked(); final boolean isDown = actionMasked == MotionEvent.ACTION_DOWN; final boolean isUpOrCancel = actionMasked == MotionEvent.ACTION_CANCEL @@ -785,7 +795,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { // Gesture is finished, reset state. mShouldPilferCaptionEvents = false; } - return mDragDetector.onMotionEvent(v, e); + if (isAppHandle) { + return mHandleDragDetector.onMotionEvent(v, e); + } else { + return mHeaderDragDetector.onMotionEvent(v, e); + } } @Override @@ -853,6 +867,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } } + @NonNull + private RunningTaskInfo getTaskInfo() { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + return decoration.mTaskInfo; + } + private boolean handleNonFreeformMotionEvent(DesktopModeWindowDecoration decoration, View v, MotionEvent e) { final int id = v.getId(); @@ -1421,7 +1441,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { touchEventListener, touchEventListener, touchEventListener, touchEventListener); windowDecoration.setExclusionRegionListener(mExclusionRegionListener); windowDecoration.setDragPositioningCallback(taskPositioner); - windowDecoration.setDragDetector(touchEventListener.mDragDetector); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */); if (!Flags.enableAdditionalWindowsAboveStatusBar()) { 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 5521c2ee6578..fe2b6717429e 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 @@ -139,7 +139,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private Function0<Unit> mOnManageWindowsClickListener; private DragPositioningCallback mDragPositioningCallback; private DragResizeInputListener mDragResizeListener; - private DragDetector mDragDetector; private RelayoutParams mRelayoutParams = new RelayoutParams(); private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult = new WindowDecoration.RelayoutResult<>(); @@ -320,11 +319,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mDragPositioningCallback = dragPositioningCallback; } - void setDragDetector(DragDetector dragDetector) { - mDragDetector = dragDetector; - mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop()); - } - void setOpenInBrowserClickListener(Consumer<Uri> listener) { mOpenInBrowserClickListener = listener; } @@ -482,7 +476,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) .getScaledTouchSlop(); - mDragDetector.setTouchSlop(touchSlop); // If either task geometry or position have changed, update this task's // exclusion region listener diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java index 3fd3656ccbc5..01bb7f74ba06 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java @@ -26,6 +26,7 @@ import static android.view.MotionEvent.ACTION_MOVE; import static android.view.MotionEvent.ACTION_POINTER_UP; import static android.view.MotionEvent.ACTION_UP; +import android.annotation.NonNull; import android.graphics.PointF; import android.view.MotionEvent; import android.view.View; @@ -48,12 +49,18 @@ class DragDetector { private int mTouchSlop; private boolean mIsDragEvent; private int mDragPointerId = -1; + private final long mHoldToDragMinDurationMs; + private boolean mDidStrayBeforeFullHold; + private boolean mDidHoldForMinDuration; private boolean mResultOfDownAction; - DragDetector(MotionEventHandler eventHandler) { + DragDetector(@NonNull MotionEventHandler eventHandler, long holdToDragMinDurationMs, + int touchSlop) { resetState(); mEventHandler = eventHandler; + mHoldToDragMinDurationMs = holdToDragMinDurationMs; + mTouchSlop = touchSlop; } /** @@ -101,9 +108,26 @@ class DragDetector { if (!mIsDragEvent) { float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x; float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y; + final float dt = ev.getEventTime() - ev.getDownTime(); + final boolean pastTouchSlop = Math.hypot(dx, dy) > mTouchSlop; + final boolean withinHoldRegion = !pastTouchSlop; + + if (mHoldToDragMinDurationMs <= 0) { + mDidHoldForMinDuration = true; + } else { + if (!withinHoldRegion && dt < mHoldToDragMinDurationMs) { + // Mark as having strayed so that in case the (x,y) ends up in the + // original position we know it's not actually valid. + mDidStrayBeforeFullHold = true; + } + if (!mDidStrayBeforeFullHold && dt >= mHoldToDragMinDurationMs) { + mDidHoldForMinDuration = true; + } + } + // Touches generate noisy moves, so only once the move is past the touch // slop threshold should it be considered a drag. - mIsDragEvent = Math.hypot(dx, dy) > mTouchSlop; + mIsDragEvent = mDidHoldForMinDuration && pastTouchSlop; } // The event handler should only be notified about 'move' events if a drag has been // detected. @@ -162,6 +186,8 @@ class DragDetector { mInputDownPoint.set(0, 0); mDragPointerId = -1; mResultOfDownAction = false; + mDidStrayBeforeFullHold = false; + mDidHoldForMinDuration = false; } interface MotionEventHandler { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index a27c506e3e60..b5ed32bbd4e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -318,7 +318,8 @@ class DragResizeInputListener implements AutoCloseable { } }; - mDragDetector = new DragDetector(this); + mDragDetector = new DragDetector(this, 0 /* holdToDragMinDurationMs */, + ViewConfiguration.get(mContext).getScaledTouchSlop()); mDisplayLayoutSizeSupplier = displayLayoutSizeSupplier; mTouchRegionConsumer = touchRegionConsumer; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt index 56224b4b4025..7f7211d65fde 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt @@ -21,6 +21,7 @@ import android.testing.AndroidTestingRunner import android.view.MotionEvent import android.view.InputDevice import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -34,6 +35,7 @@ import org.mockito.Mockito.any import org.mockito.Mockito.argThat import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.kotlin.times /** * Tests for [DragDetector]. @@ -43,22 +45,17 @@ import org.mockito.Mockito.verify */ @SmallTest @RunWith(AndroidTestingRunner::class) -class DragDetectorTest { +class DragDetectorTest : ShellTestCase() { private val motionEvents = mutableListOf<MotionEvent>() @Mock private lateinit var eventHandler: DragDetector.MotionEventHandler - private lateinit var dragDetector: DragDetector - @Before fun setUp() { MockitoAnnotations.initMocks(this) `when`(eventHandler.handleMotionEvent(any(), any())).thenReturn(true) - - dragDetector = DragDetector(eventHandler) - dragDetector.setTouchSlop(SLOP) } @After @@ -71,6 +68,7 @@ class DragDetectorTest { @Test fun testNoMove_passesDownAndUp() { + val dragDetector = createDragDetector() assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN))) verify(eventHandler).handleMotionEvent(any(), argThat { return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && @@ -86,6 +84,7 @@ class DragDetectorTest { @Test fun testNoMove_mouse_passesDownAndUp() { + val dragDetector = createDragDetector() assertTrue(dragDetector.onMotionEvent( createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false))) verify(eventHandler).handleMotionEvent(any(), argThat { @@ -103,6 +102,7 @@ class DragDetectorTest { @Test fun testMoveInSlop_touch_passesDownAndUp() { + val dragDetector = createDragDetector() `when`(eventHandler.handleMotionEvent(any(), argThat { return@argThat it.action == MotionEvent.ACTION_DOWN })).thenReturn(false) @@ -129,6 +129,7 @@ class DragDetectorTest { @Test fun testMoveInSlop_mouse_passesDownMoveAndUp() { + val dragDetector = createDragDetector() `when`(eventHandler.handleMotionEvent(any(), argThat { it.action == MotionEvent.ACTION_DOWN })).thenReturn(false) @@ -158,6 +159,7 @@ class DragDetectorTest { @Test fun testMoveBeyondSlop_passesDownMoveAndUp() { + val dragDetector = createDragDetector() `when`(eventHandler.handleMotionEvent(any(), argThat { it.action == MotionEvent.ACTION_DOWN })).thenReturn(false) @@ -184,6 +186,7 @@ class DragDetectorTest { @Test fun testDownMoveDown_shouldIgnoreTheSecondDownMotion() { + val dragDetector = createDragDetector() assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN))) verify(eventHandler).handleMotionEvent(any(), argThat { return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y && @@ -206,6 +209,7 @@ class DragDetectorTest { @Test fun testDownMouseMoveDownTouch_shouldIgnoreTheTouchDownMotion() { + val dragDetector = createDragDetector() assertTrue(dragDetector.onMotionEvent( createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false))) verify(eventHandler).handleMotionEvent(any(), argThat { @@ -230,6 +234,7 @@ class DragDetectorTest { @Test fun testPassesHoverEnter() { + val dragDetector = createDragDetector() `when`(eventHandler.handleMotionEvent(any(), argThat { it.action == MotionEvent.ACTION_HOVER_ENTER })).thenReturn(false) @@ -242,6 +247,7 @@ class DragDetectorTest { @Test fun testPassesHoverMove() { + val dragDetector = createDragDetector() assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_MOVE))) verify(eventHandler).handleMotionEvent(any(), argThat { return@argThat it.action == MotionEvent.ACTION_HOVER_MOVE && it.x == X && it.y == Y @@ -250,21 +256,240 @@ class DragDetectorTest { @Test fun testPassesHoverExit() { + val dragDetector = createDragDetector() assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_EXIT))) verify(eventHandler).handleMotionEvent(any(), argThat { return@argThat it.action == MotionEvent.ACTION_HOVER_EXIT && it.x == X && it.y == Y }) } - private fun createMotionEvent(action: Int, x: Float = X, y: Float = Y, isTouch: Boolean = true): - MotionEvent { - val time = SystemClock.uptimeMillis() - val ev = MotionEvent.obtain(time, time, action, x, y, 0) + @Test + fun testHoldToDrag_holdsWithMovementWithinSlop_passesDragMoveEvents() { + val dragDetector = createDragDetector(holdToDragMinDurationMs = 100, slop = 20) + val downTime = SystemClock.uptimeMillis() + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_DOWN, + x = 500f, + y = 10f, + isTouch = true, + downTime = downTime, + eventTime = downTime + )) + + // Couple of movements within the slop, still counting as "holding" + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 10f, // within slop + y = 10f + 10f, // within slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 30 + )) + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f - 10f, // within slop + y = 10f - 5f, // within slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 70 + )) + // Now go beyond slop, but after the required holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 50f, // beyond slop + y = 10f + 50f, // beyond slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 101 // after hold period + )) + + // Had a valid hold, so there should be 1 "move". + verify(eventHandler, times(1)) + .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE }) + } + + @Test + fun testHoldToDrag_holdsWithoutAnyMovement_passesMoveEvents() { + val dragDetector = createDragDetector(holdToDragMinDurationMs = 100, slop = 20) + val downTime = SystemClock.uptimeMillis() + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_DOWN, + x = 500f, + y = 10f, + isTouch = true, + downTime = downTime, + eventTime = downTime + )) + + // First |move| is already beyond slop and after holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 50f, // beyond slop + y = 10f + 50f, // beyond slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 101 // after hold period + )) + + // Considered a valid hold, so there should be 1 "move". + verify(eventHandler, times(1)) + .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE }) + } + + @Test + fun testHoldToDrag_returnsWithinSlopAfterHoldPeriod_passesDragMoveEvents() { + val dragDetector = createDragDetector(holdToDragMinDurationMs = 100, slop = 20) + val downTime = SystemClock.uptimeMillis() + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_DOWN, + x = 500f, + y = 10f, + isTouch = true, + downTime = downTime, + eventTime = downTime + )) + // Go beyond slop after the required holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 50f, // beyond slop + y = 10f + 50f, // beyond slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 101 // after hold period + )) + + // Return to original coordinates after holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f, // within slop + y = 10f, // within slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 102 // after hold period + )) + + // Both |moves| should be passed, even the one in the slop region since it was after the + // holding period. (e.g. after you drag the handle you may return to its original position). + verify(eventHandler, times(2)) + .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE }) + } + + @Test + fun testHoldToDrag_straysDuringHoldPeriod_skipsMoveEvents() { + val dragDetector = createDragDetector(holdToDragMinDurationMs = 100, slop = 20) + val downTime = SystemClock.uptimeMillis() + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_DOWN, + x = 500f, + y = 10f, + isTouch = true, + downTime = downTime, + eventTime = downTime + )) + + // Go beyond slop before the required holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 50f, // beyond slop + y = 10f + 50f, // beyond slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 30 // during hold period + )) + + // The |move| was too quick and did not held, do not pass it to the handler. + verify(eventHandler, never()) + .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE }) + } + + @Test + fun testHoldToDrag_straysDuringHoldPeriodAndReturnsWithinSlop_skipsMoveEvents() { + val dragDetector = createDragDetector(holdToDragMinDurationMs = 100, slop = 20) + val downTime = SystemClock.uptimeMillis() + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_DOWN, + x = 500f, + y = 10f, + isTouch = true, + downTime = downTime, + eventTime = downTime + )) + // Go beyond slop before the required holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 50f, // beyond slop + y = 10f + 50f, // beyond slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 30 // during hold period + )) + + // Return to slop area during holding period. + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 10f, // within slop + y = 10f + 10f, // within slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 50 // during hold period + )) + + // The first |move| invalidates the drag even if you return within the hold period, so the + // |move| should not be passed to the handler. + verify(eventHandler, never()) + .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE }) + } + + @Test + fun testHoldToDrag_noHoldRequired_passesMoveEvents() { + val dragDetector = createDragDetector(holdToDragMinDurationMs = 0, slop = 20) + val downTime = SystemClock.uptimeMillis() + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_DOWN, + x = 500f, + y = 10f, + isTouch = true, + downTime = downTime, + eventTime = downTime + )) + + dragDetector.onMotionEvent(createMotionEvent( + action = MotionEvent.ACTION_MOVE, + x = 500f + 50f, // beyond slop + y = 10f + 50f, // beyond slop + isTouch = true, + downTime = downTime, + eventTime = downTime + 1 + )) + + // The |move| should be passed to the handler as no hold period was needed. + verify(eventHandler, times(1)) + .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE }) + } + + private fun createMotionEvent( + action: Int, + x: Float = X, + y: Float = Y, + isTouch: Boolean = true, + downTime: Long = SystemClock.uptimeMillis(), + eventTime: Long = SystemClock.uptimeMillis() + ): MotionEvent { + val ev = MotionEvent.obtain(downTime, eventTime, action, x, y, 0) ev.source = if (isTouch) InputDevice.SOURCE_TOUCHSCREEN else InputDevice.SOURCE_MOUSE motionEvents.add(ev) return ev } + private fun createDragDetector( + holdToDragMinDurationMs: Long = 0, + slop: Int = SLOP + ) = DragDetector( + eventHandler, + holdToDragMinDurationMs, + slop + ) + companion object { private const val SLOP = 10 private const val X = 123f diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 3f6a0bf49eb4..c77413b6a55a 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -20,6 +20,7 @@ import android.content.Context import android.graphics.Insets import android.graphics.Rect import android.graphics.Region +import android.os.SystemClock import android.platform.uiautomator_helpers.DeviceHelpers import android.tools.device.apphelpers.IStandardAppHelper import android.tools.helpers.SYSTEMUI_PACKAGE @@ -27,11 +28,14 @@ import android.tools.traces.parsers.WindowManagerStateHelper import android.tools.traces.wm.WindowingMode import android.view.WindowInsets import android.view.WindowManager +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import androidx.test.uiautomator.By import androidx.test.uiautomator.BySelector import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until +import com.android.server.wm.flicker.helpers.MotionEventHelper.InputMethod.TOUCH +import com.android.window.flags.Flags import java.time.Duration /** @@ -69,13 +73,22 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : fun enterDesktopWithDrag( wmHelper: WindowManagerStateHelper, device: UiDevice, + motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH) ) { innerHelper.launchViaIntent(wmHelper) - dragToDesktop(wmHelper, device) + dragToDesktop( + wmHelper = wmHelper, + device = device, + motionEventHelper = motionEventHelper + ) waitForAppToMoveToDesktop(wmHelper) } - private fun dragToDesktop(wmHelper: WindowManagerStateHelper, device: UiDevice) { + private fun dragToDesktop( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + motionEventHelper: MotionEventHelper + ) { val windowRect = wmHelper.getWindowRegion(innerHelper).bounds val startX = windowRect.centerX() @@ -88,7 +101,17 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : val endY = displayRect.centerY() / 2 // drag the window to move to desktop - device.drag(startX, startY, startX, endY, 100) + if (motionEventHelper.inputMethod == TOUCH + && Flags.enableHoldToDragAppHandle()) { + // Touch requires hold-to-drag. + val downTime = SystemClock.uptimeMillis() + motionEventHelper.actionDown(startX, startY, time = downTime) + SystemClock.sleep(100L) // hold for 100ns before starting the move. + motionEventHelper.actionMove(startX, startY, startX, endY, 100, downTime = downTime) + motionEventHelper.actionUp(startX, endY, downTime = downTime) + } else { + device.drag(startX, startY, startX, endY, 100) + } } private fun getMaximizeButtonForTheApp(caption: UiObject2?): UiObject2 { @@ -220,9 +243,10 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : val endY = startY + verticalChange val endX = startX + horizontalChange - motionEvent.actionDown(startX, startY) - motionEvent.actionMove(startX, startY, endX, endY, /* steps= */100) - motionEvent.actionUp(endX, endY) + val downTime = SystemClock.uptimeMillis() + motionEvent.actionDown(startX, startY, time = downTime) + motionEvent.actionMove(startX, startY, endX, endY, /* steps= */100, downTime = downTime) + motionEvent.actionUp(endX, endY, downTime = downTime) wmHelper .StateSyncBuilder() .withAppTransitionIdle() diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt index 083539890906..86a0b0f8c66e 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/MotionEventHelper.kt @@ -21,6 +21,7 @@ import android.os.SystemClock import android.view.ContentInfo.Source import android.view.InputDevice.SOURCE_MOUSE import android.view.InputDevice.SOURCE_STYLUS +import android.view.InputDevice.SOURCE_TOUCHSCREEN import android.view.MotionEvent import android.view.MotionEvent.ACTION_DOWN import android.view.MotionEvent.ACTION_MOVE @@ -36,23 +37,24 @@ import android.view.MotionEvent.ToolType */ class MotionEventHelper( private val instr: Instrumentation, - private val inputMethod: InputMethod + val inputMethod: InputMethod ) { enum class InputMethod(@ToolType val toolType: Int, @Source val source: Int) { STYLUS(TOOL_TYPE_STYLUS, SOURCE_STYLUS), MOUSE(TOOL_TYPE_MOUSE, SOURCE_MOUSE), - TOUCHPAD(TOOL_TYPE_FINGER, SOURCE_MOUSE) + TOUCHPAD(TOOL_TYPE_FINGER, SOURCE_MOUSE), + TOUCH(TOOL_TYPE_FINGER, SOURCE_TOUCHSCREEN) } - fun actionDown(x: Int, y: Int) { - injectMotionEvent(ACTION_DOWN, x, y) + fun actionDown(x: Int, y: Int, time: Long = SystemClock.uptimeMillis()) { + injectMotionEvent(ACTION_DOWN, x, y, downTime = time, eventTime = time) } - fun actionUp(x: Int, y: Int) { - injectMotionEvent(ACTION_UP, x, y) + fun actionUp(x: Int, y: Int, downTime: Long) { + injectMotionEvent(ACTION_UP, x, y, downTime = downTime) } - fun actionMove(startX: Int, startY: Int, endX: Int, endY: Int, steps: Int) { + fun actionMove(startX: Int, startY: Int, endX: Int, endY: Int, steps: Int, downTime: Long) { val incrementX = (endX - startX).toFloat() / (steps - 1) val incrementY = (endY - startY).toFloat() / (steps - 1) @@ -61,14 +63,19 @@ class MotionEventHelper( val x = startX + incrementX * i val y = startY + incrementY * i - val moveEvent = getMotionEvent(time, time, ACTION_MOVE, x, y) + val moveEvent = getMotionEvent(downTime, time, ACTION_MOVE, x, y) injectMotionEvent(moveEvent) } } - private fun injectMotionEvent(action: Int, x: Int, y: Int): MotionEvent { - val eventTime = SystemClock.uptimeMillis() - val event = getMotionEvent(eventTime, eventTime, action, x.toFloat(), y.toFloat()) + private fun injectMotionEvent( + action: Int, + x: Int, + y: Int, + downTime: Long = SystemClock.uptimeMillis(), + eventTime: Long = SystemClock.uptimeMillis() + ): MotionEvent { + val event = getMotionEvent(downTime, eventTime, action, x.toFloat(), y.toFloat()) injectMotionEvent(event) return event } |