android

Android Automotive Bug Sends Activities to the Wrong Car Panel

If you build for Android Automotive OS, here's a scenario worth knowing: your code returns START_SUCCESS, logcat shows everything running smoothly, yet the activity lands in the wrong panel. No crash. No exception. Just a navigation app appearing on the media pane while the nav pane stays blank.

This isn't a hypothetical. A developer documented exactly this happening on a production AAOS build with two panes — navigation on the left, media on the right. The navigation app ended up on the right. Three days of debugging followed.

The short version

  • Android 12 introduced a new layer between display and task that broke the old routing assumption
  • SetLaunchDisplayId() tells the framework which screen to use, but says nothing about which panel within that screen
  • Most AAOS production vehicles shipping today run Android 12 or 14, making this bug active in real codebases
  • The fix requires CarActivityManager.setPersistentActivity() instead of relying on standard ActivityOptions
  • Automated tests miss this because they check for activity state, not spatial placement

Why Android 12 Changed the Routing Rules

On earlier Android versions, there was nothing between DisplayContent and the task itself. You had DisplayContent, then ActivityStack, then Task. One screen meant one landing point, and calling setLaunchDisplayId(0) was enough because there was nowhere else for the activity to go.

Android 12 added a new layer called TaskDisplayArea (TDA). The hierarchy now stacks DisplayContent above TaskDisplayArea, then Task, then ActivityRecord. On phones this rarely matters — one screen typically has one TDA.

When an AAOS setup uses more than one pane, manufacturers set up separate TDAs for each region. The left side might correspond to Feature ID 1; the right side to Feature ID 2. Calling setLaunchDisplayId(0) specifies the display only. The framework then calls getDefaultTaskDisplayArea() and picks whichever pane the OEM has set as default.

START_SUCCESS. Wrong panel. No error recorded.

This is what makes the bug expensive at the enterprise level. The logs tell you everything worked. They don't tell you where the activity landed.

Our Analysis

Two factors kept this bug hidden.

First, the code followed AOSP best practice. ActivityOptions.makeBasic() with setLaunchDisplayId() is exactly what documentation recommends. It works on emulators (one TDA per display) and on single-panel reference hardware. The failure only appears on multi-pane builds.

Second, automated integration tests check whether an activity reaches RESUMED state. If it does, the test passes. The test framework has no spatial awareness — it never verifies which physical pane holds the surface. Same log output, different outcome.

This points to a broader issue with how the Android framework reports success. START_SUCCESS does not carry panel information. The only way to see where an activity actually landed is running:

Adb shell dumpsys activity activities | grep -A 5 "MapActivity"

The featureId in that output reveals everything.

For teams building on Android 12–14 right now, this is not a theoretical problem. The Renault Scenic E-Tech (2024 production) ships on Android 12. Android 16 exists, but the fleet hasn't caught up. Every AAOS platform engineer working on multi-pane builds today is on Android 12–14 territory, and this routing behavior is active in their codebases.

What This Means for Your Sprint

Say you're a developer adding a new instrument cluster widget to a two-pane AAOS system. You follow the standard pattern: create ActivityOptions, call setLaunchDisplayId(0), fire the intent. Everything compiles. Tests pass. On the reference emulator, the widget appears in the correct panel.

Then on the target hardware, the widget shows up on the wrong side. The other pane sits empty. Your product owner asks why the feature broke in staging.

The root cause is that setLaunchDisplayId() is only half the address. For Android 12–14 multi-pane builds, you need to specify the TDA explicitly using CarActivityManager.setPersistentActivity(). This binds a component to a specific TDA feature ID, telling the framework not just which display but which panel to use.

This change belongs in your next sprint planning if your team builds AAOS interfaces. Treat multi-pane routing as a first-class concern, not an afterthought.

Bottom Line

The lesson here isn't just about one API call. It's about the gap between what the framework reports as success and what actually happened. In multi-pane environments, START_SUCCESS can be misleading.

For teams on Android 12–14 with multi-pane AAOS builds, CarActivityManager.setPersistentActivity() is the correct answer today. It bypasses the default TDA selection and routes activities explicitly. Test your launch paths against actual multi-pane hardware or get the featureId from dumpsys during automated runs.

Android 16 brings extensible UI improvements that will reshape how this works. Until then, knowing where your activities land matters as much as knowing they started.

Frequently asked questions

What does START_SUCCESS actually tell you in Android Automotive OS?

It confirms the activity started and registered a window. It does not confirm which panel within a multi-pane display received the surface.

Why did automated tests miss this bug?

Tests typically verify that an activity reaches RESUMED state. They don't check spatial placement, so an activity landing in the wrong pane still passes.

How do I see which panel an activity actually landed in?

Run adb shell dumpsys activity activities and look for the featureId field in the output for that activity. That ID tells you which TaskDisplayArea holds the surface.

Does this bug affect Android 16?

Android 16 introduces different UI extensibility features, but Android 12–14 remains the active version for most AAOS production vehicles in 2024–2026.

What's the correct way to launch an activity in a specific panel on Android 12–14?

Use CarActivityManager.setPersistentActivity() to bind the component to a specific TDA feature ID rather than relying on setLaunchDisplayId() alone.

Related