blob: 1a25f45db6b8516003c789762d51b93ea63c80f7 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.externalnav;
import static org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider.EXTRA_BROWSER_LAUNCH_SOURCE;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.SystemClock;
import android.provider.Browser;
import android.support.annotation.IntDef;
import android.text.TextUtils;
import android.util.Pair;
import android.webkit.WebView;
import org.chromium.base.CommandLine;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.VisibleForTesting;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.ChromeSwitches;
import org.chromium.chrome.browser.IntentHandler;
import org.chromium.chrome.browser.UrlConstants;
import org.chromium.chrome.browser.customtabs.CustomTabIntentDataProvider.LaunchSourceType;
import org.chromium.chrome.browser.instantapps.InstantAppsHandler;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.tab.TabRedirectHandler;
import org.chromium.chrome.browser.util.IntentUtils;
import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.chrome.browser.webapps.WebappScopePolicy;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.ui.base.PageTransition;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Logic related to the URL overriding/intercepting functionality.
* This feature allows Chrome to convert certain navigations to Android Intents allowing
* applications like Youtube to direct users clicking on a http(s) link to their native app.
*/
public class ExternalNavigationHandler {
private static final String TAG = "UrlHandler";
// Enables debug logging on a local build.
private static final boolean DEBUG = false;
private static final String WTAI_URL_PREFIX = "wtai://wp/";
private static final String WTAI_MC_URL_PREFIX = "wtai://wp/mc;";
private static final String PLAY_PACKAGE_PARAM = "id";
private static final String PLAY_REFERRER_PARAM = "referrer";
private static final String PLAY_APP_PATH = "/store/apps/details";
private static final String PLAY_HOSTNAME = "play.google.com";
@VisibleForTesting
static final String EXTRA_BROWSER_FALLBACK_URL = "browser_fallback_url";
// An extra that may be specified on an intent:// URL that contains an encoded value for the
// referrer field passed to the market:// URL in the case where the app is not present.
@VisibleForTesting
static final String EXTRA_MARKET_REFERRER = "market_referrer";
// These values are persisted in histograms. Please do not renumber. Append only.
@IntDef({AiaIntent.FALLBACK_USED, AiaIntent.SERP, AiaIntent.OTHER})
@Retention(RetentionPolicy.SOURCE)
public @interface AiaIntent {
int FALLBACK_USED = 0;
int SERP = 1;
int OTHER = 2;
int NUM_ENTRIES = 3;
}
private final ExternalNavigationDelegate mDelegate;
/**
* Result types for checking if we should override URL loading.
* NOTE: this enum is used in UMA, do not reorder values. Changes should be append only.
* Values should be numerated from 0 and can't have gaps.
*/
@IntDef({OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT,
OverrideUrlLoadingResult.OVERRIDE_WITH_CLOBBERING_TAB,
OverrideUrlLoadingResult.OVERRIDE_WITH_ASYNC_ACTION,
OverrideUrlLoadingResult.NO_OVERRIDE})
@Retention(RetentionPolicy.SOURCE)
public @interface OverrideUrlLoadingResult {
/* We should override the URL loading and launch an intent. */
int OVERRIDE_WITH_EXTERNAL_INTENT = 0;
/* We should override the URL loading and clobber the current tab. */
int OVERRIDE_WITH_CLOBBERING_TAB = 1;
/* We should override the URL loading. The desired action will be determined
* asynchronously (e.g. by requiring user confirmation). */
int OVERRIDE_WITH_ASYNC_ACTION = 2;
/* We shouldn't override the URL loading. */
int NO_OVERRIDE = 3;
int NUM_ENTRIES = 4;
}
/**
* A constructor for UrlHandler.
*
* @param tab The tab that initiated the external intent.
*/
public ExternalNavigationHandler(Tab tab) {
this(new ExternalNavigationDelegateImpl(tab));
}
/**
* Constructs a new instance of {@link ExternalNavigationHandler}, using the injected
* {@link ExternalNavigationDelegate}.
*/
public ExternalNavigationHandler(ExternalNavigationDelegate delegate) {
mDelegate = delegate;
}
/**
* Determines whether the URL needs to be sent as an intent to the system,
* and sends it, if appropriate.
* @return Whether the URL generated an intent, caused a navigation in
* current tab, or wasn't handled at all.
*/
public @OverrideUrlLoadingResult int shouldOverrideUrlLoading(ExternalNavigationParams params) {
if (DEBUG) Log.i(TAG, "shouldOverrideUrlLoading called on " + params.getUrl());
Intent intent;
// Perform generic parsing of the URI to turn it into an Intent.
try {
intent = Intent.parseUri(params.getUrl(), Intent.URI_INTENT_SCHEME);
} catch (Exception ex) {
Log.w(TAG, "Bad URI %s", params.getUrl(), ex);
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
boolean hasBrowserFallbackUrl = false;
String browserFallbackUrl =
IntentUtils.safeGetStringExtra(intent, EXTRA_BROWSER_FALLBACK_URL);
if (browserFallbackUrl != null
&& UrlUtilities.isValidForIntentFallbackNavigation(browserFallbackUrl)) {
hasBrowserFallbackUrl = true;
} else {
browserFallbackUrl = null;
}
long time = SystemClock.elapsedRealtime();
@OverrideUrlLoadingResult
int result = shouldOverrideUrlLoadingInternal(
params, intent, hasBrowserFallbackUrl, browserFallbackUrl);
RecordHistogram.recordTimesHistogram("Android.StrictMode.OverrideUrlLoadingTime",
SystemClock.elapsedRealtime() - time, TimeUnit.MILLISECONDS);
if (result != OverrideUrlLoadingResult.NO_OVERRIDE) {
int pageTransitionCore = params.getPageTransition() & PageTransition.CORE_MASK;
boolean isFormSubmit = pageTransitionCore == PageTransition.FORM_SUBMIT;
boolean isRedirectFromFormSubmit = isFormSubmit && params.isRedirect();
if (isRedirectFromFormSubmit) {
RecordHistogram.recordBooleanHistogram(
"Android.Intent.LaunchExternalAppFormSubmitHasUserGesture",
params.hasUserGesture());
}
} else if (result == OverrideUrlLoadingResult.NO_OVERRIDE && hasBrowserFallbackUrl
&& (params.getRedirectHandler() == null
// For instance, if this is a chained fallback URL, we ignore it.
|| !params.getRedirectHandler().shouldNotOverrideUrlLoading())) {
if (InstantAppsHandler.isIntentToInstantApp(intent)) {
RecordHistogram.recordEnumeratedHistogram(
"Android.InstantApps.DirectInstantAppsIntent", AiaIntent.FALLBACK_USED,
AiaIntent.NUM_ENTRIES);
}
return clobberCurrentTabWithFallbackUrl(browserFallbackUrl, params);
}
return result;
}
private boolean resolversSubsetOf(List<ResolveInfo> infos, List<ResolveInfo> container) {
if (container == null) return false;
HashSet<ComponentName> containerSet = new HashSet<>();
for (ResolveInfo info : container) {
containerSet.add(
new ComponentName(info.activityInfo.packageName, info.activityInfo.name));
}
for (ResolveInfo info : infos) {
if (!containerSet.contains(new ComponentName(
info.activityInfo.packageName, info.activityInfo.name))) {
return false;
}
}
return true;
}
private @OverrideUrlLoadingResult int shouldOverrideUrlLoadingInternal(
ExternalNavigationParams params, Intent intent, boolean hasBrowserFallbackUrl,
String browserFallbackUrl) {
// http://crbug.com/441284 : Disallow firing external intent while Chrome is in the
// background.
if (params.isApplicationMustBeInForeground() && !mDelegate.isChromeAppInForeground()) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Chrome is not in foreground");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// http://crbug.com/464669 : Disallow firing external intent from background tab.
if (params.isBackgroundTabNavigation()) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Navigation in background tab");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// pageTransition is a combination of an enumeration (core value) and bitmask.
int pageTransitionCore = params.getPageTransition() & PageTransition.CORE_MASK;
boolean isLink = pageTransitionCore == PageTransition.LINK;
boolean isFormSubmit = pageTransitionCore == PageTransition.FORM_SUBMIT;
boolean isFromIntent = (params.getPageTransition() & PageTransition.FROM_API) != 0;
boolean isForwardBackNavigation =
(params.getPageTransition() & PageTransition.FORWARD_BACK) != 0;
boolean isExternalProtocol = !UrlUtilities.isAcceptedScheme(params.getUrl());
// http://crbug.com/169549 : If you type in a URL that then redirects in server side to an
// link that cannot be rendered by the browser, we want to show the intent picker.
boolean isTyped = (pageTransitionCore == PageTransition.TYPED)
|| ((params.getPageTransition() & PageTransition.FROM_ADDRESS_BAR) != 0);
boolean typedRedirectToExternalProtocol = isTyped && params.isRedirect()
&& isExternalProtocol;
// We do not want to show the intent picker for core types typed, bookmarks, auto toplevel,
// generated, keyword, keyword generated. See below for exception to typed URL and
// redirects:
// - http://crbug.com/143118 : URL intercepting should not be invoked on navigations
// initiated by the user in the omnibox / NTP.
// - http://crbug.com/159153 : Don't override http or https URLs from the NTP or bookmarks.
// - http://crbug.com/162106: Intent picker should not be presented on returning to a page.
// This should be covered by not showing the picker if the core type is reload.
// http://crbug.com/164194 . A navigation forwards or backwards should never trigger
// the intent picker.
if (isForwardBackNavigation) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Forward or back navigation");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// http://crbug.com/605302 : Allow Chrome to handle all pdf file downloads.
if (!isExternalProtocol && mDelegate.isPdfDownload(params.getUrl())) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: PDF downloads are now handled by Chrome");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// If accessing a file URL, ensure that the user has granted the necessary file access
// to Chrome. This check should happen for reloads, navigations, etc..., which is why
// it occurs before the subsequent blocks.
if (params.getUrl().startsWith(UrlConstants.FILE_URL_SHORT_PREFIX)
&& mDelegate.shouldRequestFileAccess(params.getUrl())) {
mDelegate.startFileIntent(intent, params.getReferrerUrl(),
params.shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent());
if (DEBUG) Log.i(TAG, "OVERRIDE_WITH_ASYNC_ACTION: Requesting filesystem access");
return OverrideUrlLoadingResult.OVERRIDE_WITH_ASYNC_ACTION;
}
// http://crbug.com/149218: We want to show the intent picker for ordinary links, providing
// the link is not an incoming intent from another application, unless it's a redirect (see
// below).
boolean linkNotFromIntent = isLink && !isFromIntent;
boolean isOnEffectiveIntentRedirect = params.getRedirectHandler() == null ? false
: params.getRedirectHandler().isOnEffectiveIntentRedirectChain();
// http://crbug.com/170925: We need to show the intent picker when we receive an intent from
// another app that 30x redirects to a YouTube/Google Maps/Play Store/Google+ URL etc.
boolean incomingIntentRedirect = (isLink && isFromIntent && params.isRedirect())
|| isOnEffectiveIntentRedirect;
// http://crbug/331571 : Do not override a navigation started from user typing.
// http://crbug/424029 : Need to stay in Chrome for an intent heading explicitly to Chrome.
// http://crbug/881740 : Relax stay in Chrome restriction for Custom Tabs.
if (params.getRedirectHandler() != null) {
TabRedirectHandler handler = params.getRedirectHandler();
boolean shouldStayInChrome = handler.shouldStayInChrome(
isExternalProtocol, mDelegate.isIntentForTrustedCallingApp(intent));
if (shouldStayInChrome || handler.shouldNotOverrideUrlLoading()) {
// http://crbug.com/659301: Handle redirects to Instant Apps out of Custom Tabs.
if (handler.isFromCustomTabIntent() && !isExternalProtocol && incomingIntentRedirect
&& !handler.shouldNavigationTypeStayInChrome()
&& mDelegate.maybeLaunchInstantApp(
params.getUrl(), params.getReferrerUrl(), true)) {
if (DEBUG) {
Log.i(TAG, "OVERRIDE_WITH_EXTERNAL_INTENT: Launching redirect to "
+ "an instant app");
}
return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
}
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: RedirectHandler decision");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
}
// http://crbug.com/181186: We need to show the intent picker when we receive a redirect
// following a form submit.
boolean isRedirectFromFormSubmit = isFormSubmit && params.isRedirect();
if (!typedRedirectToExternalProtocol) {
if (!linkNotFromIntent && !incomingIntentRedirect && !isRedirectFromFormSubmit) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Incoming intent (not a redirect)");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// http://crbug.com/839751: Require user gestures for form submits to external
// protocols.
// TODO(tedchoc): Remove the ChromeFeatureList check once we verify this change does
// not break the world.
if (isRedirectFromFormSubmit && !params.hasUserGesture()
&& ChromeFeatureList.isEnabled(
ChromeFeatureList.INTENT_BLOCK_EXTERNAL_FORM_REDIRECT_NO_GESTURE)) {
if (DEBUG) {
Log.i(TAG,
"NO_OVERRIDE: Incoming form intent attempting to redirect without "
+ "user gesture");
}
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
if (params.getRedirectHandler() != null
&& params.getRedirectHandler().isNavigationFromUserTyping()) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Navigation from user typing");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
}
// Don't override navigation from a chrome:* url to http or https. For example,
// when clicking a link in bookmarks or most visited. When navigating from such a
// page, there is clear intent to complete the navigation in Chrome.
if (params.getReferrerUrl() != null
&& params.getReferrerUrl().startsWith(UrlConstants.CHROME_URL_PREFIX)
&& (params.getUrl().startsWith(UrlConstants.HTTP_URL_PREFIX)
|| params.getUrl().startsWith(UrlConstants.HTTPS_URL_PREFIX))) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Link from an internal chrome:// page");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
if (params.getUrl().startsWith(WTAI_MC_URL_PREFIX)) {
// wtai://wp/mc;number
// number=string(phone-number)
Intent wtaiIntent = new Intent(Intent.ACTION_VIEW,
Uri.parse(WebView.SCHEME_TEL
+ params.getUrl().substring(WTAI_MC_URL_PREFIX.length())));
wtaiIntent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES);
mDelegate.startActivity(wtaiIntent, false);
if (DEBUG) Log.i(TAG, "OVERRIDE_WITH_EXTERNAL_INTENT wtai:// link handled");
RecordUserAction.record("Android.PhoneIntent");
return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
}
if (params.getUrl().startsWith(WTAI_URL_PREFIX)) {
// TODO: handle other WTAI schemes.
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Unsupported wtai:// link");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// The "about:", "chrome:", and "chrome-native:" schemes are internal to the browser;
// don't want these to be dispatched to other apps.
if (params.getUrl().startsWith(ContentUrlConstants.ABOUT_URL_SHORT_PREFIX)
|| params.getUrl().startsWith(UrlConstants.CHROME_URL_SHORT_PREFIX)
|| params.getUrl().startsWith(UrlConstants.CHROME_NATIVE_URL_SHORT_PREFIX)) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Navigating to a chrome-internal page");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// The "content:" scheme is disabled in Clank. Do not try to start an activity.
if (params.getUrl().startsWith(UrlConstants.CONTENT_URL_SHORT_PREFIX)) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Navigation to content: URL");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// Special case - It makes no sense to use an external application for a YouTube
// pairing code URL, since these match the current tab with a device (Chromecast
// or similar) it is supposed to be controlling. Using a different application
// that isn't expecting this (in particular YouTube) doesn't work.
if (params.getUrl().matches(".*youtube\\.com(\\/.*)?\\?(.+&)?pairingCode=[^&].+")) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: YouTube URL with a pairing code");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// TODO(changwan): check if we need to handle URL even when external intent is off.
if (CommandLine.getInstance().hasSwitch(
ChromeSwitches.DISABLE_EXTERNAL_INTENT_REQUESTS)) {
Log.w(TAG, "External intent handling is disabled by a command-line flag.");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// http://crbug.com/647569 : Stay in a PWA window for a URL within the same scope.
@WebappScopePolicy.NavigationDirective
int webappScopePolicyDirective = mDelegate.applyWebappScopePolicyForUrl(params.getUrl());
if (webappScopePolicyDirective
== WebappScopePolicy.NavigationDirective.IGNORE_EXTERNAL_INTENT_REQUESTS) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Stay in PWA window");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// Sanitize the Intent, ensuring web pages can not bypass browser
// security (only access to BROWSABLE activities).
intent.addCategory(Intent.CATEGORY_BROWSABLE);
// Do not target packages that have not been launched directly by the
// user at least once.
intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES);
intent.setComponent(null);
Intent selector = intent.getSelector();
if (selector != null) {
selector.addCategory(Intent.CATEGORY_BROWSABLE);
selector.setComponent(null);
}
List<ResolveInfo> resolvingInfos = mDelegate.queryIntentActivities(intent);
if (resolvingInfos == null) return OverrideUrlLoadingResult.NO_OVERRIDE;
boolean canResolveActivity = resolvingInfos.size() > 0;
String packageName = ContextUtils.getApplicationContext().getPackageName();
// Check whether the intent can be resolved. If not, we will see whether we can download it
// from the Market.
if (!canResolveActivity) {
if (hasBrowserFallbackUrl) {
return clobberCurrentTabWithFallbackUrl(browserFallbackUrl, params);
}
if (intent.getPackage() != null) {
String marketReferrer = IntentUtils.safeGetStringExtra(
intent, EXTRA_MARKET_REFERRER);
if (TextUtils.isEmpty(marketReferrer)) {
marketReferrer = packageName;
}
return sendIntentToMarket(intent.getPackage(), marketReferrer, params);
}
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Could not find an external activity to use");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
if (hasBrowserFallbackUrl) {
intent.removeExtra(EXTRA_BROWSER_FALLBACK_URL);
}
final Uri uri = intent.getData();
if (intent.getPackage() == null && uri != null
&& UrlConstants.SMS_SCHEME.equals(uri.getScheme())) {
intent.setPackage(getDefaultSmsPackageName(resolvingInfos));
} else if (uri != null && UrlConstants.TEL_SCHEME.equals(uri.getScheme())
|| (Intent.ACTION_DIAL.equals(intent.getAction()))
|| (Intent.ACTION_CALL.equals(intent.getAction()))) {
RecordUserAction.record("Android.PhoneIntent");
}
// Set the Browser application ID to us in case the user chooses Chrome
// as the app. This will make sure the link is opened in the same tab
// instead of making a new one.
intent.putExtra(Browser.EXTRA_APPLICATION_ID, packageName);
if (params.isOpenInNewTab()) intent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// Ensure intents re-target potential caller activity when we run in CCT mode.
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
mDelegate.maybeSetWindowId(intent);
mDelegate.maybeRecordAppHandlersInIntent(intent, resolvingInfos);
if (params.getReferrerUrl() != null) {
IntentHandler.setPendingReferrer(intent, params.getReferrerUrl());
}
if (params.isIncognito()) {
// In incognito mode, links that can be handled within the browser should just do so,
// without asking the user.
if (!isExternalProtocol) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Stay incognito");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
IntentHandler.setPendingIncognitoUrl(intent);
}
// Make sure webkit can handle it internally before checking for specialized
// handlers. If webkit can't handle it internally, we need to call
// startActivityIfNeeded or startActivity.
if (!isExternalProtocol) {
if (mDelegate.countSpecializedHandlers(resolvingInfos) == 0) {
if (incomingIntentRedirect
&& mDelegate.maybeLaunchInstantApp(
params.getUrl(), params.getReferrerUrl(), true)) {
if (DEBUG) Log.i(TAG, "OVERRIDE_WITH_EXTERNAL_INTENT: Instant Apps redirect");
return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
} else if (linkNotFromIntent && !params.isIncognito()
&& mDelegate.maybeLaunchInstantApp(
params.getUrl(), params.getReferrerUrl(), false)) {
if (DEBUG) Log.i(TAG, "OVERRIDE_WITH_EXTERNAL_INTENT: Instant Apps link");
return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
}
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: No specialized handler for URL");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
String delegatePreviousUrl = mDelegate.getPreviousUrl();
String previousUriString =
delegatePreviousUrl != null ? delegatePreviousUrl : params.getReferrerUrl();
if (previousUriString != null && (isLink || isFormSubmit)) {
// Current URL has at least one specialized handler available. For navigations
// within the same host, keep the navigation inside the browser unless the set of
// available apps to handle the new navigation is different. http://crbug.com/463138
URI currentUri;
URI previousUri;
try {
currentUri = new URI(params.getUrl());
previousUri = new URI(previousUriString);
} catch (Exception e) {
currentUri = null;
previousUri = null;
}
if (currentUri != null && previousUri != null
&& TextUtils.equals(currentUri.getHost(), previousUri.getHost())) {
Intent previousIntent;
try {
previousIntent =
Intent.parseUri(previousUriString, Intent.URI_INTENT_SCHEME);
} catch (Exception e) {
previousIntent = null;
}
if (previousIntent != null
&& resolversSubsetOf(resolvingInfos,
mDelegate.queryIntentActivities(previousIntent))) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Same host, no new resolvers");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
}
}
}
boolean isDirectInstantAppsIntent =
isExternalProtocol && InstantAppsHandler.isIntentToInstantApp(intent);
boolean shouldProxyForInstantApps = isDirectInstantAppsIntent && mDelegate.isSerpReferrer();
if (shouldProxyForInstantApps) {
RecordHistogram.recordEnumeratedHistogram("Android.InstantApps.DirectInstantAppsIntent",
AiaIntent.SERP, AiaIntent.NUM_ENTRIES);
intent.putExtra(InstantAppsHandler.IS_GOOGLE_SEARCH_REFERRER, true);
} else if (isDirectInstantAppsIntent) {
// For security reasons, we disable all intent:// URLs to Instant Apps that are
// not coming from SERP.
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Intent URL to an Instant App");
RecordHistogram.recordEnumeratedHistogram("Android.InstantApps.DirectInstantAppsIntent",
AiaIntent.OTHER, AiaIntent.NUM_ENTRIES);
return OverrideUrlLoadingResult.NO_OVERRIDE;
} else {
// Make sure this extra is not sent unless we've done the verification.
intent.removeExtra(InstantAppsHandler.IS_GOOGLE_SEARCH_REFERRER);
}
boolean deviceCanHandleIntent = deviceCanHandleIntent(intent);
if (params.isIncognito() && !mDelegate.willChromeHandleIntent(intent)) {
// Assume the browser can handle it if there's no activity for this intent.
if (!deviceCanHandleIntent) {
if (DEBUG) {
Log.i(TAG, "NO_OVERRIDE: Not showing alert dialog with no handler for intent");
}
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// This intent may leave Chrome. Warn the user that incognito does not carry over
// to apps out side of Chrome.
try {
if (!mDelegate.startIncognitoIntent(intent, params.getReferrerUrl(),
hasBrowserFallbackUrl ? browserFallbackUrl : null, params.getTab(),
params.shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent(),
shouldProxyForInstantApps)) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Failed to show incognito alert dialog.");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
} catch (ActivityNotFoundException e) {
// The activity that we thought was going to handle the intent no longer exists,
// so catch the exception and assume Chrome can handle it.
Log.i(TAG, "NO_OVERRIDE: Not showing alert dialog with no handler for intent");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
if (DEBUG) Log.i(TAG, "OVERRIDE_WITH_ASYNC_ACTION: Incognito navigation out");
return OverrideUrlLoadingResult.OVERRIDE_WITH_ASYNC_ACTION;
}
// Some third-party app launched Chrome with an intent, and the URL got redirected. The
// user has explicitly chosen Chrome over other intent handlers, so stay in Chrome
// unless there was a new intent handler after redirection or Chrome cannot handle it
// any more.
// Custom tabs are an exception to this rule, since at no point, the user sees an intent
// picker and "picking Chrome" is handled inside the support library.
if (params.getRedirectHandler() != null && incomingIntentRedirect) {
if (!isExternalProtocol && !params.getRedirectHandler().isFromCustomTabIntent()
&& !params.getRedirectHandler().hasNewResolver(intent)) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Custom tab redirect no handled");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
}
// The intent can be used to launch Chrome itself, record the user
// gesture here so that it can be used later.
if (params.hasUserGesture()) {
IntentWithGesturesHandler.getInstance().onNewIntentWithGesture(intent);
}
// If the only specialized intent handler is a WebAPK, set the intent's package to
// launch the WebAPK without showing the intent picker.
String targetWebApkPackageName = mDelegate.findFirstWebApkPackageName(resolvingInfos);
// We can't rely on this falling through to startActivityIfNeeded and behaving
// correctly for WebAPKs. This is because the target of the intent is the WebApk's main
// activity but that's just a bouncer which will redirect to WebApkActivity in chrome.
// To avoid bouncing indefinitely, don't override the navigation if we are currently
// showing the WebApk |params.webApkPackageName()| that we will redirect to.
if (targetWebApkPackageName != null
&& targetWebApkPackageName.equals(params.nativeClientPackageName())) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Navigation in WebApk");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
if (targetWebApkPackageName != null
&& mDelegate.countSpecializedHandlers(resolvingInfos) == 1) {
intent.setPackage(targetWebApkPackageName);
}
// http://crbug.com/831806 : Stay in the CCT if the CCT is opened by WebAPK and the url
// is within the WebAPK scope.
if (shouldStayInWebappCCT(params, resolvingInfos)) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Navigation in CCT within scope of parent webapp.");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
try {
if (deviceCanHandleIntent
&& mDelegate.startActivityIfNeeded(intent, shouldProxyForInstantApps)) {
// Assume the browser can handle it if there's no activity for this intent.
if (DEBUG) Log.i(TAG, "OVERRIDE_WITH_EXTERNAL_INTENT: startActivityIfNeeded");
return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
}
} catch (ActivityNotFoundException e) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Activity not found.");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
/**
* @return OVERRIDE_WITH_EXTERNAL_INTENT when we successfully started market activity,
* NO_OVERRIDE otherwise.
*/
private @OverrideUrlLoadingResult int sendIntentToMarket(
String packageName, String marketReferrer, ExternalNavigationParams params) {
Uri marketUri =
new Uri.Builder()
.scheme("market")
.authority("details")
.appendQueryParameter(PLAY_PACKAGE_PARAM, packageName)
.appendQueryParameter(PLAY_REFERRER_PARAM, Uri.decode(marketReferrer))
.build();
Intent intent = new Intent(Intent.ACTION_VIEW, marketUri);
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setPackage("com.android.vending");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (params.getReferrerUrl() != null) {
intent.putExtra(Intent.EXTRA_REFERRER, Uri.parse(params.getReferrerUrl()));
}
if (!deviceCanHandleIntent(intent)) {
// Exit early if the Play Store isn't available. (https://crbug.com/820709)
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Play Store not installed.");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
if (params.isIncognito()) {
if (!mDelegate.startIncognitoIntent(intent, params.getReferrerUrl(), null,
params.getTab(),
params.shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent(), false)) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Failed to show incognito alert dialog.");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
if (DEBUG) Log.i(TAG, "OVERRIDE_WITH_ASYNC_ACTION: Incognito intent to Play Store.");
return OverrideUrlLoadingResult.OVERRIDE_WITH_ASYNC_ACTION;
} else {
mDelegate.startActivity(intent, false);
if (DEBUG) Log.i(TAG, "OVERRIDE_WITH_EXTERNAL_INTENT: Intent to Play Store.");
return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
}
}
/**
* Clobber the current tab with fallback URL.
*
* @param browserFallbackUrl The fallback URL.
* @param params The external navigation params.
* @return {@link OverrideUrlLoadingResult} if the tab was clobbered, or we launched an
* intent.
*/
private @OverrideUrlLoadingResult int clobberCurrentTabWithFallbackUrl(
String browserFallbackUrl, ExternalNavigationParams params) {
// If the fallback URL is a link to Play Store, send the user to Play Store app
// instead: crbug.com/638672.
Pair<String, String> appInfo = maybeGetPlayStoreAppIdAndReferrer(browserFallbackUrl);
if (appInfo != null) {
String marketReferrer = TextUtils.isEmpty(appInfo.second)
? ContextUtils.getApplicationContext().getPackageName() : appInfo.second;
return sendIntentToMarket(appInfo.first, marketReferrer, params);
}
// For subframes, we don't support fallback url for now.
// http://crbug.com/364522.
if (!params.isMainFrame()) {
if (DEBUG) Log.i(TAG, "NO_OVERRIDE: Don't support fallback url in subframes");
return OverrideUrlLoadingResult.NO_OVERRIDE;
}
// NOTE: any further redirection from fall-back URL should not override URL loading.
// Otherwise, it can be used in chain for fingerprinting multiple app installation
// status in one shot. In order to prevent this scenario, we notify redirection
// handler that redirection from the current navigation should stay in Chrome.
if (params.getRedirectHandler() != null) {
params.getRedirectHandler().setShouldNotOverrideUrlLoadingUntilNewUrlLoading();
}
if (DEBUG) Log.i(TAG, "OVERRIDE: clobberCurrentTab called");
return mDelegate.clobberCurrentTab(browserFallbackUrl, params.getReferrerUrl());
}
/**
* If the given URL is to Google Play, extracts the package name and referrer tracking code
* from the {@param url} and returns as a Pair in that order. Otherwise returns null.
*/
private Pair<String, String> maybeGetPlayStoreAppIdAndReferrer(String url) {
Uri uri = Uri.parse(url);
if (PLAY_HOSTNAME.equals(uri.getHost()) && uri.getPath() != null
&& uri.getPath().startsWith(PLAY_APP_PATH)
&& !TextUtils.isEmpty(uri.getQueryParameter(PLAY_PACKAGE_PARAM))) {
return new Pair<String, String>(uri.getQueryParameter(PLAY_PACKAGE_PARAM),
uri.getQueryParameter(PLAY_REFERRER_PARAM));
}
return null;
}
/**
* @return Whether the |url| could be handled by an external application on the system.
*/
public boolean canExternalAppHandleUrl(String url) {
if (url.startsWith(WTAI_MC_URL_PREFIX)) return true;
try {
Intent intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
if (intent.getPackage() != null) return true;
List<ResolveInfo> resolvingInfos = mDelegate.queryIntentActivities(intent);
if (resolvingInfos != null && resolvingInfos.size() > 0) return true;
} catch (Exception ex) {
// Ignore the error.
Log.w(TAG, "Bad URI %s", url, ex);
}
return false;
}
/**
* Dispatch SMS intents to the default SMS application if applicable.
* Most SMS apps refuse to send SMS if not set as default SMS application.
*
* @param resolvingComponentNames The list of ComponentName that resolves the current intent.
*/
private String getDefaultSmsPackageName(List<ResolveInfo> resolvingComponentNames) {
String defaultSmsPackageName = mDelegate.getDefaultSmsPackageName();
if (defaultSmsPackageName == null) return null;
// Makes sure that the default SMS app actually resolves the intent.
for (ResolveInfo resolveInfo : resolvingComponentNames) {
if (defaultSmsPackageName.equals(resolveInfo.activityInfo.packageName)) {
return defaultSmsPackageName;
}
}
return null;
}
// Returns whether a navigation in a CustomTabActivity opened from a WebAPK/TWA should stay
// within the CustomTabActivity. Returns false if the navigation does not occur within a
// CustomTabActivity or the CustomTabActivity was not opened from a WebAPK/TWA.
private boolean shouldStayInWebappCCT(
ExternalNavigationParams params, List<ResolveInfo> handlers) {
Tab tab = params.getTab();
if (tab == null || !tab.isCurrentlyACustomTab() || tab.getActivity() == null) {
return false;
}
int launchSource = IntentUtils.safeGetIntExtra(
tab.getActivity().getIntent(), EXTRA_BROWSER_LAUNCH_SOURCE, LaunchSourceType.OTHER);
if (launchSource != LaunchSourceType.WEBAPK) {
return false;
}
String appId = IntentUtils.safeGetStringExtra(
tab.getActivity().getIntent(), Browser.EXTRA_APPLICATION_ID);
if (appId == null) return false;
try {
Intent.parseUri(params.getUrl(), Intent.URI_INTENT_SCHEME);
} catch (URISyntaxException ex) {
return false;
}
return ExternalNavigationDelegateImpl.getSpecializedHandlersWithFilter(handlers, appId)
.size()
> 0;
}
/**
* Returns whether or not there's an activity available to handle the intent.
*/
private boolean deviceCanHandleIntent(Intent intent) {
List<ResolveInfo> resolveInfos = mDelegate.queryIntentActivities(intent);
return resolveInfos != null && resolveInfos.size() > 0;
}
}