This document provides a technical deep dive into how Trusted Web Activity (TWA) launch parameters (e.g. file handles passed via Android intents) are securely routed and delivered to the web application's launch queue in Chromium.
For the design history and rationale, see the Design Doc.
TWA launch parameters handling spans across the JNI boundary:
WebAppLaunchHandler): Manages the Android intent lifecycle, initiates Digital Asset Links (DAL) verification, and triggers C++ actions.TwaLaunchQueueTabHelper): Manages the state of active launches, matches them to C++ navigations, and enqueues parameters to the cross-platform LaunchQueue.To prevent security vulnerabilities (specifically, delivering file handles to malicious origins via redirects), the system correlates launch intents with C++ navigations using a unique launch token and performs strict scope checks upon navigation commit.
The following diagram illustrates the typical flow of a TWA launch where the navigation commits before DAL verification completes (the most common race condition scenario):
sequenceDiagram autonumber participant Java as WebAppLaunchHandler (Java) participant Helper as TwaLaunchQueueTabHelper (C++) participant Nav as NavigationHandle (C++) participant Queue as LaunchQueue (C++) Note over Java, Helper: 1. Preparation Java->>Helper: PrepareForLaunch(token, params) Note over Helper: Stores in pending_launches_ Note over Java, Nav: 2. Navigation Start Java->>Nav: Starts Navigation (with token in UI Data) Helper->>Nav: DidStartNavigation() Note over Helper: Extracts token, matches pending.<br/>Attaches UserData to Nav.<br/>Adds to active_launches_. Note over Java: 3. Verification (Async) Java->>Java: Starts DAL Verification Note over Nav, Helper: 4. Navigation Commit Nav->>Helper: DidFinishNavigation(commit) Note over Helper: Removes from active_launches_ alt Verification already succeeded Helper->>Queue: Enqueue(params) else Verification still pending Helper->>Helper: Move to committed_launches_ (stash) end Note over Java, Helper: 5. Verification Complete Java-->>Helper: OnLaunchVerified(token, success) alt success && stashed in committed_launches_ Note over Helper: Verifies RFH is active & in-scope Helper->>Queue: Enqueue(params) end
When a TWA launch intent is received, Java's WebAppLaunchHandler determines how to route the launch parameters based on the requested LaunchHandlerClientMode and whether the current page in the tab is in scope:
NAVIGATE_NEW: Java launches a new Android Activity (which creates a new task).NAVIGATE_EXISTING: Java reuses the existing tab and triggers a C++ navigation with a token (calls PrepareForLaunch).FOCUS_EXISTING:EnqueueNonNavigating).PrepareForLaunch).navigate-existing), rather than opening a new activity.If the client mode is not specified or set to AUTO, Android defaults to NAVIGATE_EXISTING (unlike Desktop which may default to NAVIGATE_NEW).
TwaLaunchQueueTabHelper tracks launches in three maps:
pending_launches_ (token -> PendingLaunch): Holds launches prepared by Java that have not yet started navigating.active_launches_ (token -> NavigationHandle*): Holds active navigations. The launch parameters are stored in TwaLaunchNavigationHandleUserData attached to the NavigationHandle.committed_launches_ (token -> CommittedLaunch): Holds launches that committed but are waiting for DAL verification. Stores the parameters and the target RenderFrameHost ID.The centralized C++ state machine protects against several critical scope and stability edge cases:
If a TWA launch starts a navigation to a verified URL, but that page immediately redirects to a different origin:
DidFinishNavigation, we check TwaLaunchQueueDelegate::IsInScope against the final committed URL.If a tab was pre-warmed (e.g. via CustomTabsSession::mayLaunchUrl), the navigation might have started before Java received the launch intent and prepared the C++ helper. In this case, the navigation will not have a token in ChromeNavigationUIData.
has_speculative_navigation = true in PrepareForLaunch.DidFinishNavigation, if user_data is missing, we perform a fallback match by comparing the committed URL against pending_launches_ that are marked speculative.If a launch navigation is started but then aborted (e.g. replaced by a new user navigation before commit):
DidFinishNavigation is called with committed = false.active_launches_ entry to avoid dangling pointers.If a launch commits and is stashed in committed_launches_ (waiting verification), but the verification hangs or takes too long, and the user navigates away:
committed_launches_.clear().If a new launch intent arrives while a pre-existing navigation is already in progress in the tab:
PrepareForLaunch stashes the new parameters in pending_launches_.pending_launches_.pending_launches_ is only cleaned up (clearing all entries, including speculative ones) when a new launch is prepared, rather than on navigation commits.