Loading services/accessibility/accessibility.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -72,6 +72,16 @@ flag { bug: "319175022" } 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" Loading services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +127 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import android.annotation.NonNull; 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; Loading @@ -35,6 +36,8 @@ import android.view.InputEvent; 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; Loading Loading @@ -203,6 +206,62 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo 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)); } Loading Loading @@ -260,6 +319,17 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo 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); Loading Loading @@ -353,6 +423,63 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo } } 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); Loading services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java +21 −2 Original line number Diff line number Diff line Loading @@ -106,11 +106,30 @@ class EventDispatcher { 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 Loading services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt +265 −52 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
services/accessibility/accessibility.aconfig +10 −0 Original line number Diff line number Diff line Loading @@ -72,6 +72,16 @@ flag { bug: "319175022" } 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" Loading
services/accessibility/java/com/android/server/accessibility/AccessibilityInputFilter.java +127 −0 Original line number Diff line number Diff line Loading @@ -25,6 +25,7 @@ import android.annotation.NonNull; 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; Loading @@ -35,6 +36,8 @@ import android.view.InputEvent; 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; Loading Loading @@ -203,6 +206,62 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo 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)); } Loading Loading @@ -260,6 +319,17 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo 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); Loading Loading @@ -353,6 +423,63 @@ class AccessibilityInputFilter extends InputFilter implements EventStreamTransfo } } 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); Loading
services/accessibility/java/com/android/server/accessibility/gestures/EventDispatcher.java +21 −2 Original line number Diff line number Diff line Loading @@ -106,11 +106,30 @@ class EventDispatcher { 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 Loading
services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt +265 −52 File changed.Preview size limit exceeded, changes collapsed. Show changes