| # Deep Dive: TWA Launch Parameters Handling | |
| 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](projects/twa-launch-params/design.md). | |
| ## Architecture Overview | |
| TWA launch parameters handling spans across the JNI boundary: | |
| - **Java (`WebAppLaunchHandler`)**: Manages the Android intent lifecycle, | |
| initiates Digital Asset Links (DAL) verification, and triggers C++ actions. | |
| - **C++ (`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. | |
| ## Life of a TWA Launch (Sequence Diagram) | |
| 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): | |
| ```mermaid | |
| 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 | |
| ``` | |
| ## Java-side Routing & Client Modes | |
| 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`**: | |
| - **In-Scope**: If the current page is already within the TWA's scope, Java | |
| delivers the parameters immediately without navigating (calls | |
| `EnqueueNonNavigating`). | |
| - **Out-of-Scope Fallback**: If the current page is out of scope (e.g. the | |
| user navigated away to an external site), Java falls back to navigating the | |
| existing tab to the target URL (calls `PrepareForLaunch`). | |
| - *Note*: This fallback models the default Android behavior of reusing and | |
| navigating the existing tab when launching an app with a URL (behaving | |
| like `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`). | |
| ## State Machine | |
| `TwaLaunchQueueTabHelper` tracks launches in three maps: | |
| 1. `pending_launches_` (`token -> PendingLaunch`): Holds launches prepared by | |
| Java that have not yet started navigating. | |
| 2. `active_launches_` (`token -> NavigationHandle*`): Holds active navigations. | |
| The launch parameters are stored in `TwaLaunchNavigationHandleUserData` | |
| attached to the `NavigationHandle`. | |
| 3. `committed_launches_` (`token -> CommittedLaunch`): Holds launches that | |
| committed but are waiting for DAL verification. Stores the parameters and the | |
| target `RenderFrameHost` ID. | |
| ## Scope Protections & Edge Cases | |
| The centralized C++ state machine protects against several critical scope and | |
| stability edge cases: | |
| ### 1. Cross-Origin Redirects (Race Mitigation) | |
| If a TWA launch starts a navigation to a verified URL, but that page immediately | |
| redirects to a different origin: | |
| - In `DidFinishNavigation`, we check `TwaLaunchQueueDelegate::IsInScope` against | |
| the final committed URL. | |
| - Since the redirected URL is cross-origin/out-of-scope, the check fails. | |
| - The parameters are discarded and NOT enqueued, preventing the redirect target | |
| from accessing the file handles. | |
| ### 2. Speculative Loads (Fallback Matching) | |
| 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`. | |
| - Java passes `has_speculative_navigation = true` in `PrepareForLaunch`. | |
| - For **initial intents** (cold start), this is true if the tab was created | |
| hidden and the speculated URL matches the target URL. | |
| - For **new intents** (warm start), this is true if a speculative navigation | |
| was started in the existing tab and the speculated URL matches the target | |
| URL. | |
| - In `DidFinishNavigation`, if `user_data` is missing, we perform a fallback | |
| match by comparing the committed URL against `pending_launches_` that are | |
| marked speculative. | |
| - Fallback matching is strictly restricted to speculative launches to prevent | |
| normal/manual navigations from accidentally adopting stale pending launches. | |
| ### 3. Aborted Navigations | |
| 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`. | |
| - We clean up the `active_launches_` entry to avoid dangling pointers. | |
| - The launch parameters are discarded as the navigation failed. We do not | |
| deliver parameters to a document that did not result from the launch | |
| navigation. | |
| ### 4. Stale Launch Cleanup | |
| 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: | |
| - When any new primary main-frame navigation commits to a new document, we call | |
| `committed_launches_.clear()`. | |
| - This prevents stale parameters from accumulating in memory and potentially | |
| being delivered if verification completes much later. | |
| ### 5. Pre-existing Navigation Commit Race | |
| 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_`. | |
| - If the pre-existing navigation commits before the new launch navigation | |
| starts, we must NOT clear `pending_launches_`. | |
| - To avoid this race, `pending_launches_` is only cleaned up (clearing all | |
| entries, including speculative ones) when a *new* launch is prepared, rather | |
| than on navigation commits. |