| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 1 | This directory holds the core logic that Chrome uses to launch external intents |
| Michael Thiessen | dac7b0a | 2022-11-04 17:20:02 | [diff] [blame] | 2 | on Android. External Navigation is surprisingly subtle, with implications on |
| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 3 | privacy, security, and platform integration. This document goes through the |
| 4 | following aspects: |
| Michael Thiessen | dac7b0a | 2022-11-04 17:20:02 | [diff] [blame] | 5 | - Motivation |
| 6 | - Examples of supported and unsupported navigations |
| 7 | - What happens when a navigation is blocked |
| 8 | - Code Structure |
| 9 | - Additional Details |
| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 10 | - Embedding of the component |
| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 11 | - Opportunities for code health improvements in the component |
| 12 | |
| Michael Thiessen | dac7b0a | 2022-11-04 17:20:02 | [diff] [blame] | 13 | Throughout this document we will be using Chrome's exercising of this component |
| 14 | for illustration. |
| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 15 | |
| Michael Thiessen | dac7b0a | 2022-11-04 17:20:02 | [diff] [blame] | 16 | # Motivation |
| 17 | |
| 18 | The goal of External Navigation in Chrome is to seamlessly and securely |
| 19 | integrate the web with the Android app ecosystem. If the user has installed an |
| 20 | app for a website, then clicking a link to that website should cause the app to |
| 21 | open. |
| 22 | |
| 23 | # Supported Flows |
| 24 | |
| 25 | There are many ways web and app developers use intents, here are a few examples: |
| 26 | |
| 27 | - Clicking a link to [https://www.youtube.com](https://www.youtube.com/) opens |
| 28 | the Youtube app. |
| 29 | - Clicking a link to |
| 30 | [intent://www.youtube.com#Intent;scheme=https;action=android.intent.action.VIEW;S.browser\_fallback\_url=https%3A%2F%2Fwww.example.com/;end](http://www.youtube.com/#Intent;scheme=https;action=android.intent.action.VIEW;S.browser_fallback_url=https%3A%2F%2Fwww.example.com/;end) |
| 31 | opens the Youtube app if installed, and example.com if not. |
| 32 | - An app opens a URL redirector in Chrome, which does a server redirect to an |
| 33 | app link without any user interaction. |
| 34 | - Clicking a link triggers an async XHR to perform sign-in, then client |
| 35 | redirects to an app after receiving the response. |
| 36 | - An app opens a CCT to start a payment, the user manually switches to their |
| 37 | bank app to validate the payment, then later returns to the CCT, which, without |
| 38 | user input, redirects back to the app after the payment is confirmed. |
| 39 | |
| 40 | # Unsupported Flows |
| 41 | |
| 42 | ## In order to protect users, there are many things we don't allow: |
| 43 | |
| 44 | - Launching an app without a |
| 45 | [Verified App Link](https://developer.android.com/training/app-links/verify-android-applinks) |
| 46 | (or specialized intent filter pre-S) if the scheme can instead be handled in |
| 47 | Chrome. |
| 48 | - Explicit intents (i.e. with a component specified), or intent without the |
| 49 | BROWSABLE category (which Chrome always adds to intents). |
| 50 | - Launching an app from a tab not currently visible to the user. |
| 51 | - Launching an app without |
| 52 | [user activation](https://html.spec.whatwg.org/multipage/interaction.html#tracking-user-activation) |
| 53 | (unless doing trusted CCT/TWA navigation). |
| 54 | - Launching an app after a long timeout (as the user won't be expecting it). |
| 55 | - Launching certain schemes like file:, content: and other chrome-internal |
| 56 | schemes. |
| 57 | - Launching Instant Apps directly on old Android versions. |
| 58 | - Launching apps from tab restore, back/forward, reload. |
| 59 | - Launching apps from fallback URLs or repeatedly attempting to launch apps with |
| 60 | the same user activation (prevents fingerprinting). |
| 61 | - Launching arbitrary URLs in other browsers that may be behind on security |
| 62 | patches. |
| 63 | |
| 64 | ## Other reasons to keep navigation in Chrome: |
| 65 | |
| 66 | - Navigations from Chrome UI initially load in Chrome, but are allowed to |
| 67 | redirect to apps. |
| 68 | - Form submissions are expected to be sent to a server, not an app, unless |
| 69 | redirected. |
| 70 | - Intents URLs load in Chrome as they were intentionally sent to Chrome, |
| 71 | though may redirect out. |
| 72 | - If an intent redirects to a URL for an Activity that the initial intent also |
| 73 | matched (i.e. the set of apps supporting the navigation hasn't changed), we |
| 74 | stay in Chrome as the user has already presumably chosen Chrome over the app. |
| 75 | - If an intent explicitly targeted the Chrome package, we interpret that as a |
| 76 | strong signal the intent was supposed to stay in Chrome even through redirects. |
| 77 | - If the user is in incognito, keep them in incognito unless Chrome can't handle |
| 78 | the URL, in which case we ask the user if they would like to leave. |
| 79 | - Same-host navigations shouldn't leave Chrome unless the list of handling apps |
| 80 | changes (eg. going from google.com/search to google.com/maps should launch the |
| 81 | Maps app, but going to google.com/search?page=2 should stay in Chrome). |
| 82 | - If in CCT or other contexts where the disambiguation dialog would be shown, |
| 83 | but Chrome doesn't have an entry to put on it, but can handle the URL, the URL |
| 84 | stays in Chrome. |
| 85 | |
| 86 | ## Debugging blocked Navigations |
| 87 | |
| 88 | When trying to debug why a navigation was blocked, it's helpful to turn on |
| 89 | "External Navigation Debug Logs" in chrome://flags, which will cause detailed |
| 90 | logging to be output to logcat under the "UrlHandler" tag. |
| 91 | |
| 92 | # What happens when a navigation is blocked |
| 93 | |
| 94 | What happens when a navigation is blocked depends on the reason for the |
| 95 | navigation being blocked. Some URLs should never be launched to apps under any |
| 96 | circumstances, like content: schemes. In cases like this, the navigation will |
| 97 | simply be ignored, and a warning will be printed to the developer console. In |
| 98 | other cases, like when the external navigation was disallowed because Chrome |
| 99 | thinks the user probably expected to stay in Chrome (like navigation from typing |
| 100 | into the URL bar), it depends on whether Chrome can handle the link or not. If |
| 101 | Chrome can handle the link itself, or a fallback URL for an intent: URI exists, |
| 102 | it simply loads in Chrome. If Chrome can't handle the link, a Message is |
| 103 | displayed to the user asking them if they would like to leave Chrome. |
| 104 | |
| 105 | # Code structure |
| 106 | |
| 107 | The vast majority of the logic controlling whether a navigation leaves Chrome |
| 108 | lives in **ExternalNavigationHandler#shouldOverrideUrlLoading**. This function |
| 109 | makes use of the **ExternalNavigationDelegate** to allow content embedders to |
| 110 | customize the behavior, and the **RedirectHandler** to track Navigation history. |
| 111 | |
| Michael Thiessen | c88960c | 2023-02-01 13:48:41 | [diff] [blame] | 112 | There are 2 ways shouldOverrideUrlLoading gets invoked: |
| Michael Thiessen | dac7b0a | 2022-11-04 17:20:02 | [diff] [blame] | 113 | |
| 114 | - Main frame navigations |
| 115 | - Called through a NavigationThrottle in intercept\_navigation\_delegate.cc |
| 116 | - Subframe navigations |
| 117 | - Only intercepted for external protocols |
| 118 | - Called through ExternalProtocolHandler::LaunchUrl |
| Michael Thiessen | dac7b0a | 2022-11-04 17:20:02 | [diff] [blame] | 119 | |
| 120 | # Additional Details |
| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 121 | |
| 122 | ## InterceptNavigationDelegateImpl |
| 123 | |
| Michael Thiessen | dac7b0a | 2022-11-04 17:20:02 | [diff] [blame] | 124 | The entrypoint to the component is usually InterceptNavigationDelegateImpl.java, |
| 125 | which layers on top of //components/navigation_interception's support for |
| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 126 | NavigationThrottles that delegate to Java for their core logic. Within the |
| 127 | context of Chrome, InterceptNavigationDelegateImpl is a per-Tab (or Tab-like, |
| Michael Thiessen | dac7b0a | 2022-11-04 17:20:02 | [diff] [blame] | 128 | e.g. OverlayPanel) object that intercepts every main frame navigation made in |
| 129 | the given Tab and determines whether the navigation should result in an external |
| 130 | intent being launched. The key method is |
| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 131 | InterceptNavigationDelegateImpl#shouldIgnoreNavigation(). This method sets up |
| 132 | state related to the current navigation and then invokes |
| 133 | ExternalNavigationHandler to do the heavy lifting of determining whether the |
| 134 | navigation should result in an external intent being launched. If so, |
| 135 | InterceptNavigationDelegateImpl does cleanup in the given Tab, including |
| 136 | restoring the navigation state to what it was before the navigation chain that |
| 137 | resulted in this intent being launched and potentially closing the Tab itself if |
| 138 | opening the Tab led to the intent launch. |
| 139 | |
| 140 | ## ExternalNavigationHandler |
| 141 | |
| 142 | ExternalNavigationHandler is the core of the component. It handles all of the |
| Michael Thiessen | dac7b0a | 2022-11-04 17:20:02 | [diff] [blame] | 143 | intent launching semantics that Chrome has accumulated over 10+ years. See the |
| 144 | list of supported and unsupported flows above - this class is responsible for |
| 145 | the vast majority of that logic. |
| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 146 | |
| Michael Thiessen | dac7b0a | 2022-11-04 17:20:02 | [diff] [blame] | 147 | ExternalNavigationHandler.java is a large and complex class. The key external |
| 148 | entrypoint is ExternalNavigationHandler#shouldOverrideUrlLoading(), and the |
| 149 | method that actually holds the core logic for when and how external intents |
| 150 | should be launched is |
| 151 | ExternalNavigationHandler#shouldOverrideUrlLoadingInternal(). |
| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 152 | |
| 153 | ## RedirectHandler |
| 154 | |
| 155 | This class tracks state across navigations in order to aid |
| 156 | InterceptNavigationDelegateImpl and ExternalNavigationHandler both in making |
| 157 | decisions on whether to launch an intent for a given navigation and in properly |
| 158 | handling the state within a Tab in the event that an intent is launched. Most |
| 159 | notably, it provides information about the redirect chain (if any) that a given |
| 160 | navigation is part of and whether the set of apps that can handle an intent |
| 161 | changes while processing the redirect chain. ExternalNavigationHandlerImpl uses |
| 162 | this information as part of its determination process (e.g., for determining |
| 163 | whether an intent can be launched from a user-typed navigation). |
| 164 | InterceptNavigationDelegateImpl also uses this information to determine how to |
| 165 | restore the navigation state in its Tab after an intent being launched. |
| 166 | |
| 167 | ## ExternalNavigationDelegate and InterceptNavigationDelegateClient |
| 168 | |
| 169 | These interfaces allow embedders to customize the behavior of the component (see |
| 170 | the next section for details). Note that they should *not* be used to customize |
| 171 | the behavior of the components for tests; if that is necessary (e.g., to stub |
| 172 | out a production method), instead make the method protected and |
| 173 | @VisibleForTesting and override it as suitable in a test subclass. |
| 174 | |
| Michael Thiessen | dac7b0a | 2022-11-04 17:20:02 | [diff] [blame] | 175 | ## Other useful information |
| 176 | |
| 177 | - Resource requests like XHR are not typically visible to the browser process, |
| 178 | but sites often perform a client redirect to an app upon their completion. To |
| 179 | support this we have a separate IPC coming from the renderer to update the |
| 180 | RedirectHandler, see InterceptNavigationDelegate::OnResourceRequestWithGesture. |
| 181 | - Client-side redirects are challenging to deal with in general, as they're not |
| 182 | considered part of the previous navigation, not directly associated with any |
| 183 | user gesture, and generally poorly defined. The RedirectHandler deals with this |
| 184 | by treating all renderer-initiated navigations without a user gesture as a |
| 185 | client redirect on the previous navigation. This can lead to indefinite |
| 186 | redirect chains, so in order to make sure an unattended page isn't redirecting, |
| 187 | and the user isn't caught off guard, a hard 15 second timeout from the |
| 188 | navigation with a user gesture is used. |
| 189 | |
| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 190 | # Embedding the Component |
| 191 | |
| 192 | To embed the component, it's necessary to install |
| 193 | InterceptNavigationDelegateImpl for each "tab" of the embedder (where a tab is |
| Scott Violet | 447e4fe | 2023-09-19 16:47:03 | [diff] [blame] | 194 | the embedder-level object that holds a WebContents). |
| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 195 | |
| 196 | There are two interfaces that the embedder must implement in order to embed the |
| 197 | component: InterceptNavigationDelegateClient and ExternalNavigationDelegate. |
| Scott Violet | 447e4fe | 2023-09-19 16:47:03 | [diff] [blame] | 198 | |
| Colin Blundell | 196d72d3 | 2020-07-14 13:10:11 | [diff] [blame] | 199 | # Opportunities for Code Health Improvements |
| 200 | - For historical reasons, there is overlap between the |
| 201 | InterceptNavigationDelegateClient and ExternalNavigationDelegate interfaces. |
| 202 | It is likely that the collective API surface could be thinned. |
| 203 | - It is also potentially even possible that the two interfaces could ultimately |
| 204 | be merged into one. |