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),
+            )
+        )
+    }
 }