Skip to content

Fix touch exploration when Android leaves touches down - #194

Open
aaron-gh wants to merge 2 commits into
trypsynth:masterfrom
aaron-gh:stuck-touch-recovery
Open

aaron-gh wants to merge 2 commits into
trypsynth:masterfrom
aaron-gh:stuck-touch-recovery

Conversation

@aaron-gh

@aaron-gh aaron-gh commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

After some touches with several fingers, Android can stop touch exploration for every screen reader until the phone restarts. Swipes and taps still work, but dragging a finger says nothing. I've had this on a Galaxy Z Fold7 (Android 17) several times. Turning the screen reader off and on doesn't help.

The cause is in Android. TouchExplorer injects touch events for delegated and dragged touches, and InputDispatcher checks every injected event against the pointers it holds as down. After the multi-finger touch, TouchExplorer leaves injected pointers down. The dispatcher keeps them until the display is removed and rejects every hover event touch exploration injects afterwards:

Inconsistent event: MotionEvent { action=HOVER_ENTER, ... deviceId=-1, ... }
reason: Injection on 0: Invalid HOVER_ENTER event - pointers are down for device DeviceId(-1): {DeviceId(-1): {4, 1}}

I've reported it to Google. Until it's fixed, this PR lets Backtalk fix it without a restart.

How it works

  • Backtalk notices touches that Android reports as touch exploration, that last at least 300 ms and hover over nothing.
  • With the DUMP permission, granted once with adb shell pm grant fyi.quin.backtalk android.permission.DUMP, it reads the pointers the dispatcher holds for device -1 from dumpsys input, twice, 300 ms apart.
  • If the same pointers are held both times, it says "Fixing touch exploration", turns explore by touch off, lifts them with a gesture, turns explore by touch back on, checks again and says "Touch exploration fixed". If a touch is in progress, it waits for it to end.
  • Gesture events from dispatchGesture are also device -1, so a gesture can lift the stuck pointers. The injector gives each new stroke the lowest unused pointer id, so the gesture opens one stroke per id up to the highest stuck one, keeps open the ones the dispatcher holds, lifts the others, then lifts the held ones. The dispatcher rejects every event until the open pointers match what it holds, then accepts the lifts. Strokes are outside the display, so no window gets a touch.
  • Without the permission, or when dumpsys input doesn't show the pointers, Backtalk says after three such touches in a row that touch exploration may be stuck and the phone needs a restart.
  • While the DUMP permission is missing, the setting's summary says so and gives the adb command with the installed package name.
  • Advanced settings > Fix stuck touch exploration, on by default. differences.md has a section on it.

Limitations

  • Reading the pointers needs a dumpsys input that prints the injected event verifier ("Filter verifier on display N"), as Samsung's builds do. AOSP doesn't print it, so on other phones Backtalk can only warn.
  • Exploring something with nothing to read, such as blank space or a game, also hovers over nothing. Without the permission that can give a false warning, at most once every 10 minutes. With it, such a touch costs a dumpsys input (about 40 ms), at most every 2 seconds.
  • Explore by touch is off while the gesture runs: 0.34 s in testing, at most 3 seconds. A touch in that time goes straight to the app and cancels the gesture; Backtalk tries again after the next exploring touch.
  • The gesture injector only gives pointer ids up to 10, and Android 8 to 10 allow 10 strokes, so stuck ids above 10 (above 9 on those versions) can't be lifted. Backtalk then says to restart.

Testing

Unit tests replay the gesture plan through models of MotionEventInjector and the dispatcher's verifier for every stuck set of ids 0 to 10, check the stroke limit, and test the dumpsys input parser.

On the Fold7, with the stuck state reproduced by injecting a down from the shell at an off-screen point and never lifting it:

Test Result
Nothing stuck, one-finger exploring drag One check, nothing found, nothing said
Nothing stuck, no DUMP, three two-finger drags Nothing
{4, 1} stuck, two-finger drag No check
{4, 1} stuck, one-finger exploring drag Fixed, explore by touch off 0.34 s
{11} stuck "Touch exploration may be stuck. Restart the phone to fix it.", explore by touch left on
{2} stuck, no DUMP, three exploring drags Warning with the DUMP hint
Backtalk turned off during a fix No crash, nothing ran after shutdown

After some touches with several fingers, Android's touch exploration
can leave pointers it injected down. The input dispatcher then rejects
every hover event that touch exploration injects, for every screen
reader, until the phone restarts. Swipes and taps still work, but
dragging a finger says nothing. Turning the screen reader off and on
doesn't help, because the dispatcher keeps those pointers until the
display is removed.

Backtalk now notices touches that Android reports as touch exploration,
that last at least 300 ms and hover over nothing. With the DUMP
permission, granted once with adb, it reads the pointers that the
dispatcher holds for device -1 from dumpsys input, twice 300 ms apart.
If the same ones are held, it says "Fixing touch exploration", turns
explore by touch off, lifts them with a gesture, turns explore by touch
back on, and reads them again to check. If a touch is in progress, it
waits for the touch to end first.

Gesture events are also device -1, and the injector gives each new
stroke the lowest unused pointer id. The gesture opens a stroke for each
id up to the highest stuck one, keeps open the ones the dispatcher holds
and lifts the others, then lifts the held ones. The dispatcher rejects
every event until the open pointers match, then accepts the lifts.
Strokes are outside the display, so no window gets a touch.

Limitations:
- Reading the pointers needs a dumpsys input that prints the injected
  event verifier, as Samsung's builds do. AOSP doesn't, so on other
  phones, and without the permission, Backtalk only says after three such
  touches in a row that touch exploration may be stuck.
- Exploring something with nothing to read, such as blank space or a
  game, also hovers over nothing. Without the permission that can give a
  false warning, at most once every 10 minutes. With it, each such touch
  costs a dumpsys input, at most every 2 seconds.
- Explore by touch is off while the gesture runs, usually under half a
  second and at most 3 seconds, and a touch then goes straight to the
  app. Such a touch cancels the gesture, and Backtalk tries again after
  the next exploring touch.
- The injector gives pointer ids up to 10 only, and Android 8 to 10
  allow 10 strokes, so stuck ids above 10, or above 9 on those versions,
  can't be lifted. Backtalk then says to restart the phone.

- StuckTouchController watches touches, reads dumpsys input on a
  background thread with a 3 second limit, and runs the fix.
- StuckPointerGesture plans the strokes. InjectedPointers parses
  dumpsys input.
- Advanced settings > Fix stuck touch exploration, on by default.
- The manifest asks for DUMP, which only adb can grant.
- Unit tests replay the plan through models of the gesture injector and
  the dispatcher's verifier for every stuck set of ids 0 to 10, check the
  stroke limit, and test the parser.
@trypsynth

Copy link
Copy Markdown
Owner

Thanks, the Samsung stuck-pointer problem is real and this is a clever way out. A few things before it can go in:

  • Default. pref_fix_stuck_touch_exploration_default is true. On Pixels and other non-Samsung phones, or anywhere DUMP isn't granted (almost everyone), three explores over blank space, a game or an app without accessibility give "Touch exploration may be stuck. Restart the phone", up to every 10 minutes. Please default it off, or at least never warn without DUMP.
  • Going around TouchInteractionMonitor. turnOff/restore call TalkBackService's requestTouchExploration directly, which skips TouchInteractionMonitor.requestA11yTouchExploreState. That one holds the change until the monitor's state is clear. Please go through it.
  • Pass-through regions. Toggling explore by touch rebuilds the input filter, which may drop the regions set by DirectTouchController and PassThroughModeActor until something resends them. Please check on a device with direct touch on, and resend the region after restoring if needed.
  • Taps during the fix. While explore is off (0.34 s, up to 3 s), a tap goes straight to the app and can activate whatever is under the finger. Could the window be kept as short as possible, or touches be ignored during it?
  • Tests. The plan and the parser are covered, but the controller's state machine isn't.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants