Back to Chromium

Design: Centralized TWA Launch Parameters Matching in C++

components/webapps/docs/projects/twa-launch-params/design.md

152.0.7965.18.4 KB
Original Source

Design: Centralized TWA Launch Parameters Matching in C++

  • Last Modified: 2026-07-14
  • Status: Implemented

Background & Concepts

Trusted Web Activities (TWAs) allow Android applications to display verified web content in a customized Custom Tab. TWAs support Web App Launch Handler API, allowing them to receive launch parameters (including file handles) from the OS (via share/file intents) and access them via window.launchQueue.setConsumer.

Previously, the launch parameters were stashed in C++ TwaLaunchQueueTabHelper as a single pending_launch_params_ field. C++ would then attach these parameters to the next primary main-frame navigation that it observed in DidStartNavigation.

Problem Statement

The previous implementation suffered from a timing race and lack of scope enforcement, leading to a potential race condition (b/523738212):

  1. Timing Race: The JNI notification from Java containing the launch parameters is asynchronous (delayed by Digital Asset Links verification). If the navigation started by the TWA launch triggers DidStartNavigation before the JNI notification arrives, the parameters are left pending. They would then be blindly attached to the next navigation, which could be an untrusted page if the TWA immediately redirected.
  2. No Scope Enforcement: C++ did not verify if the navigation destination was actually in-scope of the TWA before delivering the parameters. TwaLaunchQueueDelegate::IsInScope was a stub returning true.
  3. Complex State: Attempting to manage this state and handle edge cases (like navigation cancellations, pre-warming, etc.) across the JNI boundary with split logic in Java and C++ was highly complex and prone to errors.

Goals & Requirements

  • Security: Prevent cross-origin file handle leaks. Launch parameters must only be delivered to the specific document/navigation that was triggered by the TWA launch.
  • Robustness: Support speculative loads (pre-warmed tabs) and handle aborted navigations gracefully without losing parameters.
  • Simplification: Centralize the launch lifecycle state machine in C++ to allow reliable observation of C++ navigation events (starts, commits, redirects) and avoid complex JNI synchronization.

Design Proposal

We centralize the launch state machine in C++ TwaLaunchQueueTabHelper and use a unique launch_token to correlate Java launch intents with C++ navigations.

1. Launch Correlation via Tokens

  • Java generates a unique int64_t launch_token for each TWA launch.
  • Java passes this token to C++ navigation initiation using ChromeNavigationUIData::set_twa_launch_token().
  • Java also notifies C++ of the launch parameters via JNI PrepareForLaunch(token, params, ...).

2. C++ State Machine (TwaLaunchQueueTabHelper)

We track launches in three states:

mermaid
stateDiagram-v2
    [*] --> Pending : PrepareForLaunch(token)
    Pending --> Active : DidStartNavigation(token)
    Pending --> [*] : OnLaunchVerified(failed)

    Active --> Committed_PendingVerify : DidFinishNavigation(commit) && Verification Pending
    Active --> Enqueued : DidFinishNavigation(commit) && Verification Success
    Active --> [*] : DidFinishNavigation(abort/out-of-scope)

    Committed_PendingVerify --> Enqueued : OnLaunchVerified(success)
    Committed_PendingVerify --> [*] : OnLaunchVerified(failed) or New Document Commit
  • Pending: Launch prepared but navigation not started. Stored in pending_launches_ map.
  • Active: Navigation in progress. C++ DidStartNavigation extracts the token from ChromeNavigationUIData, finds it in pending_launches_, and associates the NavigationHandle with the token in active_launches_. TwaLaunchNavigationHandleUserData is attached to the handle.
  • Committed (Waiting Verification): Navigation committed but DAL verification is still pending. Stored in committed_launches_ along with the GlobalRenderFrameHostId.
  • Enqueued: Parameters delivered to the document's LaunchQueue.

3. JNI Interface

  • PrepareForLaunch(webContents, token, startUrl, packageName, fileUris, scopeUrl, hasSpeculativeNavigation): Prepares the C++ helper for a launch.
  • OnLaunchVerified(webContents, token, success): Called when Java DAL verification completes. Updates the state:
    • If Active: Updates status in TwaLaunchNavigationHandleUserData.
    • If Committed: Enqueues the parameters if success and RFH is still active/in-scope.
  • EnqueueNonNavigating(...): Handles "focus-existing" launches where no navigation is triggered.

4. Edge Cases

  • Speculative Launches: If a navigation was started before the launch token was available (e.g. via mayLaunchUrl), C++ will not find a token in ChromeNavigationUIData. In DidFinishNavigation, if the navigation committed and has no TwaLaunchNavigationHandleUserData, C++ falls back to matching by URL against pending_launches_ that have has_speculative_navigation set to true.
    • Cold Start: Java detects this if the CCT is created in TabCreationMode.HIDDEN and the target URL matches the speculated URL.
    • Warm Start (New Intent): Java detects this if a speculative navigation was pre-warmed in the existing tab (e.g., via a subsequent mayLaunchUrl call) by checking if CustomTabActivityTabProvider::getSpeculatedUrl() matches the target URL.
  • Aborted Navigations: If an active launch navigation is aborted, we remove it from active_launches_ but do not discard the parameters immediately, allowing them to potentially match a subsequent navigation if it was a temporary redirect or retry (though normally we expect a new launch intent to generate a new token).
  • Stale Cleanup: To prevent memory accumulation, committed_launches_ is cleared whenever any new primary main frame navigation commits to a different document.
  • Pre-existing Navigation Commit Race: If a TWA receives a new launch intent while an unrelated navigation is already in progress, PrepareForLaunch will stash the new parameters. If the pre-existing navigation commits after PrepareForLaunch but before the new launch navigation actually starts, we must ensure we do not discard the newly pending parameters. For this reason, we do not clear pending_launches_ on arbitrary navigation commits. Instead, stale entries in pending_launches_ (both speculative and non-speculative) are cleared when a new launch is prepared.

5. Scope Verification

TwaLaunchQueueDelegate::IsInScope is implemented to check if the committed URL is within the TWA scope passed from Java. This check is performed:

  1. On navigation commit in DidFinishNavigation before enqueuing or stashing.
  2. On verification completion in OnLaunchVerified before enqueuing.
  3. In EnqueueNonNavigating before enqueuing.

Alternatives Considered

Maintaining WebContentsObservers in both Java and C++

  • Cons: Resulted in too many edge cases due to the decoupling of the Java lifecycle and C++ navigation events (which can be aborted, redirected, etc. independently). Centralizing the state machine in C++ simplifies this.

Relying purely on URL matching

  • Pros: Simple JNI interface (no tokens).
  • Cons: Vulnerable to race conditions and redirects. If a page immediately redirects to a different origin, the new page's navigation might match the URL of the initial launch if we only checked the start URL. Using tokens ensures we only target the navigation initiated by the launch.

Discarding parameters immediately on any new navigation start

  • Pros: Aggressive cleanup.
  • Cons: If a background navigation (e.g., speculative prefetch or subframe) starts, it could destroy the pending launch params for the main frame navigation we care about. We only clear stashed committed launches when a new primary main frame navigation commits to a different document, which is a safer signal that the user has navigated away.

Other Considerations

  • Security: The design ensures that file handles are only delivered if the final committed document is same-origin and in-scope of the verified TWA.
  • Memory Safety: TwaLaunchQueueTabHelper observes navigation destruction and cleans up its maps to avoid dangling NavigationHandle or RenderFrameHost pointers.

References