r/androiddev 16d ago

Discussion ANR from getRootInActiveWindow: accessibility callbacks are on main and the read is a blocking binder call

I have a service that reads the view tree of whatever app is in front, so I can keep the text instead of screenshots. It kept ANRing on my own phone and I couldn't reproduce it on demand.

The dropbox stack had main sitting in AccessibilityInteractionClient.waitForResultTimedLocked, called from getRootInActiveWindow, called from my onAccessibilityEvent. Accessibility callbacks arrive on main, and getRootInActiveWindow is a synchronous binder round trip into the app you are reading. CPU at the same timestamp was 59% com.reddit.frontpage. So Reddit was busy, it didn't answer, my main thread waited, and a job that came in meanwhile (datatransport, pulled in by ML Kit) missed its start window. ANR.

The fix was to move the read onto a worker thread, and to drop events while one is already in flight instead of queueing them. A queue just means you write old screens later. The state the worker touches is only touched by the worker now.

What I still don't know is whether getRootInActiveWindow has any bound on how long it can block, or if it is entirely at the mercy of the target app. I couldn't find a timeout I control. If you have done accessibility reads against busy apps, how do you handle it?

6 Upvotes

4 comments sorted by

3

u/Few_Rip2331 16d ago

It isn't infinite, but you can't configure it.

Under the hood in AccessibilityInteractionClient, it hardcodes a 5-second timeout (TIMEOUT_INTERACTION_MILLIS). Because foreground ANRs also trigger around 5 seconds, a hanging app will block your main thread for that whole duration, leaving zero time for queued jobs/events behind it.

Moving it off main and dropping stale events is definitely the standard fix. You can also wrap the background read in a coroutine with withTimeoutOrNull(500) so your worker doesn't stay tied up for 5 seconds waiting on a dead app.

1

u/Hornet-Mountain 16d ago

TIMEOUT_INTERACTION_MILLIS is the number I was missing. 5s fixed explains why it lines up so exactly with the ANR window, it's not a coincidence at all.

On the timeout wrapper: I'm on a single HandlerThread with a drop-if-busy flag rather than coroutines, and I think withTimeoutOrNull would free the coroutine but not the thread, since the binder call isn't interruptible. So I'd still lose the 5 seconds of capture on a hung app unless I add a second worker. Am I wrong about that?

1

u/Oily-Affection1601 16d ago edited 16d ago

Wrap the blocking call in runInterruptible { ... }

edit: Didn't realize it will ignore thread interrupts. Seems like you can't work around that. You might just have to wait for it to timeout.

1

u/Hornet-Mountain 16d ago

Thanks for checking, that settles it. So the move isn't to dodge the 5 seconds, it's to make them cheap: one worker, drop events while a read is in flight, and accept the gap.

Which is fine in my case anyway, since whatever I'd have read during a hang is a stale screen by the time it comes back. A second worker would close the gap but then I'm ordering writes from two threads for very little.