This directory holds the core logic that Chrome uses to launch external intents on Android. External Navigation is surprisingly subtle, with implications on privacy, security, and platform integration. This document goes through the following aspects:
Throughout this document we will be using Chrome's exercising of this component for illustration.
The goal of External Navigation in Chrome is to seamlessly and securely integrate the web with the Android app ecosystem. If the user has installed an app for a website, then clicking a link to that website should cause the app to open.
There are many ways web and app developers use intents, here are a few examples:
When trying to debug why a navigation was blocked, it's helpful to turn on “External Navigation Debug Logs” in chrome://flags, which will cause detailed logging to be output to logcat under the “UrlHandler” tag.
What happens when a navigation is blocked depends on the reason for the navigation being blocked. Some URLs should never be launched to apps under any circumstances, like content: schemes. In cases like this, the navigation will simply be ignored, and a warning will be printed to the developer console. In other cases, like when the external navigation was disallowed because Chrome thinks the user probably expected to stay in Chrome (like navigation from typing into the URL bar), it depends on whether Chrome can handle the link or not. If Chrome can handle the link itself, or a fallback URL for an intent: URI exists, it simply loads in Chrome. If Chrome can't handle the link, a Message is displayed to the user asking them if they would like to leave Chrome.
The vast majority of the logic controlling whether a navigation leaves Chrome lives in ExternalNavigationHandler#shouldOverrideUrlLoading. This function makes use of the ExternalNavigationDelegate to allow content embedders to customize the behavior, and the RedirectHandler to track Navigation history.
There are 2 ways shouldOverrideUrlLoading gets invoked:
The entrypoint to the component is usually InterceptNavigationDelegateImpl.java, which layers on top of //components/navigation_interception's support for NavigationThrottles that delegate to Java for their core logic. Within the context of Chrome, InterceptNavigationDelegateImpl is a per-Tab (or Tab-like, e.g. OverlayPanel) object that intercepts every main frame navigation made in the given Tab and determines whether the navigation should result in an external intent being launched. The key method is InterceptNavigationDelegateImpl#shouldIgnoreNavigation(). This method sets up state related to the current navigation and then invokes ExternalNavigationHandler to do the heavy lifting of determining whether the navigation should result in an external intent being launched. If so, InterceptNavigationDelegateImpl does cleanup in the given Tab, including restoring the navigation state to what it was before the navigation chain that resulted in this intent being launched and potentially closing the Tab itself if opening the Tab led to the intent launch.
ExternalNavigationHandler is the core of the component. It handles all of the intent launching semantics that Chrome has accumulated over 10+ years. See the list of supported and unsupported flows above - this class is responsible for the vast majority of that logic.
ExternalNavigationHandler.java is a large and complex class. The key external entrypoint is ExternalNavigationHandler#shouldOverrideUrlLoading(), and the method that actually holds the core logic for when and how external intents should be launched is ExternalNavigationHandler#shouldOverrideUrlLoadingInternal().
This class tracks state across navigations in order to aid InterceptNavigationDelegateImpl and ExternalNavigationHandler both in making decisions on whether to launch an intent for a given navigation and in properly handling the state within a Tab in the event that an intent is launched. Most notably, it provides information about the redirect chain (if any) that a given navigation is part of and whether the set of apps that can handle an intent changes while processing the redirect chain. ExternalNavigationHandlerImpl uses this information as part of its determination process (e.g., for determining whether an intent can be launched from a user-typed navigation). InterceptNavigationDelegateImpl also uses this information to determine how to restore the navigation state in its Tab after an intent being launched.
These interfaces allow embedders to customize the behavior of the component (see the next section for details). Note that they should not be used to customize the behavior of the components for tests; if that is necessary (e.g., to stub out a production method), instead make the method protected and @VisibleForTesting and override it as suitable in a test subclass.
To embed the component, it‘s necessary to install InterceptNavigationDelegateImpl for each “tab” of the embedder (where a tab is the embedder-level object that holds a WebContents). For an example of a relatively straightforward embedding, look at //weblayer’s creation of InterceptNavigationDelegateImpl.
There are two interfaces that the embedder must implement in order to embed the component: InterceptNavigationDelegateClient and ExternalNavigationDelegate. Again, //weblayer‘s implementation of these interfaces provides a good starting point to follow. //chrome’s implementations are significantly more complex for several reasons: handling of differences between Chrome browser and Chrome Custom Tabs, integration with handling of incoming intents, an extended set of use cases, ....
In this section we highlight differences between the Chrome and WebLayer embeddings of this component, all of which are encapsulated in the implementations of the above-mentioned interfaces:
There are almost certainly further smaller differences, but those are the major highlights.