Skip to content

Two-finger gestures on a physical Android phone leave both pointers stuck down #1645

Open

Description

@tomekzaw

Symptom. After one gesture-pinch or gesture-rotate on a physical Android 14+ phone, the phone ignores every later touch from Argent, even after restarting the app. Only adb reboot recovers it. Seen on a Samsung Galaxy A16 (SM-A165F, Android 16) with argent 0.30.1. Android emulators are not affected because they inject through the virtual touchscreen.

Cause. The android_device controller in simulator-server ends a two-finger gesture with a single ACTION_UP that still carries both pointers. Android requires a two-pointer gesture to end with ACTION_POINTER_UP for the leaving finger (action index bits set, both pointers present), followed by a one-pointer ACTION_UP. The input dispatcher's verifier rejects the malformed event, so the injected pointers stay down, and every subsequent ACTION_DOWN is refused as "pointers already down".

The start of the gesture is already encoded correctly (ACTION_DOWN, then ACTION_POINTER_DOWN(1), then two-pointer ACTION_MOVEs). There is also no way to lift a single finger from the tool-server side: a touch command with type: "Up" or "Move" and no second point becomes a one-pointer ACTION_UP or ACTION_MOVE, which is rejected the same way. ACTION_POINTER_UP is never emitted, so this cannot be worked around in gesture-pinch / gesture-rotate / gesture-custom.

Reproduce.

  1. Connect an Android 14+ phone over USB (a Samsung on Android 16 is a reliable choice) and note its serial.
  2. Run one pinch:
    gesture-pinch { "udid": "<serial>", "centerX": 0.5, "centerY": 0.5, "startDistance": 0.2, "endDistance": 0.6 }
    
  3. Check the dispatcher state. Both pointers are still down:
    adb shell dumpsys input | grep -A6 TouchStatesByDisplay
    #   DeviceId(-1):[touchingPointers=[Pointer(id=0, FINGER), Pointer(id=1, FINGER)], ...
    
  4. The agent's own log shows the rejected event:
    adb logcat -d | grep -E "Unable to inject|Inconsistent event"
    # E/studio.screen.sharing: Unable to inject an input event MotionEvent { action=ACTION_UP, ... id[0]=0, ... id[1]=1, ... pointerCount=2
    
  5. Any gesture-tap now fails with Inconsistent event: ... action=DOWN ... reason: Invalid DOWN event - pointers already down.

To see every injected event, not just the rejected ones, enable the dispatcher's inbound log and reboot once before step 2:

adb shell setprop persist.log.tag.InputDispatcherInboundEvent DEBUG && adb reboot
adb logcat -d | grep injectInputEvent

Fix. On a touch command with type: "Up" and a second point, send two events: ACTION_POINTER_UP with action index 1 and both pointers at their last positions, then ACTION_UP with pointer 0 alone, and reset the second-pointer bookkeeping afterwards. It would also be worth handling a Move or Up that drops the second point the same way, so a custom gesture can lift one finger at a time.

Also seen while reproducing. The very first touch command after the screen-sharing agent starts on a phone is silently dropped: the tool returns success but the dispatcher logs no injected events. It looks like events are sent before the agent is ready to inject. Probably worth a separate issue.

Activity

  1. camchis commented on Oct 28, 2026

    @camchis
    Contributor

    Reproduced on a second device, and a few details that may help with the fix.

    Device: Solana Seeker (Android 16, SDK 36, Mali), Argent 0.31.0, USB. The report was on a Samsung A16 with 0.30.1, so this is not Samsung-specific.

    Repro: one gesture-pinch (centre 0.5/0.5, 0.1 to 0.5, default duration). The tool returns pinched: true, and dumpsys input then shows both pointers still down on the app window and on the gesture monitors:

    touchingPointers=[Pointer(id=0, FINGER), Pointer(id=1, FINGER)]
    

    What it does to later input:

    • The next gesture-tap returns tapped: true but is dropped. The app stayed on the same screen.
    • The pointer position of the stuck pinch is left with a pressed highlight on whatever view it landed on.
    • The state survives argent server stop, so it belongs to the dispatcher and not to a live agent process.

    Why shell input cannot clear it: adb shell input motionevent CANCEL and UP inject as deviceId=4, while the stuck pointers belong to the agent's virtual device. The dispatcher refuses both:

    Injection on 0: Got ACTION_CANCEL, but the pointers don't match. Existing pointers: {DeviceId(-1): {0, 1}}
    Injection on 0: Got ACTION_UP, but we have pointers: {0, 1} for device DeviceId(-1)
    

    So nothing outside the agent's own injector can recover it, which matches the reporter's finding that only a reboot does.

    I did not reproduce the first-touch-dropped bug (#1627 / #1646) in this order: the first tap landed, but a screenshot had already started the screen-sharing agent. Possibly relevant to those issues.

    One untested thought for the fix: besides ending a two-finger gesture with ACTION_POINTER_UP then ACTION_UP, an ACTION_CANCEL sent from the same virtual device would be a recovery path for a session that is already stuck. I have not tried it, since I cannot inject from that device.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions