← Back to list

Scalable UI in Android Automotive OS: From UI Embedding to System Window Orchestration

In Android Automotive’s stock home screen, everything lives inside one Activity. CarLauncher draws the map, the media card, and status…

Daniel Georg in ProAndroidDev · 2026-04-23 08:30 · 154 claps · 11.9 min read
#aosp #android #android-automotive #androiddev #android-app-development
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval ☁️ · DevOps & Cloud

Scalable UI in Android Automotive OS: From UI Embedding to System Window Orchestration

In Android Automotive’s stock home screen, everything lives inside one Activity. CarLauncher draws the map, the media card, and status widgets all together. Switching apps means swapping the whole UI out. The Scalable UI framework takes a different approach: each panel hosts its own independent Activity, all running side by side, driven entirely by declarative XML in an RRO overlay.

This started from a different problem. In my previous post, I explored Remote Compose as a new approach to share UI across app boundaries in AAOS. It offered a solution without the usual cross-process overhead and coupling. However, a comment from **Ralph Thomas (Google)** shifted my thinking toward a much broader horizon and led me down a rabbit hole that completely changed my perspective.

I had been following Scalable UI closely, but that comment made me stop and ask a different question. Remote Compose solves the problem of embedding foreign UI without coupling, but what if embedding itself is the wrong abstraction?

We no longer need to build widgets inside a launcher because in the era of Scalable UI, the application itself IS the widget.

Instead of a launcher that orchestrates embedded content, OEMs now declare the layout in System UI configuration through RRO overlays. There is no longer a “Home” Activity in the traditional sense because the system itself becomes the launcher. Each app owns its domain and runs independently without surface sharing, IPC, or embedding.

In this model, the entire configuration, including panel sizes, activity placement, and transitions, is declared in XML. From the OEM’s perspective, this means achieving complex windowing behavior without writing a single line of Java/Kotlin — just XML overlays.

The full vision behind Scalable UI has a name, at least one I’d give it: State-Driven Cockpit Orchestration. The entire cockpit, including applications, system bars, HUNs, and overlays, becomes a single coordinated surface that moves through synchronized states, instead of a collection of independently-managed windows. System UI declares the states in XML; the Window Manager drives every app, every bar, and every overlay through the transition in lockstep.

The rest of this article is about what that looks like in practice: the building blocks, a proof-of-concept dashboard built entirely with XML, and the trade-offs this architecture brings. I’ll also introduce the visual editor I built to author Scalable UI without writing XML by hand.

The Heavy Legacy: Why Launchers Became a Nightmare

For years, AAOS launchers acted as monolithic hosts for the entire home screen experience. Want to show a map? You needed CarTaskView or SurfaceControlViewHost. A media widget? That meant integrating the full MediaBrowser stack. Phone, climate, and vehicle widgets each brought their own dependencies.

Every integration introduced unique lifecycle challenges and ongoing maintenance overhead. As a result, launchers gradually became the most complex, brittle, and feared components in the entire AAOS stack.

The “Sophisticated” Trap: TDAs Orchestration and CTS Hell

When standard embedding techniques fell short for sophisticated multi-window layouts, the next step was to dive deeper into the system by (for example) creating a CustomDisplayAreaProvider and directly orchestrating TaskDisplayAreas (TDAs).

I have invested months into implementing this kind of custom low-level windowing m̶a̶g̶i̶c̶ logic. To actually place and animate those display areas, you had to register a custom TaskDisplayAreaOrganizer inside System UI. Setting the layout required a WindowContainerTransaction to tell WindowManager about the new TDA bounds, while animations had to be driven frame-by-frame with SurfaceControl.Transaction directly on the display area’s leash.

Every transition was a hand-wired ValueAnimator interpolating bounds and applying them to SurfaceFlinger yourself either directly or by extending the WMShell transitions framework with your own TransitionHandler. At that point, you were no longer writing app code; you were writing part of the window management pipeline itself.

WinScope became your only friend in this process. It was the only tool that allowed you to visually grasp the mess of layers, focus transfers, and visibility flags in the Window Manager. Without it, you were essentially flying blind.

After months of engineering, you finally ran the Compatibility Test Suite (CTS). Several hours later, the results showed that your infotainment system was no longer CTS-compliant. This marked the beginning of an agonizing bug-fixing marathon where every fix for a Google-mandated test broke your custom UX. Custom window management logic was a double-edged sword that could easily wreck a certification timeline.

After walking through why the old paths break down, the next question is obvious: is there a better abstraction altogether?

Orchestration vs. Embedding

To understand the shift, we have to look at the abstraction. In my previous post, I showed how Remote Compose solves the coupling problem by allowing a Launcher to render foreign UI primitives as a smart host.

But Scalable UI asks a different question: Why do we need a host app at all?

  • Remote Compose, and every other UI embedding technology, ties the app’s visibility and placement to a host.
  • Scalable UI focuses on orchestrating the actual app process.

Instead of hosting content, the system manages the applications. System UI tells the Window Manager to place the Navigation Task in one panel and the Media Task in another. This is the fundamental difference: each app keeps its own process and stability, while the framework ensures they look like a single, unified interface.

Building Blocks of Scalable UI

Concretely, Scalable UI is built from four primitives, all declared in XML. The “Cockpit State” is the single source of truth — you are no longer coding animations, you are defining:

  • TaskPanels: Rectangular containers that map to dedicated RootTaskStack to host full applications (Navigation or Media) as functional widgets.
<TaskPanel id="navigation" defaultVariant="@id/base" displayId="0">
</TaskPanel>
  • DecorPanels: Generic containers for injecting custom UI/overlays that sync perfectly with app tasks.
<DecorPanel id="navigation_decor" defaultVariant="@id/base" displayId="0">
</DecorPanel>
  • Variants: Visual states managing precise bounds, Z-order, and visibility.
<Variant id="@+id/base"> </Variant>

Transitions: Synchronized animations coordinated by the Window Manager to move panels between variants based on system events. OEMs can also define their own custom events and fire them from panel controllers or bind them to intents, extending the system beyond the built-in System* events.

<Transitions>
        <Transition onEvent="_System_TaskOpenEvent" fromVariant="@id/base" toVariant="@id/open" duration="300" interpolator="@android:anim/accelerate_decelerate_interpolator"/>
 </Transitions>

Here is a practical example of a navigation panel defined with two visual states (variants) and an animated transition:

<TaskPanel id="navigation" defaultVariant="@id/base" displayId="0">
    <Variant id="@+id/base">
        <Layer layer="2"/>
        <Visibility isVisible="true"/>
        <Alpha alpha="1.0"/>
        <Bounds left="0dp" top="0dp" right="850dp" bottom="1600dp"/>
    </Variant>
    <Variant id="@+id/open">
        <Layer layer="2"/>
        <Visibility isVisible="true"/>
        <Alpha alpha="1.0"/>
        <Bounds left="0dp" top="0dp" right="2480dp" bottom="1600dp"/>
    </Variant>
    <Transitions>
        <Transition onEvent="_System_TaskOpenEvent" fromVariant="@id/base" toVariant="@id/open" duration="300" interpolator="@android:anim/accelerate_decelerate_interpolator"/>
    </Transitions>
</TaskPanel>

Why this matters: Notice there is no code involved in calculating the interpolation between 850dp and 2480dp. The Scalable UI framework orchestrates the transition based on this XML, ensuring the Window Manager scales the underlying tasks in perfect sync with the container.

That’s State-Driven Cockpit Orchestration in practice — System Bars and HUNs are Scalable UI panels too, driven by the same Cockpit State as applications.

And this directly answers the CTS pain from earlier. Because the framework comes pre-certified and compliant with the CTS, the primary risk of late-cycle test failures is eliminated. What used to take months of custom windowing logic and possible certification regressions is now available out of the box.

Those are the core components. I’m not going to rewrite the manual here — if you need the full technical specs, the library source, or want to see how it’s integrated into SystemUI, check these resources:

The Proof: A 3-Column Dashboard without Kotlin/Java Code

To challenge the framework capabilities, I built a custom PoC. I wanted to move away from the “boring” standard horizontal emulator and create something that feels alive. I built this PoC on AOSP’s android16-qpr2-release branch.

Using the same layout from my Remote Compose PoC, I re-implemented the entire dashboard using only Scalable UI and added several ‘sophisticated features’ to demonstrate that what once required deep knowledge of Android internals can now be fully orchestrated through a high-level declarative model:

  • Three independent apps (Media, Phone, Maps ): Running side-by-side as native tasks in their own dedicated containers.

  • Complex transitions: Orchestrated via XML with elastic overshoot and background blur.
  • Zero custom Java: Everything is driven via XML overlays; the framework acts as the invisible conductor, managing the layout without owning the application logic.
  • Deep UI Stack: Added half-screen overlays for Settings and the Media player apps.
  • Multi-Page Navigation: Added a second Dashboard page and extended the System UI with a custom OEM event, which triggers the screen paging directly on the home screen. The whole “page switching” feature came down to a few XML lines. A standard CarSystemBarButton in the bottom bar fires custom “show_page2” / “hide_page2” events, which Scalable UI Transitions pick up to swap panel variants:

<com.android.systemui.car.systembar.CarSystemBarButton
 android:id="@+id/page2_nav"
 android:contentDescription="Page 2"
 style="@style/SystemBarButton"
 systemui:icon="@drawable/car_ic_dashboard"
 systemui:selectedIcon="@drawable/car_ic_dashboard_selected"
 systemui:selectedEvent="hide_page2"
 systemui:unselectedEvent="show_page2"/>

No Java code, just XML wiring.

Google’s official reference target for Scalable UI is sdk_car_dewd_x86_64, which is a great starting point for any investigation. However, for my PoC, I wanted to use the standard landscape AAOS emulator every developer already has: sdk_car_x86_64.

Scalable UI isn’t just “there” by default on standard targets. I had to figure out the exact plumbing required to get the framework running outside of the official dewd environment. It turns out that Scalable UI is gated at two layers of the Android stack, and each layer is controlled by a boolean resource — so let’s unlock them one by one.

Step 1: Framework-level Handshake

<resources>
    <bool name="config_remoteInsetsControllerControlsSystemBars">true</bool>
</resources>

Note: This flag is already available via RRO CarFrameworkDewdRRO (targeting @android). You can simply include it in your build configuration.

Step 2: System UI Activation & Panel Manifest

Next, you need to set the flags inside your layout RRO’s res/values/config.xml (targeting @com.android.systemui):

<resources>                                                                                                                                                                                                    
      <!-- Turns on Scalable UI in SystemUI -->                                                                                                                                                                
      <bool name="config_enableScalableUI">true</bool>

      <!-- Optional: SystemUI wiring -->
      <bool name="config_enableTopSystemBar">true</bool>                                                                                                                                                         
      <bool name="config_enableBottomSystemBar">true</bool>                                                                                                                                                      
      <bool name="config_enableClearBackStack">false</bool>
      <bool name="config_enableSafeAreaAndToolbarPerDisplay">false</bool>                                                                                                                                        
      <integer name="config_systemBarSuwBehavior">1</integer>                                                                                                                                                    

      <!-- The panel manifest — every panel XML listed here -->                                                                                                                                                  
      <array name="window_states">                                                                                                                                                                               
          <item>@xml/media_panel</item>                                                                                                                                                                          
          <item>@xml/phone_panel</item>                                                                                                                                                                          
          <item>@xml/map_panel</item>
          <!-- ...all your panels -->                                                                                                                                                                            
      </array>                                                                                                                                                                                                   

      <!-- Auto-launch activities into specific panels at boot -->                                                                                                                                               
      <string-array name="config_default_activities">                                                                                                                                                          
          <item>media_panel;com.android.car.carlauncher/.ControlBarActivity</item>                                                                                                                               
          <item>phone_panel;com.android.car.dialer/.ui.TelecomActivity</item>                                                                                                                                    
          <item>map_panel;com.android.car.mapsplaceholder/.MapsPlaceholderActivity</item>                                                                                                                        
      </string-array>                                                                                                                                                                                            
 </resources>

Step 3: Install the StubCarLauncher

To get the empty home screen, you need to remove the default Car Launcher and install an empty stub (StubCarLauncher) in its place. According to the comments in the AOSP source code, this is only a temporary solution a dedicated visibility barrier is expected to be implemented, likely in the next Android release.

Reboot.

That’s it!

Once all three pieces are in place, the framework boots, discovers your panel manifest, and starts orchestrating. You’re ready to build your own custom multi-window infotainment.

The full PoC, with both config overlays and all panel XMLs, is also available on my GitHub.

Beyond XML: Panel Controllers

XML handles structure: layout, states, transitions but Scalable UI also gives OEMs a code-level extension point. Each Panel can be backed by a custom Panel Controller, a Kotlin or Java class that extends BaseTaskPanelController or DecorPanelControllerBase and plugs into the framework’s Dagger DI graph. Controllers are where OEM-specific behavior lives: deciding which Activity to launch into a panel based on runtime state, firing custom events, or implementing context-aware policies.

Here’s the MapsPanelController from AOSP source code:

public final class MapsPanelController extends BaseTaskPanelController {
    private static final String TAG = MapsPanelController.class.getSimpleName();
    @AssistedInject
    public MapsPanelController(Context context,
        @Assisted PanelControllerMetadata panelControllerMetadata,
        PanelUtils panelUtils) {
        super(context, panelControllerMetadata, panelUtils);
    }
    /** Creates an instance of MapsPanelController using the provided PanelControllerMetadata. */
    @AssistedFactory
    public interface Factory extends TaskPanelController.Factory < MapsPanelController > {
        MapsPanelController create(PanelControllerMetadata metadata);
    }
    @Override
    public Intent getDefaultComponent() {
        Intent mapIntent = super.getDefaultComponent();
        Intent result = TosHelper.maybeReplaceWithTosMapIntent(mContext, mapIntent,
            R.string.config_tosMapIntent, ActivityManager.getCurrentUser());
        logIfDebuggable(TAG + ", getDefaultComponent = " + result);
        return result;
    }
}

The XML wiring below shows how an OEM would connect it to a panel:

  <!-- res/xml/map_panel.xml -->
  <TaskPanel id="map_panel" controller="@xml/map_controller" ...>

  <!-- res/xml/map_controller.xml -->
  <Controller id="map_controller">                                                                                                                                                
      <Config key="ControllerName"                                                                                                                                                
          value="com.android.systemui.car.wm.scalableui.panel.controller.MapsPanelController"/>                                                                                 
      <Config key="PersistentActivity"                                                                                                                                            
          value="com.google.android.apps.maps/com.google.android.maps.MapsActivity"/>                                                                                             
  </Controller>                                                                                                                                                                   

For my PoC, I deliberately stayed XML-only to show how far the declarative model goes on its own. In practice, most production cockpits will need controllers. I’ll cover controller patterns in more depth in a follow-up post.

Trade-offs and Open Questions

Every design makes trade-offs, and the one I want to dig into here is worth understanding before committing to a production cockpit on Scalable UI. The behavior I’ve observed through my own experiments may be an intentional design decision or an emergent side-effect of centralized orchestration. I don’t always know which, and in practice the distinction matters less than understanding how it affects real deployments.

System UI Becomes a Single Point of Failure

This architectural shift fundamentally changes the failure model of the dashboard. Because the system itself acts as the orchestrator, System UI effectively becomes the single point of failure for the entire UI.

Before Scalable UI, a Launcher crash might leave you without a home screen, but the status bar, HVAC controls, notifications, and custom OEM SystemUIOverlayWindows would continue running independently. A SystemUI crash, conversely, would take down those system surfaces while Launcher and hosted apps “survived”.

With Scalable UI, when System UI crashes, the framework removes all panel root tasks that System UI created as the TaskOrganizer. Every app hosted inside those panels, whether navigation, phone, settings, or any other running activity, is killed along with its task. System UI restarts quickly, but since Scalable UI does not persist the last active state, it rebuilds the panel layout from its initial configuration rather than the state the user left it in. Only apps configured for auto-launch are relaunched automatically, and only into their default panel positions. All app state, navigation history, and scroll positions are lost. For example, in my poc, the system always restarted in the three-column layout regardless of which panels the user had rearranged or which variant was active.

For OEMs, this shifts what’s at stake when System UI fails. Every new feature added to System UI, every panel controller, transition animator, and event dispatcher, increases the blast radius of a single crash. One uncaught exception in a panel’s controller, and the driver loses not just a status bar, but the entire infotainment experience.

Features I’d Like to See Added

Beyond the trade-offs above, here are a few capabilities I’d like to see added to Scalable UI:

  • Hot reload of Scalable UI configurations. Currently, applying an updated panel layout requires killing System UI, which tears down all panel root tasks and kills every app hosted inside them. A hot-reload mechanism that picks up new panel definitions, variant changes, and transition updates without restarting System UI would let OEMs update their dashboard layouts at runtime, without tearing down the running session.
  • **Scaling and matrix transforms for panel animations.** SurfaceControl.Transaction already supports setScale() and setMatrix() at the framework level, but the Scalable UI framework does not expose them. Adding scale and rotation properties to the Variant model and Panel interface would enable zoom, shrink, and tilt effects during transitions without requiring custom window management hacks.
  • Per-property animation timing. Currently, all animated properties share a single duration and interpolator. The framework’s custom animator path exists but is non-functional for bounds due to missing property setters, absent from/to value injection, and task pre-positioning. Fixing these three issues would allow staggered, per-property transitions, for example bounds sliding with overshoot while alpha fades linearly.
  • Synchronized app transitions. Scalable UI handles all task transitions internally with no hook for external animation participants. Integrating RemoteAnimationAdapter support would let OEM launchers animate app surfaces as they enter a panel, for example morphing an app out of a widget icon into its target panel rather than simply appearing at the destination bounds.
  • State persistence across restarts. Scalable UI does not persist the last active variant state. After any restart, every panel reverts to its default configuration. Persisting the current variant per panel would let the infotainment resume exactly where the user left it.

Introducing Scalable UI Editor

Working with Scalable UI over the past months, one thing kept coming up: authoring Scalable UI by hand means juggling Variants, Z-layers, and transitions across multiple XML files, with no visual feedback until you flash the RRO and restart SystemUI to see the result. The workflow hasn’t caught up with the framework’s power.

So I started building my own tool for it, a visual editor for Scalable UI that exports RRO projects directly from a layout design. You place TaskPanels on a canvas, switch between Variants to define states, and tweak Transitions, all with live preview. One click flashes the RRO directly to the running emulator or device. The output is a standard RRO file, fully compatible with manual workflows.

If your team is building on Scalable UI, let’s talk.

Closing Thoughts

Scalable UI is the real deal. The Google team didn’t ship a workaround, they shipped a platform that turns what used to be months of CTS firefighting and fragile system-level modifications into XML. For OEMs, this means significant savings in engineering effort, faster time-to-market, and a much lower barrier to entry for building sophisticated multi-window cockpits. The team behind it deserves real credit. Hats off.

Me on LinkedIn


메타데이터
post_id
dae03b335eee
slug
scalable-ui-in-aaos-from-ui-embedding-to-system-window-orchestration-dae03b335eee
url
https://proandroiddev.com/scalable-ui-in-aaos-from-ui-embedding-to-system-window-orchestration-dae03b335eee
canonical_url
https://proandroiddev.com/scalable-ui-in-aaos-from-ui-embedding-to-system-window-orchestration-dae03b335eee
author_url
https://medium.com/@passenger6
status
ok
fetched_at
2026-06-12 10:20:10