Add multi-device checks to AccessibilityInputFilterInputTest

Prior to this CL, AccessibilityInputFilter assumed that only a single
input device could be active at a time. However, after the recent
multi-device input feature, that assumption no longer holds.

As a result, InputFilter is being sent multi-device streams from the
InputDispatcher. This is problematic, because InputFilter gets confused
and start processing the events from all devices at the same time.
Components like TouchExplorer don't look at the device id, and quickly
get into an inconsistent state.

This causes some events to be dropped by InputFilter, and some to be
sent back to the InputDispatcher. In some situations, this leads to an
inconsistent input stream being injected back into dispatcher, which
could lead to a crash.

In this CL, we add a multiplexer to AccessibilityInputFilter so that
only one input device can be active at a time. All MotionEvents from
other devices are going to be ignored.

Multi-device behaviour of AccessibilityInputFilter is also being tested
in this CL, for the cases where
none of the a11y features are on, and where all of the a11y features are
on.

Bug: 310014874
Test: atest FrameworksServicesTests:AccessibilityInputFilterInputTest
Change-Id: Iac18fd671787d880aca1f6fbfd17d822e5ae8ee6
diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig
index 015c35e..7c48550 100644
--- a/services/accessibility/accessibility.aconfig
+++ b/services/accessibility/accessibility.aconfig
@@ -66,6 +66,16 @@
 }
 
 flag {
+    name: "handle_multi_device_input"
+    namespace: "accessibility"
+    description: "Select a single active device when a multi-device stream is received by AccessibilityInputFilter"
+    bug: "310014874"
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
+
+flag {
     name: "pinch_zoom_zero_min_span"
     namespace: "accessibility"
     description: "Whether to set min span of ScaleGestureDetector to zero."
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
index abcd8e2..16119d11 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java
@@ -25,6 +25,7 @@
 import android.content.Context;
 import android.graphics.Region;
 import android.os.PowerManager;
+import android.os.SystemClock;
 import android.provider.Settings;
 import android.util.Slog;
 import android.util.SparseArray;
@@ -35,6 +36,8 @@
 import android.view.InputFilter;
 import android.view.KeyEvent;
 import android.view.MotionEvent;
+import android.view.MotionEvent.PointerCoords;
+import android.view.MotionEvent.PointerProperties;
 import android.view.accessibility.AccessibilityEvent;
 
 import com.android.server.LocalServices;
@@ -203,6 +206,62 @@
 
     private EventStreamState mKeyboardStreamState;
 
+    /**
+     * The last MotionEvent emitted from the input device that's currently active. This is used to
+     * keep track of which input device is currently active, and also to generate the cancel event
+     * if a new device becomes active.
+     */
+    private MotionEvent mLastActiveDeviceMotionEvent = null;
+
+    private static MotionEvent cancelMotion(MotionEvent event) {
+        if (event.getActionMasked() == MotionEvent.ACTION_CANCEL
+                || event.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT
+                || event.getActionMasked() == MotionEvent.ACTION_UP) {
+            throw new IllegalArgumentException("Can't cancel " + event);
+        }
+        final int action;
+        if (event.getActionMasked() == MotionEvent.ACTION_HOVER_ENTER
+                || event.getActionMasked() == MotionEvent.ACTION_HOVER_MOVE) {
+            action = MotionEvent.ACTION_HOVER_EXIT;
+        } else {
+            action = MotionEvent.ACTION_CANCEL;
+        }
+
+        final int pointerCount;
+        if (event.getActionMasked() == MotionEvent.ACTION_POINTER_UP) {
+            pointerCount = event.getPointerCount() - 1;
+        } else {
+            pointerCount = event.getPointerCount();
+        }
+        final PointerProperties[] properties = new PointerProperties[pointerCount];
+        final PointerCoords[] coords = new PointerCoords[pointerCount];
+        int newPointerIndex = 0;
+        for (int i = 0; i < event.getPointerCount(); i++) {
+            if (event.getActionMasked() == MotionEvent.ACTION_POINTER_UP) {
+                if (event.getActionIndex() == i) {
+                    // Skip the pointer that's going away
+                    continue;
+                }
+            }
+            final PointerCoords c = new PointerCoords();
+            c.x = event.getX(i);
+            c.y = event.getY(i);
+            coords[newPointerIndex] = c;
+            final PointerProperties p = new PointerProperties();
+            p.id = event.getPointerId(i);
+            p.toolType = event.getToolType(i);
+            properties[newPointerIndex] = p;
+            newPointerIndex++;
+        }
+
+        return MotionEvent.obtain(event.getDownTime(), SystemClock.uptimeMillis(), action,
+                pointerCount, properties, coords,
+                event.getMetaState(), event.getButtonState(),
+                event.getXPrecision(), event.getYPrecision(), event.getDeviceId(),
+                event.getEdgeFlags(), event.getSource(), event.getDisplayId(), event.getFlags(),
+                event.getClassification());
+    }
+
     AccessibilityInputFilter(Context context, AccessibilityManagerService service) {
         this(context, service, new SparseArray<>(0));
     }
@@ -260,6 +319,17 @@
                     AccessibilityTrace.FLAGS_INPUT_FILTER,
                     "event=" + event + ";policyFlags=" + policyFlags);
         }
+        if (Flags.handleMultiDeviceInput()) {
+            if (!shouldProcessMultiDeviceEvent(event, policyFlags)) {
+                // We are only allowing a single device to be active at a time.
+                return;
+            }
+        }
+
+        onInputEventInternal(event, policyFlags);
+    }
+
+    private void onInputEventInternal(InputEvent event, int policyFlags) {
         if (mEventHandler.size() == 0) {
             if (DEBUG) Slog.d(TAG, "No mEventHandler for event " + event);
             super.onInputEvent(event, policyFlags);
@@ -353,6 +423,63 @@
         }
     }
 
+    boolean shouldProcessMultiDeviceEvent(InputEvent event, int policyFlags) {
+        if (event instanceof MotionEvent motion) {
+            // Only allow 1 device to be sending motion events at a time
+            // If the event is from an active device, let it through.
+            // If the event is not from an active device, only let it through if it starts a new
+            // gesture like ACTION_DOWN or ACTION_HOVER_ENTER
+            final boolean eventIsFromCurrentDevice = mLastActiveDeviceMotionEvent != null
+                    && mLastActiveDeviceMotionEvent.getDeviceId() == motion.getDeviceId();
+            final int actionMasked = motion.getActionMasked();
+            switch (actionMasked) {
+                case MotionEvent.ACTION_DOWN:
+                case MotionEvent.ACTION_HOVER_ENTER:
+                case MotionEvent.ACTION_HOVER_MOVE: {
+                    if (mLastActiveDeviceMotionEvent != null
+                            && mLastActiveDeviceMotionEvent.getDeviceId() != motion.getDeviceId()) {
+                        // This is a new gesture from a new device. Cancel the existing state
+                        // and let this through
+                        MotionEvent canceled = cancelMotion(mLastActiveDeviceMotionEvent);
+                        onInputEventInternal(canceled, policyFlags);
+                    }
+                    mLastActiveDeviceMotionEvent = MotionEvent.obtain(motion);
+                    return true;
+                }
+                case MotionEvent.ACTION_MOVE:
+                case MotionEvent.ACTION_POINTER_DOWN:
+                case MotionEvent.ACTION_POINTER_UP: {
+                    if (eventIsFromCurrentDevice) {
+                        mLastActiveDeviceMotionEvent = MotionEvent.obtain(motion);
+                        return true;
+                    } else {
+                        return false;
+                    }
+                }
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                case MotionEvent.ACTION_HOVER_EXIT: {
+                    if (eventIsFromCurrentDevice) {
+                        // This is the last event of the gesture from this device.
+                        mLastActiveDeviceMotionEvent = null;
+                        return true;
+                    } else {
+                        // Event is from another device
+                        return false;
+                    }
+                }
+                default: {
+                    if (mLastActiveDeviceMotionEvent != null
+                            && event.getDeviceId() != mLastActiveDeviceMotionEvent.getDeviceId()) {
+                        // This is an event from another device, ignore it.
+                        return false;
+                    }
+                }
+            }
+        }
+        return true;
+    }
+
     private void processMotionEvent(EventStreamState state, MotionEvent event, int policyFlags) {
         if (!state.shouldProcessScroll() && event.getActionMasked() == MotionEvent.ACTION_SCROLL) {
             super.onInputEvent(event, policyFlags);
diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java b/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java
index b6223c7..bf9202f1b 100644
--- a/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java
+++ b/services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java
@@ -106,11 +106,30 @@
                 return;
             }
         }
+        final long downTime;
         if (action == MotionEvent.ACTION_DOWN) {
-            event.setDownTime(event.getEventTime());
+            downTime = event.getEventTime();
         } else {
-            event.setDownTime(mState.getLastInjectedDownEventTime());
+            downTime = mState.getLastInjectedDownEventTime();
         }
+
+        // The only way to change device id of the motion event is by re-creating the whole thing
+        final PointerProperties[] properties = new PointerProperties[event.getPointerCount()];
+        final PointerCoords[] coords = new PointerCoords[event.getPointerCount()];
+        for (int i = 0; i < event.getPointerCount(); i++) {
+            final PointerCoords c = new PointerCoords();
+            event.getPointerCoords(i, c);
+            coords[i] = c;
+            final PointerProperties p = new PointerProperties();
+            event.getPointerProperties(i, p);
+            properties[i] = p;
+        }
+        event = MotionEvent.obtain(downTime, event.getEventTime(), event.getAction(),
+                event.getPointerCount(), properties, coords,
+                event.getMetaState(), event.getButtonState(),
+                event.getXPrecision(), event.getYPrecision(), rawEvent.getDeviceId(),
+                event.getEdgeFlags(), rawEvent.getSource(), event.getDisplayId(), event.getFlags(),
+                event.getClassification());
         // If the user is long pressing but the long pressing pointer
         // was not exactly over the accessibility focused item we need
         // to remap the location of that pointer so the user does not