Implement MouseToTouch compatibility
This adds per-app overrides compatibility feature that rewrites motion
event from mouse to touch/finger events.
Bug: 413207127
Test: MouseToTouchProcessorTest
Flag: com.android.hardware.input.mouse_to_touch_per_app_compat
Change-Id: I1481786fdb7eb5e08ced04b0c687abcdbccf2ff2
diff --git a/core/java/android/view/input/MouseToTouchProcessor.java b/core/java/android/view/input/MouseToTouchProcessor.java
index e65f8e2..6ce2164 100644
--- a/core/java/android/view/input/MouseToTouchProcessor.java
+++ b/core/java/android/view/input/MouseToTouchProcessor.java
@@ -16,16 +16,23 @@
package android.view.input;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.os.Handler;
+import android.util.Log;
+import android.util.SparseArray;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.InputEventCompatProcessor;
import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
@@ -38,6 +45,20 @@
public class MouseToTouchProcessor extends InputEventCompatProcessor {
private static final String TAG = MouseToTouchProcessor.class.getSimpleName();
+ @IntDef({STATE_AWAITING, STATE_CONVERTING, STATE_NON_PRIMARY_CLICK})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface State {}
+ private static final int STATE_AWAITING = 0;
+ private static final int STATE_CONVERTING = 1;
+ private static final int STATE_NON_PRIMARY_CLICK = 2;
+ private @State int mState = STATE_AWAITING;
+
+ /**
+ * Map the processed event's id to the original event, or null if the event is synthesized
+ * in this class.
+ */
+ private final SparseArray<InputEvent> mModifiedEventMap = new SparseArray<>();
+
/**
* Return {@code true} if this compatibility is required based on the given context.
*
@@ -57,15 +78,196 @@
super(context, handler);
}
+ @Nullable
@Override
public List<InputEvent> processInputEventForCompatibility(@NonNull InputEvent event) {
- // TODO(b/413207127): Implement the feature.
+ if (!(event instanceof MotionEvent motionEvent)
+ || !motionEvent.isFromSource(InputDevice.SOURCE_MOUSE)
+ || motionEvent.getActionMasked() == MotionEvent.ACTION_OUTSIDE
+ || motionEvent.getActionMasked() == MotionEvent.ACTION_SCROLL) {
+ return null;
+ }
+
+ final List<InputEvent> result = processMotionEvent(motionEvent);
+ if (result != null && !result.isEmpty()) {
+ for (int i = 0; i < result.size() - 1; i++) {
+ mModifiedEventMap.put(result.get(i).getId(), null);
+ }
+ mModifiedEventMap.put(result.getLast().getId(), event);
+ }
+ return result;
+ }
+
+ @Nullable
+ private List<InputEvent> processMotionEvent(@NonNull MotionEvent event) {
+ return switch (mState) {
+ case STATE_AWAITING -> processEventInAwaitingState(event);
+ case STATE_CONVERTING -> processEventInConvertingState(event);
+ case STATE_NON_PRIMARY_CLICK -> processEventInNonPrimaryClickState(event);
+ default -> null;
+ };
+ }
+
+ @Nullable
+ private List<InputEvent> processEventInAwaitingState(@NonNull MotionEvent event) {
+ if (event.isHoverEvent()) {
+ // Don't modify hover events, but clear the button state (e.g. BACK).
+ if (event.getButtonState() != 0) {
+ event.setButtonState(0);
+ return List.of(event);
+ }
+ return null;
+ }
+
+ final int action = event.getActionMasked();
+ if (isActionButtonEvent(action)) {
+ // Button events, usually BACK and FORWARD, are dropped.
+ return List.of();
+ }
+ if (action != MotionEvent.ACTION_DOWN) {
+ Log.e(TAG, "Broken input sequence is observed. event=" + event);
+ // Decide the next state based anyway on the primary button state.
+ }
+ boolean primaryButton = (event.getButtonState() & MotionEvent.BUTTON_PRIMARY)
+ == MotionEvent.BUTTON_PRIMARY;
+ if (primaryButton || isTouchpadGesture(event)) {
+ mState = STATE_CONVERTING;
+ return List.of(obtainRewrittenEventAsTouch(event));
+ } else {
+ mState = STATE_NON_PRIMARY_CLICK;
+ return null;
+ }
+ }
+
+ @Nullable
+ private List<InputEvent> processEventInConvertingState(@NonNull MotionEvent event) {
+ // STATE_CONVERTING starts with a primary button.
+ // In this state, events are converted to touch events, and other button properties and
+ // events are dropped.
+ // Note that non-primary buttons can be also pressed and released during this state, and
+ // ACTION_UP is dispatched only when all of (primary, secondary, and tertiary) buttons
+ // are released.
+
+ final int action = event.getActionMasked();
+ if (isActionButtonEvent(action)) {
+ // Button events are always dropped.
+ return List.of();
+ } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mState = STATE_AWAITING;
+ }
+
+ return List.of(obtainRewrittenEventAsTouch(event));
+ }
+
+ @Nullable
+ private List<InputEvent> processEventInNonPrimaryClickState(@NonNull MotionEvent event) {
+ final int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+ mState = STATE_AWAITING;
+ }
+ // During a gesture that was started from the non-primary (e.g. right) click, no rewrite
+ // happens even if there's primary (left) button click.
return null;
}
@Nullable
@Override
public InputEvent processInputEventBeforeFinish(@NonNull InputEvent inputEvent) {
- return inputEvent;
+ final int idx = mModifiedEventMap.indexOfKey(inputEvent.getId());
+ if (idx < 0) {
+ return inputEvent;
+ }
+
+ final InputEvent originalEvent = mModifiedEventMap.valueAt(idx);
+ mModifiedEventMap.removeAt(idx);
+ if (inputEvent != originalEvent) {
+ inputEvent.recycleIfNeededAfterDispatch();
+ }
+ return originalEvent;
+ }
+
+ @NonNull
+ private static MotionEvent obtainRewrittenEventAsTouch(@NonNull MotionEvent original) {
+ final int numPointers = original.getPointerCount();
+
+ final PointerProperties[] pointerProps = new PointerProperties[numPointers];
+ for (int i = 0; i < numPointers; i++) {
+ pointerProps[i] = new PointerProperties();
+ original.getPointerProperties(i, pointerProps[i]);
+ pointerProps[i].toolType = MotionEvent.TOOL_TYPE_FINGER;
+ }
+
+ final long firstEventTime;
+ final PointerCoords[] pointerCoords = new PointerCoords[numPointers];
+ for (int pointerIdx = 0; pointerIdx < numPointers; pointerIdx++) {
+ pointerCoords[pointerIdx] = new PointerCoords();
+ }
+
+ if (original.getHistorySize() > 0) {
+ firstEventTime = original.getHistoricalEventTime(0);
+ for (int pointerIdx = 0; pointerIdx < numPointers; pointerIdx++) {
+ original.getHistoricalPointerCoords(pointerIdx, 0, pointerCoords[pointerIdx]);
+ }
+ } else {
+ firstEventTime = original.getEventTime();
+ for (int pointerIdx = 0; pointerIdx < numPointers; pointerIdx++) {
+ original.getPointerCoords(pointerIdx, pointerCoords[pointerIdx]);
+ }
+ }
+
+ final MotionEvent result =
+ MotionEvent.obtain(
+ original.getDownTime(),
+ firstEventTime,
+ original.getAction(),
+ original.getPointerCount(),
+ pointerProps,
+ pointerCoords,
+ original.getMetaState(),
+ /* buttonState= */ 0,
+ original.getXPrecision(),
+ original.getYPrecision(),
+ original.getDeviceId(),
+ original.getEdgeFlags(),
+ InputDevice.SOURCE_TOUCHSCREEN,
+ original.getDisplayId(),
+ original.getFlags(),
+ original.getClassification());
+
+ // If there are one or more history, add them to the event, and the last one is not from the
+ // history but the current coords.
+ for (int historyIdx = 1; historyIdx <= original.getHistorySize(); historyIdx++) {
+ long eventTime;
+ if (historyIdx == original.getHistorySize()) {
+ eventTime = original.getEventTime();
+ for (int pointerIdx = 0; pointerIdx < numPointers; pointerIdx++) {
+ original.getPointerCoords(pointerIdx, pointerCoords[pointerIdx]);
+ }
+ } else {
+ eventTime = original.getHistoricalEventTime(historyIdx);
+ for (int pointerIdx = 0; pointerIdx < numPointers; pointerIdx++) {
+ original.getHistoricalPointerCoords(
+ pointerIdx, historyIdx, pointerCoords[pointerIdx]);
+ }
+ }
+ result.addBatch(eventTime, pointerCoords, original.getMetaState());
+ }
+
+ result.setActionButton(0);
+ result.setButtonState(0);
+
+ return result;
+ }
+
+ private static boolean isActionButtonEvent(int action) {
+ return action == MotionEvent.ACTION_BUTTON_PRESS
+ || action == MotionEvent.ACTION_BUTTON_RELEASE;
+ }
+
+ private static boolean isTouchpadGesture(MotionEvent event) {
+ return event.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER
+ && (event.getClassification() == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
+ || event.getClassification() == MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE
+ || event.getClassification() == MotionEvent.CLASSIFICATION_PINCH);
}
}
diff --git a/core/tests/coretests/src/android/view/input/MouseToTouchProcessorTest.kt b/core/tests/coretests/src/android/view/input/MouseToTouchProcessorTest.kt
index b4d1d0f..2dcf1c1 100644
--- a/core/tests/coretests/src/android/view/input/MouseToTouchProcessorTest.kt
+++ b/core/tests/coretests/src/android/view/input/MouseToTouchProcessorTest.kt
@@ -22,17 +22,37 @@
import android.content.pm.FeatureInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
+import android.graphics.PointF
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.annotations.Presubmit
import android.platform.test.flag.junit.SetFlagsRule
+import android.view.InputDevice
+import android.view.KeyEvent
+import android.view.MotionEvent
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.cts.input.MotionEventBuilder
+import com.android.cts.input.PointerBuilder
+import com.android.cts.input.inputeventmatchers.withActionButton
+import com.android.cts.input.inputeventmatchers.withButtonState
+import com.android.cts.input.inputeventmatchers.withClassification
+import com.android.cts.input.inputeventmatchers.withCoords
+import com.android.cts.input.inputeventmatchers.withCoordsForHistoryPos
+import com.android.cts.input.inputeventmatchers.withCoordsForPointerIndex
+import com.android.cts.input.inputeventmatchers.withHistorySize
+import com.android.cts.input.inputeventmatchers.withMotionAction
+import com.android.cts.input.inputeventmatchers.withPointerCount
+import com.android.cts.input.inputeventmatchers.withSource
+import com.android.cts.input.inputeventmatchers.withToolType
import com.android.hardware.input.Flags
import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges
import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges
+import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.CoreMatchers.nullValue
import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.collection.IsCollectionWithSize.hasSize
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@@ -118,4 +138,600 @@
assertThat(MouseToTouchProcessor.isCompatibilityNeeded(mockContext), equalTo(true))
}
+
+ @Test
+ fun processInputEventForCompatibilityReturnsNullForNonMotionEvent() {
+ val keyEvent =
+ KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_A)
+ val result = processor.processInputEventForCompatibility(keyEvent)
+ assertThat(result, nullValue())
+ }
+
+ @Test
+ fun processInputEventForCompatibilityReturnsNullForNonMouseSource() {
+ val touchEvent = MotionEventBuilder(MotionEvent.ACTION_DOWN, InputDevice.SOURCE_TOUCHSCREEN)
+ .pointer(PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER).x(0f).y(0f))
+ .build()
+ val result = processor.processInputEventForCompatibility(touchEvent)
+ assertThat(result, nullValue())
+ }
+
+ @Test
+ fun processInputEventForCompatibilityReturnsNullForScrollEvent() {
+ val scrollEvent = MotionEventBuilder(MotionEvent.ACTION_SCROLL, InputDevice.SOURCE_MOUSE)
+ .pointer(PointerBuilder(0, MotionEvent.TOOL_TYPE_MOUSE).x(0f).y(0f))
+ .build()
+ val result = processor.processInputEventForCompatibility(scrollEvent)
+ assertThat(result, nullValue())
+ }
+
+ @Test
+ fun processInputEventForCompatibilityReturnsNullForHoverEvents() {
+ val pointer = PointerBuilder(0, MotionEvent.TOOL_TYPE_MOUSE).x(0f).y(0f)
+ val enterEvent =
+ MotionEventBuilder(MotionEvent.ACTION_HOVER_ENTER, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .build()
+ val enterResult = processor.processInputEventForCompatibility(enterEvent)
+ assertThat(enterResult, nullValue())
+
+ val moveEvent = MotionEventBuilder(MotionEvent.ACTION_HOVER_MOVE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .build()
+ val moveResult = processor.processInputEventForCompatibility(moveEvent)
+ assertThat(moveResult, nullValue())
+
+ val exitEvent = MotionEventBuilder(MotionEvent.ACTION_HOVER_EXIT, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .build()
+ val exitResult = processor.processInputEventForCompatibility(exitEvent)
+ assertThat(exitResult, nullValue())
+ }
+
+ @Test
+ fun processInputEventForCompatibilityConvertsMouseEvents() {
+ val pointer = PointerBuilder(0, MotionEvent.TOOL_TYPE_MOUSE).x(100f).y(200f)
+ // Process ACTION_DOWN
+ val mouseDownEvent = MotionEventBuilder(MotionEvent.ACTION_DOWN, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ val downResult = processor.processInputEventForCompatibility(mouseDownEvent)
+
+ assertThat(downResult, hasSize(1))
+ assertThat(
+ downResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_DOWN),
+ withSource(InputDevice.SOURCE_TOUCHSCREEN),
+ withToolType(MotionEvent.TOOL_TYPE_FINGER),
+ withButtonState(0),
+ withActionButton(0),
+ withCoords(PointF(100f, 200f)),
+ )
+ )
+
+ // Process ACTION_BUTTON_PRESS
+ val buttonPressEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_PRESS, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_PRIMARY)
+ .actionButton(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ val pressResult = processor.processInputEventForCompatibility(buttonPressEvent)
+ assertThat(pressResult, hasSize(0))
+
+ // Process ACTION_MOVE
+ pointer.x(110f).y(220f)
+ val mouseMoveEvent =
+ MotionEventBuilder(MotionEvent.ACTION_MOVE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ val moveResult = processor.processInputEventForCompatibility(mouseMoveEvent)
+
+ assertThat(moveResult, hasSize(1))
+ assertThat(
+ moveResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_MOVE),
+ withSource(InputDevice.SOURCE_TOUCHSCREEN),
+ withToolType(MotionEvent.TOOL_TYPE_FINGER),
+ withCoords(PointF(110f, 220f)),
+ withButtonState(0),
+ withActionButton(0),
+ )
+ )
+
+ // Process ACTION_BUTTON_RELEASE
+ val buttonReleaseEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_RELEASE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .actionButton(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ val releaseResult = processor.processInputEventForCompatibility(buttonReleaseEvent)
+ assertThat(releaseResult, hasSize(0))
+
+ // Process ACTION_UP
+ val mouseUpEvent = MotionEventBuilder(MotionEvent.ACTION_UP, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .build()
+ val upResult = processor.processInputEventForCompatibility(mouseUpEvent)
+
+ assertThat(upResult, hasSize(1))
+ assertThat(
+ upResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_UP),
+ withSource(InputDevice.SOURCE_TOUCHSCREEN),
+ withToolType(MotionEvent.TOOL_TYPE_FINGER),
+ withCoords(PointF(110f, 220f)),
+ withButtonState(0),
+ withActionButton(0),
+ )
+ )
+ }
+
+ @Test
+ fun processInputEventForCompatibilityPreservesHistory() {
+ val pointer = PointerBuilder(0, MotionEvent.TOOL_TYPE_MOUSE)
+
+ val mouseDownEvent = MotionEventBuilder(MotionEvent.ACTION_DOWN, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer.x(100f).y(200f))
+ .buttonState(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ processor.processInputEventForCompatibility(mouseDownEvent)
+
+ // Process ACTION_MOVE with history
+ val mouseMoveEvent =
+ MotionEventBuilder(MotionEvent.ACTION_MOVE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer.x(110f).y(220f))
+ .buttonState(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ mouseMoveEvent.addBatch(
+ System.currentTimeMillis(),
+ arrayOf(pointer.x(115f).y(230f).buildCoords()),
+ 0
+ )
+ mouseMoveEvent.addBatch(
+ System.currentTimeMillis(),
+ arrayOf(pointer.x(120f).y(240f).buildCoords()),
+ 0
+ )
+ val result = processor.processInputEventForCompatibility(mouseMoveEvent)
+
+ assertThat(result, hasSize(1))
+ assertThat(
+ result!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_MOVE),
+ withHistorySize(2),
+ withCoordsForHistoryPos(0, PointF(110f, 220f)),
+ withCoordsForHistoryPos(1, PointF(115f, 230f)),
+ withCoords(PointF(120f, 240f)),
+ )
+ )
+ }
+
+ @Test
+ fun processInputEventForCompatibilityConvertsTouchpadSynthesizedPinch() {
+ val pointer0 = PointerBuilder(0, MotionEvent.TOOL_TYPE_FINGER).x(750f).y(530f)
+ val pointer1 = PointerBuilder(1, MotionEvent.TOOL_TYPE_FINGER).x(1000f).y(530f)
+
+ // Process ACTION_DOWN for pointer0 with classification PINCH
+ val downEvent =
+ MotionEventBuilder(MotionEvent.ACTION_DOWN, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer0)
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build()
+ val downResult = processor.processInputEventForCompatibility(downEvent)
+
+ assertThat(downResult, hasSize(1))
+ assertThat(
+ downResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_DOWN),
+ withSource(InputDevice.SOURCE_TOUCHSCREEN),
+ withToolType(MotionEvent.TOOL_TYPE_FINGER),
+ withPointerCount(1),
+ withCoords(PointF(750f, 530f)),
+ withClassification(MotionEvent.CLASSIFICATION_PINCH),
+ )
+ )
+
+ // Process ACTION_POINTER_DOWN for pointer1
+ val pointerDownEvent =
+ MotionEventBuilder(
+ MotionEvent.ACTION_POINTER_DOWN or (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+ InputDevice.SOURCE_MOUSE
+ )
+ .pointer(pointer0)
+ .pointer(pointer1)
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build()
+ val pointerDownResult = processor.processInputEventForCompatibility(pointerDownEvent)
+
+ assertThat(pointerDownResult, hasSize(1))
+ assertThat(
+ pointerDownResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_POINTER_DOWN, 1),
+ withSource(InputDevice.SOURCE_TOUCHSCREEN),
+ withToolType(MotionEvent.TOOL_TYPE_FINGER),
+ withPointerCount(2),
+ withCoordsForPointerIndex(0, PointF(750f, 530f)),
+ withCoordsForPointerIndex(1, PointF(1000f, 530f)),
+ withClassification(MotionEvent.CLASSIFICATION_PINCH),
+ )
+ )
+
+ pointer0.x(700f) // Pinch out
+ pointer1.x(1050f)
+
+ // Process ACTION_MOVE
+ val moveEvent =
+ MotionEventBuilder(MotionEvent.ACTION_MOVE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer0)
+ .pointer(pointer1)
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build()
+ val moveResult = processor.processInputEventForCompatibility(moveEvent)
+
+ assertThat(moveResult, hasSize(1))
+ assertThat(
+ moveResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_MOVE),
+ withSource(InputDevice.SOURCE_TOUCHSCREEN),
+ withToolType(MotionEvent.TOOL_TYPE_FINGER),
+ withPointerCount(2),
+ withCoordsForPointerIndex(0, PointF(700f, 530f)),
+ withCoordsForPointerIndex(1, PointF(1050f, 530f)),
+ withClassification(MotionEvent.CLASSIFICATION_PINCH),
+ )
+ )
+
+ // Process ACTION_POINTER_UP for pointer1
+ val pointerUpEvent =
+ MotionEventBuilder(
+ MotionEvent.ACTION_POINTER_UP or (1 shl MotionEvent.ACTION_POINTER_INDEX_SHIFT),
+ InputDevice.SOURCE_MOUSE
+ )
+ .pointer(pointer0)
+ .pointer(pointer1)
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build()
+ val pointerUpResult = processor.processInputEventForCompatibility(pointerUpEvent)
+
+ assertThat(pointerUpResult, hasSize(1))
+ assertThat(
+ pointerUpResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_POINTER_UP, 1),
+ withSource(InputDevice.SOURCE_TOUCHSCREEN),
+ withToolType(MotionEvent.TOOL_TYPE_FINGER),
+ withPointerCount(2),
+ withClassification(MotionEvent.CLASSIFICATION_PINCH),
+ )
+ )
+
+ // Process ACTION_UP for pointer0
+ val upEvent = MotionEventBuilder(MotionEvent.ACTION_UP, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer0)
+ .classification(MotionEvent.CLASSIFICATION_PINCH)
+ .build()
+ val upResult = processor.processInputEventForCompatibility(upEvent)
+
+ assertThat(upResult, hasSize(1))
+ assertThat(
+ upResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_UP),
+ withSource(InputDevice.SOURCE_TOUCHSCREEN),
+ withToolType(MotionEvent.TOOL_TYPE_FINGER),
+ withPointerCount(1),
+ withClassification(MotionEvent.CLASSIFICATION_PINCH),
+ )
+ )
+ }
+
+ @Test
+ fun processInputEventForCompatibilityConvertsMouseCancel() {
+ val mouseDownEvent = MotionEventBuilder(MotionEvent.ACTION_DOWN, InputDevice.SOURCE_MOUSE)
+ .pointer(PointerBuilder(0, MotionEvent.TOOL_TYPE_MOUSE).x(100f).y(200f))
+ .buttonState(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ processor.processInputEventForCompatibility(mouseDownEvent)
+
+ // Process ACTION_CANCEL
+ val mouseCancelEvent =
+ MotionEventBuilder(MotionEvent.ACTION_CANCEL, InputDevice.SOURCE_MOUSE)
+ .pointer(PointerBuilder(0, MotionEvent.TOOL_TYPE_MOUSE).x(110f).y(220f))
+ .buttonState(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ val result = processor.processInputEventForCompatibility(mouseCancelEvent)
+
+ assertThat(result, hasSize(1))
+ assertThat(
+ result!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_CANCEL),
+ withSource(InputDevice.SOURCE_TOUCHSCREEN),
+ withToolType(MotionEvent.TOOL_TYPE_FINGER),
+ withCoords(PointF(110f, 220f)),
+ withButtonState(0),
+ withActionButton(0),
+ )
+ )
+ }
+
+ @Test
+ fun processInputEventForCompatibilityDoesNotConvertDuringSecondaryClick() {
+ val pointer = PointerBuilder(0, MotionEvent.TOOL_TYPE_MOUSE).x(100f).y(200f)
+
+ // Process ACTION_DOWN
+ val secondaryDownEvent =
+ MotionEventBuilder(MotionEvent.ACTION_DOWN, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_SECONDARY)
+ .build()
+ val resultDown = processor.processInputEventForCompatibility(secondaryDownEvent)
+ assertThat(resultDown, nullValue())
+
+ // Process ACTION_BUTTON_PRESS
+ val buttonPressEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_PRESS, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_SECONDARY)
+ .actionButton(MotionEvent.BUTTON_SECONDARY)
+ .build()
+ val pressResult = processor.processInputEventForCompatibility(buttonPressEvent)
+ assertThat(pressResult, nullValue())
+
+ // Process ACTION_MOVE
+ pointer.x(110f).y(220f)
+ val mouseMoveEvent =
+ MotionEventBuilder(MotionEvent.ACTION_MOVE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_SECONDARY)
+ .build()
+ val moveResult = processor.processInputEventForCompatibility(mouseMoveEvent)
+ assertThat(moveResult, nullValue())
+
+ // Process ACTION_BUTTON_RELEASE
+ val buttonReleaseEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_RELEASE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .actionButton(MotionEvent.BUTTON_SECONDARY)
+ .build()
+ val releaseResult = processor.processInputEventForCompatibility(buttonReleaseEvent)
+ assertThat(releaseResult, nullValue())
+
+ // Process ACTION_UP
+ val mouseUpEvent = MotionEventBuilder(MotionEvent.ACTION_UP, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .build()
+ val upResult = processor.processInputEventForCompatibility(mouseUpEvent)
+ assertThat(upResult, nullValue())
+ }
+
+ @Test
+ fun processInputEventForCompatibilityConsumesBackButtonEvents() {
+ val pointer = PointerBuilder(0, MotionEvent.TOOL_TYPE_MOUSE).x(100f).y(200f)
+
+ // Process ACTION_BUTTON_PRESS
+ val buttonPressEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_PRESS, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_BACK)
+ .actionButton(MotionEvent.BUTTON_BACK)
+ .build()
+ val pressResult = processor.processInputEventForCompatibility(buttonPressEvent)
+ assertThat(pressResult, hasSize(0))
+
+ // Process ACTION_MOVE with BACK button
+ pointer.x(110f).y(220f)
+ val mouseMoveEvent =
+ MotionEventBuilder(MotionEvent.ACTION_HOVER_MOVE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_BACK)
+ .build()
+ val hoverMoveResult = processor.processInputEventForCompatibility(mouseMoveEvent)
+ assertThat(hoverMoveResult, hasSize(1))
+ assertThat(
+ hoverMoveResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_HOVER_MOVE),
+ withSource(InputDevice.SOURCE_MOUSE),
+ withToolType(MotionEvent.TOOL_TYPE_MOUSE),
+ withButtonState(0),
+ withActionButton(0),
+ )
+ )
+
+ // Process ACTION_BUTTON_RELEASE
+ val buttonReleaseEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_RELEASE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .actionButton(MotionEvent.BUTTON_BACK)
+ .build()
+ val releaseResult = processor.processInputEventForCompatibility(buttonReleaseEvent)
+ assertThat(releaseResult, hasSize(0))
+ }
+
+ @Test
+ fun processInputEventForCompatibilityConvertsMouseEventsWhileForwardPressed() {
+ val pointer = PointerBuilder(0, MotionEvent.TOOL_TYPE_MOUSE).x(100f).y(200f)
+
+ // Process ACTION_BUTTON_PRESS
+ val forwardButtonPressEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_PRESS, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_FORWARD)
+ .actionButton(MotionEvent.BUTTON_FORWARD)
+ .build()
+ processor.processInputEventForCompatibility(forwardButtonPressEvent)
+
+ // Process ACTION_DOWN with FORWARD pressed
+ val mouseDownEvent = MotionEventBuilder(MotionEvent.ACTION_DOWN, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_PRIMARY or MotionEvent.BUTTON_FORWARD)
+ .build()
+ val downResult = processor.processInputEventForCompatibility(mouseDownEvent)
+
+ assertThat(downResult, hasSize(1))
+ assertThat(
+ downResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_DOWN),
+ withButtonState(0),
+ withActionButton(0),
+ )
+ )
+
+ // Process ACTION_BUTTON_PRESS with FORWARD pressed
+ val buttonPressEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_PRESS, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_PRIMARY or MotionEvent.BUTTON_FORWARD)
+ .actionButton(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ val pressResult = processor.processInputEventForCompatibility(buttonPressEvent)
+ assertThat(pressResult, hasSize(0))
+
+ pointer.x(110f).y(220f)
+ // Process ACTION_MOVE with FORWARD pressed
+ val mouseMoveEvent =
+ MotionEventBuilder(MotionEvent.ACTION_MOVE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_PRIMARY or MotionEvent.BUTTON_FORWARD)
+ .build()
+ val moveResult = processor.processInputEventForCompatibility(mouseMoveEvent)
+
+ assertThat(moveResult, hasSize(1))
+ assertThat(
+ moveResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_MOVE),
+ withButtonState(0),
+ withActionButton(0),
+ )
+ )
+
+ // Process ACTION_BUTTON_RELEASE with FORWARD pressed
+ val buttonReleaseEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_RELEASE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_FORWARD)
+ .actionButton(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ val releaseResult = processor.processInputEventForCompatibility(buttonReleaseEvent)
+ assertThat(releaseResult, hasSize(0))
+
+ // Process ACTION_UP with FORWARD pressed
+ val mouseUpEvent = MotionEventBuilder(MotionEvent.ACTION_UP, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_FORWARD)
+ .build()
+ val upResult = processor.processInputEventForCompatibility(mouseUpEvent)
+
+ assertThat(upResult, hasSize(1))
+ assertThat(
+ upResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_UP),
+ withButtonState(0),
+ withActionButton(0),
+ )
+ )
+
+ // Process ACTION_BUTTON_RELEASE
+ val forwardButtonReleaseEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_RELEASE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .actionButton(MotionEvent.BUTTON_FORWARD)
+ .build()
+ processor.processInputEventForCompatibility(forwardButtonReleaseEvent)
+ }
+
+ @Test
+ fun processInputEventForCompatibilityConvertsMousePrimaryButtonEventsThenSecondary() {
+ val pointer = PointerBuilder(0, MotionEvent.TOOL_TYPE_MOUSE).x(100f).y(200f)
+
+ // Process ACTION_DOWN
+ val mouseDownEvent = MotionEventBuilder(MotionEvent.ACTION_DOWN, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ val downResult = processor.processInputEventForCompatibility(mouseDownEvent)
+ assertThat(downResult, hasSize(1))
+
+ // Process ACTION_BUTTON_PRESS
+ val buttonPressEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_PRESS, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_PRIMARY)
+ .actionButton(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ val pressResult = processor.processInputEventForCompatibility(buttonPressEvent)
+ assertThat(pressResult, hasSize(0))
+
+ // Process ACTION_BUTTON_PRESS of the secondary button
+ val secondaryPressEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_PRESS, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_PRIMARY or MotionEvent.BUTTON_SECONDARY)
+ .actionButton(MotionEvent.BUTTON_SECONDARY)
+ .build()
+ val secondaryPressResult = processor.processInputEventForCompatibility(secondaryPressEvent)
+ assertThat(secondaryPressResult, hasSize(0))
+
+ // Process ACTION_MOVE with FORWARD pressed
+ val mouseMoveEvent =
+ MotionEventBuilder(MotionEvent.ACTION_MOVE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_PRIMARY or MotionEvent.BUTTON_FORWARD)
+ .build()
+ val moveResult = processor.processInputEventForCompatibility(mouseMoveEvent)
+ assertThat(moveResult, hasSize(1))
+ assertThat(
+ moveResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_MOVE),
+ withButtonState(0),
+ )
+ )
+ // Process ACTION_BUTTON_RELEASE of the primary button
+ val releasePrimaryEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_RELEASE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_SECONDARY)
+ .actionButton(MotionEvent.BUTTON_PRIMARY)
+ .build()
+ val releasePrimaryResult = processor.processInputEventForCompatibility(releasePrimaryEvent)
+ assertThat(releasePrimaryResult, hasSize(0))
+
+ // Process ACTION_MOVE with FORWARD pressed
+ val mouseMoveEvent2 =
+ MotionEventBuilder(MotionEvent.ACTION_MOVE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .buttonState(MotionEvent.BUTTON_PRIMARY or MotionEvent.BUTTON_FORWARD)
+ .build()
+ val moveResult2 = processor.processInputEventForCompatibility(mouseMoveEvent2)
+ assertThat(
+ moveResult2!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_MOVE),
+ withButtonState(0),
+ )
+ )
+
+ // Process ACTION_BUTTON_RELEASE of the secondary button
+ val releaseSecondaryEvent =
+ MotionEventBuilder(MotionEvent.ACTION_BUTTON_RELEASE, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .actionButton(MotionEvent.BUTTON_SECONDARY)
+ .build()
+ val releaseSecondaryResult =
+ processor.processInputEventForCompatibility(releaseSecondaryEvent)
+ assertThat(releaseSecondaryResult, hasSize(0))
+
+ // Process ACTION_UP
+ val mouseUpEvent = MotionEventBuilder(MotionEvent.ACTION_UP, InputDevice.SOURCE_MOUSE)
+ .pointer(pointer)
+ .build()
+ val upResult = processor.processInputEventForCompatibility(mouseUpEvent)
+ assertThat(upResult, hasSize(1))
+ assertThat(
+ upResult!![0] as MotionEvent, allOf(
+ withMotionAction(MotionEvent.ACTION_UP),
+ withButtonState(0),
+ withActionButton(0),
+ )
+ )
+ }
}