Skip to content

Multi Pointer MotionEvent

NanoMindExplores edited this page Jul 14, 2026 · 1 revision

Multi-Pointer MotionEvent (v3)

This page documents the correct Android multi-touch semantics implemented in v3, which fixed the analog returns to center when any other button is pressed bug.

The Bug (v1/v2)

Before v3, TouchDaemonService.injectSinglePointer() always created a MotionEvent with pointerCount=1 and ACTION_DOWN for every pointer transition. This is wrong for multi-touch. When pointer 0 (L_STICK) is already DOWN and pointer 2 (button A) injects ACTION_DOWN with pointerCount=1, Android interprets this as a NEW touch session — the previous session (stick) gets CANCELLED.

Correct Android Multi-Touch Semantics

Action When to use pointerCount
ACTION_DOWN First pointer goes DOWN (starts a new gesture) 1
ACTION_POINTER_DOWN Additional pointer goes DOWN while others active ALL active pointers
ACTION_MOVE Any pointer moves ALL active pointers
ACTION_POINTER_UP Pointer goes UP while others remain ALL active pointers
ACTION_UP Last pointer goes UP (ends the gesture) 1

The actionIndex (encoded in the upper bits of the action integer via ACTION_POINTER_INDEX_SHIFT) specifies WHICH pointer in the properties array is being added/removed.

v3 Implementation

Active Pointer Tracking

  • activePointers: ConcurrentHashMap tracking ALL currently-down pointers (IDs + last known positions)
  • gestureDownTime: set when the FIRST pointer goes DOWN, shared by ALL pointers in the same gesture

touchDown

  1. Add pointer to activePointers map
  2. If this is the first pointer, set gestureDownTime
  3. Call injectMultiPointerEvent with ACTION_DOWN

injectMultiPointerEvent

  1. Snapshot ALL active pointers (sorted by ID)
  2. Find actionIndex (position of target pointer in array)
  3. Determine finalAction:
    • If pointerCount == 1: ACTION_DOWN or ACTION_UP
    • Else: ACTION_POINTER_DOWN or ACTION_POINTER_UP with actionIndex encoded
  4. Build properties + coords for ALL active pointers
  5. Create MotionEvent with full pointer set
  6. Try Path A, then B, then (shell only if single-pointer DOWN/UP)

Why This Fixes the Bug

Before v3 (single-pointer ACTION_DOWN):

  1. L_STICK DOWN: ACTION_DOWN, pointerCount=1, pointer=0. Android starts new session.
  2. Button A DOWN: ACTION_DOWN, pointerCount=1, pointer=2. Android cancels previous session, starts new one. L_STICK touch released.

After v3 (multi-pointer ACTION_POINTER_DOWN):

  1. L_STICK DOWN: ACTION_DOWN, pointerCount=1, pointer=0. Android starts new session.
  2. Button A DOWN: ACTION_POINTER_DOWN, pointerCount=2, pointers=[0,2]. Android adds pointer 2 to existing session. L_STICK touch continues.
  3. Button A UP: ACTION_POINTER_UP, pointerCount=2, actionIndex=1. Android removes pointer 2. L_STICK still active.

Shell Fallback Guard (v3)

Path C (input tap shell command) only supports single-pointer DOWN+UP. If it fires while other pointers are active, it would inject pointer 0 DOWN+UP, hijacking/cancelling the active multi-touch session.

v3 fix: Path C only fires when pointerCount == 1 (no other pointers active).

See Also

  • Injection-Architecture — 3-path injection overview
  • Dual-AIDL-Dispatch-Thread — Why stick and button use separate threads

Clone this wiki locally