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.
The previous implementation suffered from a timing race and lack of scope enforcement, leading to a potential race condition (b/523738212):
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.TwaLaunchQueueDelegate::IsInScope was a stub returning true.We centralize the launch state machine in C++ TwaLaunchQueueTabHelper and use a unique launch_token to correlate Java launch intents with C++ navigations.
int64_t launch_token for each TWA launch.ChromeNavigationUIData::set_twa_launch_token().PrepareForLaunch(token, params, ...).TwaLaunchQueueTabHelper)We track launches in three states:
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_launches_ map.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_launches_ along with the GlobalRenderFrameHostId.LaunchQueue.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:TwaLaunchNavigationHandleUserData.EnqueueNonNavigating(...): Handles “focus-existing” launches where no navigation is triggered.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.TabCreationMode.HIDDEN and the target URL matches the speculated URL.mayLaunchUrl call) by checking if CustomTabActivityTabProvider::getSpeculatedUrl() matches the target URL.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).committed_launches_ is cleared whenever any new primary main frame navigation commits to a different document.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.TwaLaunchQueueDelegate::IsInScope is implemented to check if the committed URL is within the TWA scope passed from Java. This check is performed:
DidFinishNavigation before enqueuing or stashing.OnLaunchVerified before enqueuing.EnqueueNonNavigating before enqueuing.TwaLaunchQueueTabHelper observes navigation destruction and cleans up its maps to avoid dangling NavigationHandle or RenderFrameHost pointers.