跪拜 Guibai
← Back to the summary

Remote Compose: AAOS Gets a Cross-App UI That Doesn't Suck

This article is translated from "Remote Compose in Android Automotive OS: Embed rich UI without the usual pain", original link https://proandroiddev.com/remote-compose-in-android-automotive-os-embed-rich-ui-without-the-usual-pain-90b857d59d8c, published by Daniel Georg on May 31, 2026.

When building infotainment systems on Android Automotive OS, one problem keeps coming up: you need to display one app's UI inside another app's UI. For example, adding phone widgets to the launcher, media cards to the instrument cluster, or embedding third-party app content in the OEM system UI. Sounds simple — until you actually try to implement it.

captionless image

Android offers multiple cross-app UI sharing mechanisms, each with its pros and cons. Let's look at what's currently available:

Internal Components/Views — This is the obvious first choice: everything is built directly inside the host app (e.g., the launcher). No cross-process overhead. The downside is the required expertise and coupling issues. In the automotive domain, to build a media card, your team needs to learn and understand the full MediaSession/MediaBrowser stack. To build a phone component, they need deep knowledge of the Telephony API. Add automotive APIs, OEM internal APIs, and domain-specific features — your team becomes experts in a dozen unrelated domains. Each component is tightly coupled to its dependent APIs, and when APIs change, no one remembers why it was designed that way.

TaskView / CarTaskView — Runs at the system level, embedding another app's full Activity into your window hierarchy. It's limited to system apps, requires privileged permissions, creates tight lifecycle coupling between host and client, and adds a dedicated SurfaceFlinger layer — quickly consuming the Hardware Composer's overlay budget, especially in multi-user multi-display (MUMD) setups like host, passenger display, and rear screens/users running simultaneously.

SurfaceControlViewHost — Avoids privileged permissions and shares the same layer overhead, but introduces a fragile IPC boundary. While basic rendering works, synchronizing structural changes, handling client process crashes, and managing AAOS rotary focus or seamless touch gesture switching forces the host into complex manual workarounds.

RemoteViews — Usable by any app without special permissions. But its component library is too primitive to build any serious UI: just a handful of TextViews, ImageViews, and Buttons. Beyond that, nothing else is possible.

The Remote Compose Solution

Every approach has unavoidable trade-offs. Remote Compose breaks this pattern. It serializes rich UI intent into a compact declarative binary stream — drawing instructions, not UI. The host acts as a browser: it receives these instructions and renders them independently within its own layer, without needing to know the provider app's lifecycle, process state, or permissions. Clicks and interactions are handled locally and returned to the provider, so the UI remains responsive even if the provider process is under load. Full composable expressiveness, delivered as data.

How Remote Compose Works

At its core, a RemoteCompose document is a serialized record of Canvas drawing calls — the same primitives Android uses to render any UI (draw rectangle, set color, clip region). Instead of executing these calls immediately, the provider packs them into a compact, self-contained binary format. It contains everything needed to build the UI: shapes, text with fonts and styles, images, and even animation expressions.

When the host receives this document, it simply iterates over the instructions against its local Canvas. The host doesn't need domain knowledge of where the data came from — it just plays back the data. While this is like a browser rendering a static page, the provider can push updated documents as fast as IPC allows. This turns the flow into a high-frequency stream, enabling smooth, state-driven UI updates without any XML bloat overhead.

Animations work through expressions embedded in the document at record time. Instead of hardcoding a value, the provider writes an expression like ContinuousSec() * 360 — the host evaluates it locally on each frame, producing smooth animations without any round trips to the provider process.

PoC: Putting Theory into Practice

I wanted to see how far RemoteCompose could go as a cross-app UI mechanism. After years working in the AAOS stack, I've seen OEMs write thousands of lines of boilerplate just to share a simple widget. The automotive launcher is the perfect testing ground.

Simulation Environment

To test real-world performance, I ran the PoC in a multi-display AAOS environment.

captionless image

The simulator allowed me to trigger various call states, like incoming call or active call, and observe the host's reaction in real time. This is the ultimate stress test for the "bytes in, integers out" contract. The provider app is in a completely different display context, but the UI in the launcher remains smooth.

Coexistence Proof: One Screen, Three Architectural Worlds

The goal was also to prove that RemoteCompose can coexist with existing AAOS rendering technologies. The new three-column launcher layout perfectly demonstrates this.

captionless image

Looking at the main display from left to right. This is a live coexistence test:

This integration confirms that RemoteCompose can coexist with traditional View-based systems and complex CarTaskView environments without side effects. This validates a non-disruptive adoption path: OEMs can gradually introduce modern remote-driven components into their production stacks, avoiding the risk and cost of a full system rewrite.

PoC Architecture

captionless image

Core Split: The architecture revolves around two independent apps running in different processes, communicating via an AIDL bound service:

Happy Path: The lifecycle is entirely reactive. Whenever state changes (e.g., incoming call), the provider pushes RC document bytes to the host, which immediately deserializes and renders them. When the user clicks a button (accept, reject), the host doesn't handle logic. It just sends a lightweight user action (integer ID) back to the provider. The provider handles business logic, updates state, and pushes a new document.

Edge Case — Resilient Lifecycle Management: Unlike SurfaceControlViewHost or TaskView, where a guest process crash can leave a "dead" surface or visual "black hole", Remote Compose ensures full lifecycle isolation. In this PoC, the Host monitors the AIDL link for Binder death. If the provider process crashes, the host detects the failure and swaps the UI slot to a placeholder or shimmer state within the same frame budget. Since the host owns the drawing, there's no surface flicker or stale frames, the transition is seamless, and the UI remains perfectly responsive when the connection is re-established.

This creates a beautifully minimal contract between the two domains: only bytes in, integers out. This approach allows both apps to evolve independently without breaking the UI.

Transport Layer

RemoteCompose doesn't specify how to deliver the binary stream. It only defines the format, meaning the transport is entirely up to you. Here are some options that make sense in an AAOS environment:

For the PoC, AIDL was the obvious choice. It provides low overhead, callback-based push delivery, and direct control over when the provider pushes new documents to the host.

I mentioned earlier that the contract between the two apps is minimal. Here's the proof. Let's look at the actual implementation.

AIDL Contract in the PoC

The entire contract between host and provider boils down to two simple interfaces. The provider implements a service to manage callbacks and receive user actions. The host implements a callback to receive the UI as a byte array.

// Implemented by the Provider
interface ICallProviderService {
    void registerCallback(IDocumentCallback cb);
    void unregisterCallback(IDocumentCallback cb);
    void sendAction(int actionId); 
}
// Implemented by the Host
oneway interface IDocumentCallback {
    void onDocumentUpdated(in byte[] documentBytes, int transition); 
}

Bytes in, integers out. Literally.

Provider: Generating the UI

The provider writes UI using familiar Compose syntax. It uses remote equivalents instead of Column, Row, or Text. The result is never rendered to screen. Instead, it's directly packed into a byte array using captureSingleRemoteDocument.

// 1. Build the document familiar Compose syntax, but with Remote* components
 suspend fun createIdleDocument(context: Context, callLog: List<CallLogEntry>): ByteArray {                                                      
      return captureSingleRemoteDocument(context) { IdleScreen(callLog) }.bytes                                                                   
  }       
                                                                                                                                      
// 2. The composable itself looks like regular Compose                                                                                                                                                 
  @RemoteComposable                                                                                                                               
  @Composable                                                                                                                                     
  private fun IdleScreen(entries: List<CallLogEntry>) {                                                                                           
      RemoteColumn(modifier = RemoteModifier.fillMaxSize().padding(16.dp)) {                                                                      
          RemoteText(text = "Recent Calls", fontSize = 24.rsp)                                                                                    
                                                                                                                                                
          entries.forEachIndexed { index, entry ->                                                                                              
              RemoteRow(                                                                                                                          
                  modifier = RemoteModifier   
                      .fillMaxWidth()                                                                                                             
                      .clickable(HostAction(RemoteString("${2000 + index}")))                                                                   
              ) {                                                                                                                                 
                  RemoteText(text = entry.name, fontSize = 16.rsp)
                  RemoteText(text = entry.number, fontSize = 12.rsp)                                                                              
              }                                                                                                                                 
          }                                                                                                                                     
      }                                                                                                                                           
  }

The syntax is almost identical to standard Compose. The only differences are the Remote prefix and the fact that the output is a ByteArray rather than pixels on screen.

Host: Rendering

On the host side, the footprint is minimal. The host receives bytes via the AIDL callback, wraps them in a RemoteDocument, and passes them to RemoteDocumentPlayer. The player automatically handles all parsing and drawing.

RemoteDocumentPlayer(
    document = doc.document,
    onNamedAction = { actionId, _, _ ->
        viewModel.sendAction(actionId.toInt()) 
    }
)

The host knows nothing about the provider's UI. It doesn't know the layout structure or business logic. It just accepts bytes and renders them. When the user taps a button, the action flies back across the IPC boundary as a simple integer.

The Pragmatic Path: No Migration Required

A common problem with modern UI frameworks is adoption cost. For this PoC, I used the standard AAOS CarLauncher as the host. Like most of the core Automotive stack, it's built on the traditional Android View system: XML layouts, Fragments, and custom Views. For OEMs, migrating a production-level system shell to Jetpack Compose just to support a few "widgets" is rarely a realistic option.

Fortunately, RemoteCompose is designed for this reality. It comes with two distinct player artifacts:

Since the reference CarLauncher is View-based, I used the remote-player-view artifact. RemoteDocumentPlayer is a plain FrameLayout that you can drop directly into any XML layout. You just call setDocument(bytes), and it renders the provider's UI via Canvas.

No Compose dependencies on the host side, no ComposeView wrappers, and zero migration effort. The provider side stays exactly the same: it pushes the same byte array regardless of who renders it. This proves a key point for industry adoption: one provider can serve both modern and traditional hosts with zero coupling.

A Note on the Android Framework (API 35) If you look closely at the latest AOSP updates, you'll see that Android actually bakes its own native RemoteCompose player directly into the framework starting from API 35. It powers the new DrawInstructions for standard system widgets. However, relying on the framework player ties your host to OS update cycles. By using the AndroidX player artifacts mentioned above, third-party launchers and OEM hosts can adopt this next-generation architecture today, even on older Android versions, while supporting more operations than the basic system ones.

Limitations and Open Questions

While RemoteCompose shows strong potential as a cross-app UI mechanism, it's important to acknowledge its current limitations and areas needing further validation.

API Stability (Alpha State): RemoteCompose is still evolving. The API surface is not yet stable, and changes between versions are expected. This makes immediate production adoption risky without an additional abstraction layer.

Debugging and Tooling: Traditional UI debugging tools (Layout Inspector, Compose tools) don't fully apply. Since the UI is transmitted as a binary document, debugging rendering issues, layout problems, or state mismatches requires custom instrumentation and logging. This adds extra complexity for development teams.

Layout Inspector with Remote Compose

CVAA and Accessibility: RemoteCompose supports basic semantics (content descriptions, roles), but canvas-drawn elements don't produce semantic nodes, and the framework lacks liveRegion, focus order control, and custom accessibility actions. Compliance requires host-side workarounds that can't go beyond limited UI states. Braille display support is unverified. In practice, this shifts accessibility responsibility from the UI producer to the host, reintroducing complexity at a different layer.

Rotary Navigation: The player currently doesn't expose explicit support for focus management primitives or rotary input handling (e.g., onRotaryScrollEvent). The host can capture rotary events at the wrapper level but can't route them to scrollable content within the document.

Conclusion

RemoteCompose shifts the mental model from sharing surfaces to sharing intent. Instead of embedding an external process into your UI, you exchange a compact declarative description of what should be rendered. This distinction is subtle but fundamental in large AAOS systems. It reduces coupling, simplifies ownership boundaries, and enables teams to evolve independently without breaking each other.

The PoC demonstrates that this approach is not only feasible but can be practically integrated into existing View-based stacks without disruptive migration.

RemoteCompose is not yet production-ready, and important questions remain around tooling, performance, and system integration. However, it's the first approach that meaningfully addresses the core problem of cross-app UI in automotive systems without inheriting the complexity of previous solutions.

I've spent years working on AAOS platform architecture across multiple brands and large infotainment systems, and I've seen how these challenges repeatedly lead to tight coupling and unnecessary complexity. This PoC attempts to rethink the problem from first principles.

If you're facing similar challenges, I'd be very interested to exchange ideas.

Welcome to search and follow the public account "稀有猿诉" for more quality articles!

Protect originality, do not reprint!