Refactor DragDetector and fix a few resizing bugs

There are a few things this CL does:

1. It refactors DragDetector to filter out moves in the slop instead of
   returning a boolean value to let the caller to decide what to do with
   it.
2. It fixes the bug that window decors respond to window moves in the
   slop.
3. It skips the unnecessary transaction at the end of drag resizing
   gesture in TaskPositioner if nothing has changed.
4. It fixes the bug that TaskPositioner uses if the new task bounds is
   empty to decide if there is no change in the task bounds.

Bug: 266448890
Test: Drag resize windows with mice and fingers.
Test: atest WMShellUnitTests

Change-Id: I8e9e1204e841bfd8f730d4091e37955f1f376018
This commit is contained in:
Garfield Tan
2023-01-19 15:38:10 -08:00
parent f7347b4978
commit 7ea23aa271
9 changed files with 424 additions and 99 deletions

View File

@@ -143,8 +143,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
private boolean shouldShowWindowDecor(RunningTaskInfo taskInfo) {
return taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
|| (taskInfo.getActivityType() == ACTIVITY_TYPE_STANDARD
&& taskInfo.configuration.windowConfiguration.getDisplayWindowingMode()
== WINDOWING_MODE_FREEFORM);
&& taskInfo.configuration.windowConfiguration.getDisplayWindowingMode()
== WINDOWING_MODE_FREEFORM);
}
private void createWindowDecoration(
@@ -175,16 +175,18 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
new CaptionTouchEventListener(taskInfo, taskPositioner);
windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
windowDecoration.setDragResizeCallback(taskPositioner);
windowDecoration.setDragDetector(touchEventListener.mDragDetector);
windowDecoration.relayout(taskInfo, startT, finishT);
setupCaptionColor(taskInfo, windowDecoration);
}
private class CaptionTouchEventListener implements
View.OnClickListener, View.OnTouchListener {
View.OnClickListener, View.OnTouchListener, DragDetector.MotionEventHandler {
private final int mTaskId;
private final WindowContainerToken mTaskToken;
private final DragResizeCallback mDragResizeCallback;
private final DragDetector mDragDetector;
private int mDragPointerId = -1;
@@ -194,6 +196,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
mTaskId = taskInfo.taskId;
mTaskToken = taskInfo.token;
mDragResizeCallback = dragResizeCallback;
mDragDetector = new DragDetector(this);
}
@Override
@@ -216,7 +219,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
if (v.getId() != R.id.caption) {
return false;
}
handleEventForMove(e);
mDragDetector.onMotionEvent(e);
if (e.getAction() != MotionEvent.ACTION_DOWN) {
return false;
@@ -235,10 +238,11 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
* @param e {@link MotionEvent} to process
* @return {@code true} if a drag is happening; or {@code false} if it is not
*/
private void handleEventForMove(MotionEvent e) {
@Override
public boolean handleMotionEvent(MotionEvent e) {
final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
if (taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
return;
return false;
}
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
@@ -261,6 +265,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel {
break;
}
}
return true;
}
}
}
}

View File

@@ -49,7 +49,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
private View.OnTouchListener mOnCaptionTouchListener;
private DragResizeCallback mDragResizeCallback;
private DragResizeInputListener mDragResizeListener;
private final DragDetector mDragDetector;
private DragDetector mDragDetector;
private RelayoutParams mRelayoutParams = new RelayoutParams();
private final RelayoutResult<WindowDecorLinearLayout> mResult =
@@ -69,7 +69,6 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
mHandler = handler;
mChoreographer = choreographer;
mSyncQueue = syncQueue;
mDragDetector = new DragDetector(ViewConfiguration.get(context).getScaledTouchSlop());
}
void setCaptionListeners(
@@ -83,6 +82,11 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
mDragResizeCallback = dragResizeCallback;
}
void setDragDetector(DragDetector dragDetector) {
mDragDetector = dragDetector;
mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop());
}
@Override
void relayout(RunningTaskInfo taskInfo) {
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();

View File

@@ -205,7 +205,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
}
private class DesktopModeTouchEventListener implements
View.OnClickListener, View.OnTouchListener {
View.OnClickListener, View.OnTouchListener, DragDetector.MotionEventHandler {
private final int mTaskId;
private final WindowContainerToken mTaskToken;
@@ -216,12 +216,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
private DesktopModeTouchEventListener(
RunningTaskInfo taskInfo,
DragResizeCallback dragResizeCallback,
DragDetector dragDetector) {
DragResizeCallback dragResizeCallback) {
mTaskId = taskInfo.taskId;
mTaskToken = taskInfo.token;
mDragResizeCallback = dragResizeCallback;
mDragDetector = dragDetector;
mDragDetector = new DragDetector(this);
}
@Override
@@ -254,8 +253,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
return false;
}
if (id == R.id.caption_handle) {
isDrag = mDragDetector.detectDragEvent(e);
handleEventForMove(e);
isDrag = mDragDetector.onMotionEvent(e);
}
if (e.getAction() != MotionEvent.ACTION_DOWN) {
return isDrag;
@@ -272,19 +270,19 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
/**
* @param e {@link MotionEvent} to process
* @return {@code true} if a drag is happening; or {@code false} if it is not
* @return {@code true} if the motion event is handled.
*/
private void handleEventForMove(MotionEvent e) {
@Override
public boolean handleMotionEvent(MotionEvent e) {
final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId);
if (DesktopModeStatus.isProto2Enabled()
&& taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
return;
return false;
}
if (DesktopModeStatus.isProto1Enabled() && mDesktopModeController.isPresent()
&& mDesktopModeController.get().getDisplayAreaWindowingMode(
taskInfo.displayId)
&& mDesktopModeController.get().getDisplayAreaWindowingMode(taskInfo.displayId)
== WINDOWING_MODE_FULLSCREEN) {
return;
return false;
}
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
@@ -324,6 +322,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
break;
}
}
return true;
}
}
@@ -560,10 +559,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel {
final TaskPositioner taskPositioner =
new TaskPositioner(mTaskOrganizer, windowDecoration, mDragStartListener);
final DesktopModeTouchEventListener touchEventListener =
new DesktopModeTouchEventListener(
taskInfo, taskPositioner, windowDecoration.getDragDetector());
new DesktopModeTouchEventListener(taskInfo, taskPositioner);
windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
windowDecoration.setDragResizeCallback(taskPositioner);
windowDecoration.setDragDetector(touchEventListener.mDragDetector);
windowDecoration.relayout(taskInfo, startT, finishT);
incrementEventReceiverTasks(taskInfo.displayId);
}

View File

@@ -57,7 +57,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
private View.OnTouchListener mOnCaptionTouchListener;
private DragResizeCallback mDragResizeCallback;
private DragResizeInputListener mDragResizeListener;
private final DragDetector mDragDetector;
private DragDetector mDragDetector;
private RelayoutParams mRelayoutParams = new RelayoutParams();
private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
@@ -81,7 +81,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
mChoreographer = choreographer;
mSyncQueue = syncQueue;
mDesktopActive = DesktopModeStatus.isActive(mContext);
mDragDetector = new DragDetector(ViewConfiguration.get(context).getScaledTouchSlop());
}
void setCaptionListeners(
@@ -95,8 +94,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
mDragResizeCallback = dragResizeCallback;
}
DragDetector getDragDetector() {
return mDragDetector;
void setDragDetector(DragDetector dragDetector) {
mDragDetector = dragDetector;
mDragDetector.setTouchSlop(ViewConfiguration.get(mContext).getScaledTouchSlop());
}
@Override

View File

@@ -16,6 +16,7 @@
package com.android.wm.shell.windowdecor;
import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
@@ -25,63 +26,82 @@ import android.graphics.PointF;
import android.view.MotionEvent;
/**
* A detector for touch inputs that differentiates between drag and click inputs.
* A detector for touch inputs that differentiates between drag and click inputs. It receives a flow
* of {@link MotionEvent} and generates a new flow of motion events with slop in consideration to
* the event handler. In particular, it always passes down, up and cancel events. It'll pass move
* events only when there is at least one move event that's beyond the slop threshold. For the
* purpose of convenience it also passes all events of other actions.
*
* All touch events must be passed through this class to track a drag event.
*/
public class DragDetector {
class DragDetector {
private final MotionEventHandler mEventHandler;
private final PointF mInputDownPoint = new PointF();
private int mTouchSlop;
private PointF mInputDownPoint;
private boolean mIsDragEvent;
private int mDragPointerId;
public DragDetector(int touchSlop) {
mTouchSlop = touchSlop;
mInputDownPoint = new PointF();
mIsDragEvent = false;
mDragPointerId = -1;
private boolean mResultOfDownAction;
DragDetector(MotionEventHandler eventHandler) {
resetState();
mEventHandler = eventHandler;
}
/**
* Determine if {@link MotionEvent} is part of a drag event.
* @return {@code true} if this is a drag event, {@code false} if not
*/
public boolean detectDragEvent(MotionEvent ev) {
switch (ev.getAction()) {
* The receiver of the {@link MotionEvent} flow.
*
* @return the result returned by {@link #mEventHandler}, or the result when
* {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
*/
boolean onMotionEvent(MotionEvent ev) {
switch (ev.getActionMasked()) {
case ACTION_DOWN: {
// Only touch screens generate noisy moves.
mIsDragEvent = (ev.getSource() & SOURCE_TOUCHSCREEN) != SOURCE_TOUCHSCREEN;
mDragPointerId = ev.getPointerId(0);
float rawX = ev.getRawX(0);
float rawY = ev.getRawY(0);
mInputDownPoint.set(rawX, rawY);
return false;
mResultOfDownAction = mEventHandler.handleMotionEvent(ev);
return mResultOfDownAction;
}
case ACTION_MOVE: {
if (!mIsDragEvent) {
int dragPointerIndex = ev.findPointerIndex(mDragPointerId);
float dx = ev.getRawX(dragPointerIndex) - mInputDownPoint.x;
float dy = ev.getRawY(dragPointerIndex) - mInputDownPoint.y;
if (Math.hypot(dx, dy) > mTouchSlop) {
mIsDragEvent = true;
}
mIsDragEvent = Math.hypot(dx, dy) > mTouchSlop;
}
if (mIsDragEvent) {
return mEventHandler.handleMotionEvent(ev);
} else {
return mResultOfDownAction;
}
return mIsDragEvent;
}
case ACTION_UP: {
boolean result = mIsDragEvent;
mIsDragEvent = false;
mInputDownPoint.set(0, 0);
mDragPointerId = -1;
return result;
}
case ACTION_UP:
case ACTION_CANCEL: {
mIsDragEvent = false;
mInputDownPoint.set(0, 0);
mDragPointerId = -1;
return false;
resetState();
return mEventHandler.handleMotionEvent(ev);
}
default:
return mEventHandler.handleMotionEvent(ev);
}
return mIsDragEvent;
}
public void setTouchSlop(int touchSlop) {
void setTouchSlop(int touchSlop) {
mTouchSlop = touchSlop;
}
private void resetState() {
mIsDragEvent = false;
mInputDownPoint.set(0, 0);
mDragPointerId = -1;
mResultOfDownAction = false;
}
interface MotionEventHandler {
boolean handleMotionEvent(MotionEvent ev);
}
}

View File

@@ -48,7 +48,6 @@ import com.android.internal.view.BaseIWindow;
* Task edges are for resizing with a mouse.
* Task corners are for resizing with touch input.
*/
// TODO(b/251270585): investigate how to pass taps in corners to the tasks
class DragResizeInputListener implements AutoCloseable {
private static final String TAG = "DragResizeInputListener";
@@ -115,7 +114,8 @@ class DragResizeInputListener implements AutoCloseable {
mInputEventReceiver = new TaskResizeInputEventReceiver(
mInputChannel, mHandler, mChoreographer);
mCallback = callback;
mDragDetector = new DragDetector(ViewConfiguration.get(context).getScaledTouchSlop());
mDragDetector = new DragDetector(mInputEventReceiver);
mDragDetector.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop());
}
/**
@@ -223,12 +223,12 @@ class DragResizeInputListener implements AutoCloseable {
}
}
private class TaskResizeInputEventReceiver extends InputEventReceiver {
private class TaskResizeInputEventReceiver extends InputEventReceiver
implements DragDetector.MotionEventHandler {
private final Choreographer mChoreographer;
private final Runnable mConsumeBatchEventRunnable;
private boolean mConsumeBatchEventScheduled;
private boolean mShouldHandleEvents;
private boolean mDragging;
private TaskResizeInputEventReceiver(
InputChannel inputChannel, Handler handler, Choreographer choreographer) {
@@ -270,15 +270,15 @@ class DragResizeInputListener implements AutoCloseable {
if (!(inputEvent instanceof MotionEvent)) {
return false;
}
return mDragDetector.onMotionEvent((MotionEvent) inputEvent);
}
MotionEvent e = (MotionEvent) inputEvent;
@Override
public boolean handleMotionEvent(MotionEvent e) {
boolean result = false;
// Check if this is a touch event vs mouse event.
// Touch events are tracked in four corners. Other events are tracked in resize edges.
boolean isTouch = (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
if (isTouch) {
mDragging = mDragDetector.detectDragEvent(e);
}
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
float x = e.getX(0);
@@ -305,24 +305,17 @@ class DragResizeInputListener implements AutoCloseable {
int dragPointerIndex = e.findPointerIndex(mDragPointerId);
float rawX = e.getRawX(dragPointerIndex);
float rawY = e.getRawY(dragPointerIndex);
if (!isTouch) {
// For all other types allow immediate dragging.
mDragging = true;
}
if (mDragging) {
mCallback.onDragResizeMove(rawX, rawY);
result = true;
}
mCallback.onDragResizeMove(rawX, rawY);
result = true;
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
if (mShouldHandleEvents && mDragging) {
if (mShouldHandleEvents) {
int dragPointerIndex = e.findPointerIndex(mDragPointerId);
mCallback.onDragResizeEnd(
e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex));
}
mDragging = false;
mShouldHandleEvents = false;
mDragPointerId = -1;
result = true;

View File

@@ -40,9 +40,7 @@ class TaskPositioner implements DragResizeCallback {
private final Rect mTaskBoundsAtDragStart = new Rect();
private final PointF mResizeStartPoint = new PointF();
private final Rect mResizeTaskBounds = new Rect();
// Whether the |dragResizing| hint should be sent with the next bounds change WCT.
// Used to optimized fluid resizing of freeform tasks.
private boolean mPendingDragResizeHint = false;
private boolean mHasMoved = false;
private int mCtrlType;
private DragStartListener mDragStartListener;
@@ -60,11 +58,7 @@ class TaskPositioner implements DragResizeCallback {
@Override
public void onDragResizeStart(int ctrlType, float x, float y) {
if (ctrlType != CTRL_TYPE_UNDEFINED) {
// The task is being resized, send the |dragResizing| hint to core with the first
// bounds-change wct.
mPendingDragResizeHint = true;
}
mHasMoved = false;
mDragStartListener.onDragStart(mWindowDecoration.mTaskInfo.taskId);
mCtrlType = ctrlType;
@@ -78,30 +72,44 @@ class TaskPositioner implements DragResizeCallback {
public void onDragResizeMove(float x, float y) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
if (changeBounds(wct, x, y)) {
if (mPendingDragResizeHint) {
// The task is being resized, send the |dragResizing| hint to core with the first
// bounds-change wct.
if (!mHasMoved && mCtrlType != CTRL_TYPE_UNDEFINED) {
// This is the first bounds change since drag resize operation started.
wct.setDragResizing(mWindowDecoration.mTaskInfo.token, true /* dragResizing */);
mPendingDragResizeHint = false;
}
mTaskOrganizer.applyTransaction(wct);
mHasMoved = true;
}
}
@Override
public void onDragResizeEnd(float x, float y) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */);
changeBounds(wct, x, y);
mTaskOrganizer.applyTransaction(wct);
// |mHasMoved| being false means there is no real change to the task bounds in WM core, so
// we don't need a WCT to finish it.
if (mHasMoved) {
final WindowContainerTransaction wct = new WindowContainerTransaction();
wct.setDragResizing(mWindowDecoration.mTaskInfo.token, false /* dragResizing */);
changeBounds(wct, x, y);
mTaskOrganizer.applyTransaction(wct);
}
mCtrlType = 0;
mCtrlType = CTRL_TYPE_UNDEFINED;
mTaskBoundsAtDragStart.setEmpty();
mResizeStartPoint.set(0, 0);
mPendingDragResizeHint = false;
mHasMoved = false;
}
private boolean changeBounds(WindowContainerTransaction wct, float x, float y) {
float deltaX = x - mResizeStartPoint.x;
// |mResizeTaskBounds| is the bounds last reported if |mHasMoved| is true. If it's not true,
// we can compare it against |mTaskBoundsAtDragStart|.
final int oldLeft = mHasMoved ? mResizeTaskBounds.left : mTaskBoundsAtDragStart.left;
final int oldTop = mHasMoved ? mResizeTaskBounds.top : mTaskBoundsAtDragStart.top;
final int oldRight = mHasMoved ? mResizeTaskBounds.right : mTaskBoundsAtDragStart.right;
final int oldBottom = mHasMoved ? mResizeTaskBounds.bottom : mTaskBoundsAtDragStart.bottom;
final float deltaX = x - mResizeStartPoint.x;
final float deltaY = y - mResizeStartPoint.y;
mResizeTaskBounds.set(mTaskBoundsAtDragStart);
if ((mCtrlType & CTRL_TYPE_LEFT) != 0) {
mResizeTaskBounds.left += deltaX;
@@ -109,22 +117,22 @@ class TaskPositioner implements DragResizeCallback {
if ((mCtrlType & CTRL_TYPE_RIGHT) != 0) {
mResizeTaskBounds.right += deltaX;
}
float deltaY = y - mResizeStartPoint.y;
if ((mCtrlType & CTRL_TYPE_TOP) != 0) {
mResizeTaskBounds.top += deltaY;
}
if ((mCtrlType & CTRL_TYPE_BOTTOM) != 0) {
mResizeTaskBounds.bottom += deltaY;
}
if (mCtrlType == 0) {
if (mCtrlType == CTRL_TYPE_UNDEFINED) {
mResizeTaskBounds.offset((int) deltaX, (int) deltaY);
}
if (!mResizeTaskBounds.isEmpty()) {
wct.setBounds(mWindowDecoration.mTaskInfo.token, mResizeTaskBounds);
return true;
if (oldLeft == mResizeTaskBounds.left && oldTop == mResizeTaskBounds.top
&& oldRight == mResizeTaskBounds.right && oldBottom == mResizeTaskBounds.bottom) {
return false;
}
return false;
wct.setBounds(mWindowDecoration.mTaskInfo.token, mResizeTaskBounds);
return true;
}
interface DragStartListener {

View File

@@ -0,0 +1,210 @@
/*
* Copyright (C) 2023 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.os.SystemClock
import android.testing.AndroidTestingRunner
import android.view.MotionEvent
import android.view.InputDevice
import androidx.test.filters.SmallTest
import org.junit.After
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
import org.mockito.Mockito.argThat
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
/**
* Tests for [DragDetector].
*
* Build/Install/Run:
* atest WMShellUnitTests:DragDetectorTest
*/
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DragDetectorTest {
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())).thenReturn(true)
dragDetector = DragDetector(eventHandler)
dragDetector.setTouchSlop(SLOP)
}
@After
fun tearDown() {
motionEvents.forEach {
it.recycle()
}
motionEvents.clear()
}
@Test
fun testNoMove_passesDownAndUp() {
assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
verify(eventHandler).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
it.source == InputDevice.SOURCE_TOUCHSCREEN
})
assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP)))
verify(eventHandler).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_UP && it.x == X && it.y == Y &&
it.source == InputDevice.SOURCE_TOUCHSCREEN
})
}
@Test
fun testMoveInSlop_touch_passesDownAndUp() {
`when`(eventHandler.handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_DOWN
})).thenReturn(false)
assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
verify(eventHandler).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
it.source == InputDevice.SOURCE_TOUCHSCREEN
})
val newX = X + SLOP - 1
assertFalse(
dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y)))
verify(eventHandler, never()).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_MOVE
})
assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP, newX, Y)))
verify(eventHandler).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y &&
it.source == InputDevice.SOURCE_TOUCHSCREEN
})
}
@Test
fun testMoveInSlop_mouse_passesDownMoveAndUp() {
`when`(eventHandler.handleMotionEvent(argThat {
it.action == MotionEvent.ACTION_DOWN
})).thenReturn(false)
assertFalse(dragDetector.onMotionEvent(
createMotionEvent(MotionEvent.ACTION_DOWN, isTouch = false)))
verify(eventHandler).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
it.source == InputDevice.SOURCE_MOUSE
})
val newX = X + SLOP - 1
assertTrue(dragDetector.onMotionEvent(
createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y, isTouch = false)))
verify(eventHandler).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y &&
it.source == InputDevice.SOURCE_MOUSE
})
assertTrue(dragDetector.onMotionEvent(
createMotionEvent(MotionEvent.ACTION_UP, newX, Y, isTouch = false)))
verify(eventHandler).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y &&
it.source == InputDevice.SOURCE_MOUSE
})
}
@Test
fun testMoveBeyondSlop_passesDownMoveAndUp() {
`when`(eventHandler.handleMotionEvent(argThat {
it.action == MotionEvent.ACTION_DOWN
})).thenReturn(false)
assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_DOWN)))
verify(eventHandler).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_DOWN && it.x == X && it.y == Y &&
it.source == InputDevice.SOURCE_TOUCHSCREEN
})
val newX = X + SLOP + 1
assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_MOVE, newX, Y)))
verify(eventHandler).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_MOVE && it.x == newX && it.y == Y &&
it.source == InputDevice.SOURCE_TOUCHSCREEN
})
assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_UP, newX, Y)))
verify(eventHandler).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_UP && it.x == newX && it.y == Y &&
it.source == InputDevice.SOURCE_TOUCHSCREEN
})
}
@Test
fun testPassesHoverEnter() {
`when`(eventHandler.handleMotionEvent(argThat {
it.action == MotionEvent.ACTION_HOVER_ENTER
})).thenReturn(false)
assertFalse(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_ENTER)))
verify(eventHandler).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_HOVER_ENTER && it.x == X && it.y == Y
})
}
@Test
fun testPassesHoverMove() {
assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_MOVE)))
verify(eventHandler).handleMotionEvent(argThat {
return@argThat it.action == MotionEvent.ACTION_HOVER_MOVE && it.x == X && it.y == Y
})
}
@Test
fun testPassesHoverExit() {
assertTrue(dragDetector.onMotionEvent(createMotionEvent(MotionEvent.ACTION_HOVER_EXIT)))
verify(eventHandler).handleMotionEvent(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)
ev.source = if (isTouch) InputDevice.SOURCE_TOUCHSCREEN else InputDevice.SOURCE_MOUSE
motionEvents.add(ev)
return ev
}
companion object {
private const val SLOP = 10
private const val X = 123f
private const val Y = 234f
}
}

View File

@@ -1,6 +1,7 @@
package com.android.wm.shell.windowdecor
import android.app.ActivityManager
import android.app.WindowConfiguration
import android.graphics.Rect
import android.os.IBinder
import android.testing.AndroidTestingRunner
@@ -10,6 +11,7 @@ import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_RIGHT
import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_TOP
import com.android.wm.shell.windowdecor.TaskPositioner.CTRL_TYPE_UNDEFINED
import org.junit.Before
import org.junit.Test
@@ -62,6 +64,90 @@ class TaskPositionerTest : ShellTestCase() {
}
}
@Test
fun testDragResize_notMove_skipsTransactionOnEnd() {
taskPositioner.onDragResizeStart(
CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
STARTING_BOUNDS.left.toFloat(),
STARTING_BOUNDS.top.toFloat()
)
taskPositioner.onDragResizeEnd(
STARTING_BOUNDS.left.toFloat() + 10,
STARTING_BOUNDS.top.toFloat() + 10
)
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_noEffectiveMove_skipsTransactionOnMoveAndEnd() {
taskPositioner.onDragResizeStart(
CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
STARTING_BOUNDS.left.toFloat(),
STARTING_BOUNDS.top.toFloat()
)
taskPositioner.onDragResizeMove(
STARTING_BOUNDS.left.toFloat(),
STARTING_BOUNDS.top.toFloat()
)
taskPositioner.onDragResizeEnd(
STARTING_BOUNDS.left.toFloat() + 10,
STARTING_BOUNDS.top.toFloat() + 10
)
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_hasEffectiveMove_issuesTransactionOnMoveAndEnd() {
taskPositioner.onDragResizeStart(
CTRL_TYPE_TOP or CTRL_TYPE_RIGHT,
STARTING_BOUNDS.left.toFloat(),
STARTING_BOUNDS.top.toFloat()
)
taskPositioner.onDragResizeMove(
STARTING_BOUNDS.left.toFloat() + 10,
STARTING_BOUNDS.top.toFloat()
)
val rectAfterMove = Rect(STARTING_BOUNDS)
rectAfterMove.right += 10
verify(mockShellTaskOrganizer).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.onDragResizeEnd(
STARTING_BOUNDS.left.toFloat() + 10,
STARTING_BOUNDS.top.toFloat() + 10
)
val rectAfterEnd = Rect(rectAfterMove)
rectAfterEnd.top += 10
verify(mockShellTaskOrganizer).applyTransaction(argThat { wct ->
return@argThat wct.changes.any { (token, change) ->
token == taskBinder &&
(change.windowSetMask and WindowConfiguration.WINDOW_CONFIG_BOUNDS) != 0 &&
change.configuration.windowConfiguration.bounds == rectAfterEnd
}
})
}
@Test
fun testDragResize_move_skipsDragResizingFlag() {
taskPositioner.onDragResizeStart(