All posts

engineering · · 7 min read · Filip Gajić

Why uiautomator dump fails with "could not get idle state" (and 4 ways around it)

What the idle-state error in uiautomator dump actually means, why spinners trigger it, and four fixes with real adb and Appium settings.

If you do Android automation long enough you eventually hit this:

$ adb shell uiautomator dump
ERROR: could not get idle state.

Almost always on a screen with a loading spinner, a shimmer placeholder, a Lottie animation, a marquee, a video, or a blinking cursor in a focused text field. The same screens make Appium's page source (and therefore Appium Inspector's refresh) take ten seconds or more, or time out entirely. I've lost enough hours to this that I want to write down what is actually happening and what works.

What the error actually means

uiautomator dump doesn't just read the accessibility tree. Before it reads anything, it calls UiAutomation.waitForIdle(1000, 10000). In plain terms: wait until there has been no accessibility event for one second, and give up after ten. You can see it in the AOSP source for DumpCommand.java; the TimeoutException from that call is what prints the error, and no XML gets written.

Every animating view fires TYPE_WINDOW_CONTENT_CHANGED on every frame. An indeterminate ProgressBar is a permanent animation. So the quiet second never comes, the ten-second budget runs out, and you get the error.

Appium's UiAutomator2 driver has the same wait, because UiAutomator itself does it. The driver exposes it as the waitForIdleTimeout setting, and as of the current driver docs the default is 10000 ms. The difference is that the driver doesn't error. It silently waits the full timeout, then proceeds with the action or the source fetch. This is why "the inspector is slow on this screen" and "dump times out on this screen" are the same bug wearing two hats. Espresso has a related story on the developer side: its "disable animations before running tests" advice exists because Espresso also waits for the main thread and its idling resources to settle.

Quick sanity check that this is your problem and not ADB: run adb shell uiautomator dump on a static screen like Settings. Instant there and hanging on your screen means it's the idle wait.

Way 1: stop the animation at the source

Best when you can. Developer options, then set Window animation scale, Transition animation scale and Animator duration scale to off. Or from the shell:

adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0

This kills system window and transition animations and most Animator-driven spinners. It does not stop everything. Lottie, video, GIF and WebP, shimmer libraries with their own timers, and custom Canvas or Choreographer loops keep going. Compose animations vary by version in my experience, so test on your app.

In Appium, the capability appium:disableWindowAnimation: true applies the same settings for the session and, as of the current driver docs, restores them afterwards on API 26 and higher.

The best version of this is a build flag or debug menu that swaps indeterminate spinners for a static state in test builds. Espresso teams do the same thing with idling resources. It's a small PR for the developers and it fixes the whole class of problem rather than one screen.

Way 2: shrink the wait (Appium)

waitForIdleTimeout is a session setting rather than a capability (the driver README has a warning box about exactly this), so you change it through the Settings API:

# Python
driver.update_settings({"waitForIdleTimeout": 500})   # ms; 0 = don't wait at all
// WebdriverIO / JS
await driver.updateSettings({ waitForIdleTimeout: 500 });
// Java
((HasSettings) driver).setSetting("waitForIdleTimeout", 500);

If you want it from the first command of the session, Appium lets you initialise any setting through a capability:

{ "appium:settings[waitForIdleTimeout]": 500 }

Trade-off: with a low value you can act before the UI has settled, so pair it with explicit waits for the element you need. The driver docs say the same, and add that a value of 0 disables the wait completely, which is exactly as risky as it sounds. actionAcknowledgmentTimeout (default 3000 ms) is the sibling setting for post-action waits; the docs suggest leaving it alone unless you have a reason.

iOS has the same disease with a different name. The XCUITest driver's waitForIdleTimeout is in seconds (default 10, 0 disables) and animationCoolOffTimeout (default 2 seconds) is the post-action equivalent. Both are settings; the first can also be set as a capability.

A note on --compressed and ignoreUnimportantViews

These get suggested a lot for this error, and they don't help with it. uiautomator dump --compressed drops nodes that aren't important for accessibility, and the UiAutomator2 driver's ignoreUnimportantViews setting does the same thing for page source. Both are good when dump is slow on a huge tree. Neither skips the idle wait. In DumpCommand.java, the compression flag is applied first and waitForIdle runs anyway, so you get a smaller tree ten seconds late, or the same error.

Use them for size. Don't expect them to fix idle.

Way 3: read the hierarchy through a door that doesn't wait

Two options that skip the accessibility idle wait entirely, with caveats:

Android Studio's Layout Inspector attaches to the app process. Debuggable builds only, and it isn't scriptable, but for a one-off "what is on this screen" it works on animated screens.

adb shell dumpsys activity top dumps the foreground activity's view hierarchy: class, id, bounds. No text or content-desc, and the format shifts between Android versions, but it's instant and it's always there.

If you own the app, Espresso or UiAutomator inside an instrumentation test with animations disabled is the reliable in-house route.

Way 4: read the accessibility tree without waitForIdle

The idle wait is something uiautomator dump chooses to do before reading. The tree itself is available at any moment: UiAutomation.getRootInActiveWindow() returns the current tree immediately. A minimal instrumentation, shipped as its own tiny APK, no changes to the app under test:

class TreeDump : Instrumentation() {
    override fun onCreate(args: Bundle?) { super.onCreate(args); start() }
    override fun onStart() {
        walk(uiAutomation.rootInActiveWindow, 0)   // no waitForIdle here
        finish(Activity.RESULT_OK, Bundle())
    }
    private fun walk(n: AccessibilityNodeInfo?, depth: Int) {
        n ?: return
        val r = Rect().also(n::getBoundsInScreen)
        Log.i("dump", " ".repeat(depth) +
            "${n.className} id=${n.viewIdResourceName} text=${n.text} desc=${n.contentDescription} $r")
        for (i in 0 until n.childCount) walk(n.getChild(i), depth + 1)
    }
}

Register it in the helper's manifest:

<instrumentation
    android:name=".TreeDump"
    android:targetPackage="your.helper.package" />

Then run adb shell am instrument -w your.helper.package/.TreeDump and read logcat, or write to a file or a socket instead.

Caveats I ran into. Only one process can hold UiAutomation at a time, so this fights with a running UiAutomator2 server session on the same device; it's either/or. The tree you get is "right now", mid-animation, so bounds of moving things are a snapshot. And if you want it in the usual XML shape for existing tooling, you're writing that serialiser yourself.

Trade-offs

ApproachFixes the idle errorNeeds app changesScriptableWorks with a live Appium sessionMain cost
Animations off (adb settings, developer options, disableWindowAnimation)Mostly; not for Lottie, video, custom loopsNo (yes for the build-flag version)YesYesSome spinners keep running
Lower waitForIdleTimeoutYes (Appium only)NoYesYesYou may act before the UI settles
--compressed / ignoreUnimportantViewsNoNoYesYesSmaller tree, same wait
Layout Inspector / dumpsys activity topYesDebuggable build for Layout InspectorPartlyYesMissing text and content-desc, unstable format
Instrumentation without waitForIdleYesNoYesNo (one UiAutomation holder at a time)You write and maintain the helper

Which one to use

You control the app: Way 1 (build flag or animations off) plus Way 2 with a small non-zero timeout.

Third-party or production app through Appium: Way 2 (500 to 2000 ms) plus explicit waits.

One-off "what is on screen right now": Way 3.

You need the tree reliably on animated screens, scriptable, without an Appium session: Way 4.

If you have a better trick, especially for Compose apps where I get inconsistent results with animator scale 0 across versions, I'd like to hear it. Email is support@usespectra.dev.

Why Spectra ships an agent for this

Way 4 is what Spectra does. When you connect in Turbo Mode (direct ADB, no Appium server in the middle), Spectra installs a small Android instrumentation APK called Spectra Agent, about a megabyte and roughly 450 lines of Kotlin. It starts with adb shell am instrument, listens on a local port that Spectra reaches through adb forward, and answers with the tree read straight from getRootInActiveWindow(). No waitForIdle, so a spinner on screen no longer stalls the inspection. It answers with the tree and nothing else; there's no outbound connection and it collects no data. If you don't want it on a device, adb uninstall com.spectra.agent removes it.

To be upfront about scope: the free tier inspects through your existing Appium server, so it inherits Appium's idle wait, and Way 2 above is the fix there. The agent is part of Turbo Mode, which is in Pro (14-day trial). If this screen isn't a daily problem for you, Way 1 and Way 2 are the cheaper answer. More at https://usespectra.dev.

Spectra is free for inspection. Download at https://usespectra.dev.

Spectra is free for inspection: usespectra.dev