Enable Jetpack Compose Accessibility when Collecting Baseline Profiles
May 01, 2025 -Here's a small workaround that can be used to make Android Baseline Profile collection work better with Jetpack Compose apps. For some reason, Compose apps (at least with Compose version 1.8.0) don't emit accessibility events when running Macrobenchmarks (for example, when generating baseline profiles). With this workaround, they can be forced to be enabled, which makes a bunch of UI Automator functions work.
UI Automator & Accessibility events
UI Automator is one of Android's UI testing frameworks, and it's the one used in Macrobenchmark and baseline profile samples and documentation. It runs outside of the app, but it can find and interact with UI elements based on their content or IDs. In theory, this should work with both View and Compose-based apps.
UI Automator can leverage accessibility events generated by apps to have better visibility in the UI state. For instance, after making a scroll gesture, the automator framework listens for scroll accessibility events to determine when the scroll ends, and whether the end of a scrollable element was reached.
Today, I was updating the baseline profiles for my Wear OS app1, and I tried to use the useful-sounding scrollUntil
function: scroll down until a specific UI element is visible. Simple, right? Unfortunately, I was faced with the benchmark crashing, and there were these kinds of exceptions in the logs:
Timed out waiting 1000ms on the condition.
java.util.concurrent.TimeoutException: Expected event not received within: 1000 ms among: []
at android.app.UiAutomation.executeAndWaitForEvent(UiAutomation.java:950)
at androidx.test.uiautomator.UiDevice.performActionAndWait(UiDevice.java:220)
at androidx.test.uiautomator.GestureController.performGestureAndWait(GestureController.java:98)
at androidx.test.uiautomator.UiObject2.scrollUntil(UiObject2.java:897)
Followed by:
scrollUntil reached max retries for null events.
A little debugging revealed that scrollUntil
was not scrolling to the end of the list (where the target button was), because it was not seeing the scrolls at all! No scroll accessibility events were sent by the app. And without those, scrollUntil
just bails out after a few tries, because it can't really know if it's even doing anything.
Identifying the bug
After a couple of side quests2 I landed on this issue on Google's issue tracker: https://issuetracker.google.com/issues/342316793. This is exactly the issue I had. Apparently, Compose explicitly disables accessibility events for UI automator for some reason unknown to me.
A workaround (used in some benchmarks in Compose itself) is linked in the issue comments. It uses an API exposed in Compose to force accessibility on. However, the linked workaround is not directly applicable to my case: it creates the Compose view itself and can therefore force accessibility on directly in the test code. That does not work for me, because in my case the views are created by the benchmarked app, not the test code.
Adapted workaround
The idea is simple: add a BuildConfig
flag that's true when generating baseline profiles, and false otherwise. Then, check the flag in the app, and if it's true, make the app itself force accessibility on.
These instructions assume that the baseline profile setup is done mostly following the official guidelines, and that the automatically generated build types are used.
-
In the
build.gradle.kts
file for the main app module (in my setup,app/build.gradle.kts
), add these configurations:android { // ... defaultConfig { // ... buildConfigField("boolean", "MACROBENCHMARK", "false") } buildTypes { // ... // benchmarkRelease and nonMinifiedRelease are created by the Baseline Profile Gradle plugin create("benchmarkRelease") { buildConfigField("boolean", "MACROBENCHMARK", "true") } create("nonMinifiedRelease") { buildConfigField("boolean", "MACROBENCHMARK", "true") } } }
-
Define this composable function:
@Composable fun EnableBenchmarkAccessibilityOverride() { // Work around compose not sending accessibility events for UiAutomator // Ref: https://issuetracker.google.com/issues/342316793 val view = LocalView.current LaunchedEffect(Unit) { if (BuildConfig.MACROBENCHMARK) { Log.d(TAG, "EnableBenchmarkAccessibilityOverride: MACROBENCHMARK flag set; " + "enabling compose accessibility overrides") (view as RootForTest).apply { setAccessibilityEventBatchIntervalMillis(0L) forceAccessibilityForTesting(true) } } } }
-
In all Compose entry points (activities, fragments,
ComposeViews
...) add the previously defined composable. For example, in myMainActivity
, I did:setContent { EnableBenchmarkAccessibilityOverride() WearApp() }
When the macrobenchmarks / baseline profile collection is now run, the "enabling compose accessibility overrides" message should get logged. And scrollUntil
and other UI Automator functions now work!
In normal app builds there's no change, as the MACROBENCHMARK
flag is kept as false.
Caveats:
- The override call must be added manually in all Compose entry points. With larger hybrid apps, with a lot of fragments or compose views, this could be a problem.
- This is a bit ugly, but it works.
By implementing this, I was able to make baseline profile collection much less flaky in my app.
I tried to see if I could use the Compose UI testing framework with Macrobenchmarks — it would have much better integration with compose views. Unfortunately, it does not work (confirmed on the #compose-wear Slack channel).