summaryrefslogtreecommitdiff
path: root/chromium/components/external_intents
diff options
context:
space:
mode:
authorAllan Sandfeld Jensen <allan.jensen@qt.io>2020-07-16 11:45:35 +0200
committerAllan Sandfeld Jensen <allan.jensen@qt.io>2020-07-17 08:59:23 +0000
commit552906b0f222c5d5dd11b9fd73829d510980461a (patch)
tree3a11e6ed0538a81dd83b20cf3a4783e297f26d91 /chromium/components/external_intents
parent1b05827804eaf047779b597718c03e7d38344261 (diff)
downloadqtwebengine-chromium-552906b0f222c5d5dd11b9fd73829d510980461a.tar.gz
BASELINE: Update Chromium to 83.0.4103.122
Change-Id: Ie3a82f5bb0076eec2a7c6a6162326b4301ee291e Reviewed-by: Michael BrĂ¼ning <michael.bruning@qt.io>
Diffstat (limited to 'chromium/components/external_intents')
-rw-r--r--chromium/components/external_intents/OWNERS5
-rw-r--r--chromium/components/external_intents/README.md1
-rw-r--r--chromium/components/external_intents/android/BUILD.gn43
-rw-r--r--chromium/components/external_intents/android/DEPS6
-rw-r--r--chromium/components/external_intents/android/external_intents_feature_list.cc53
-rw-r--r--chromium/components/external_intents/android/external_intents_feature_list.h17
-rw-r--r--chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatureList.java70
-rw-r--r--chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsSwitches.java17
-rw-r--r--chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationDelegate.java195
-rw-r--r--chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java1155
-rw-r--r--chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationParams.java272
-rw-r--r--chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/RedirectHandler.java63
12 files changed, 1897 insertions, 0 deletions
diff --git a/chromium/components/external_intents/OWNERS b/chromium/components/external_intents/OWNERS
new file mode 100644
index 00000000000..e15eb72abd9
--- /dev/null
+++ b/chromium/components/external_intents/OWNERS
@@ -0,0 +1,5 @@
+mthiesse@chromium.org
+rsesek@chromium.org
+tedchoc@chromium.org
+twellington@chromium.org
+yfriedman@chromium.org
diff --git a/chromium/components/external_intents/README.md b/chromium/components/external_intents/README.md
new file mode 100644
index 00000000000..d1ab1755b4b
--- /dev/null
+++ b/chromium/components/external_intents/README.md
@@ -0,0 +1 @@
+Holds code related to the launching of external intents on Android.
diff --git a/chromium/components/external_intents/android/BUILD.gn b/chromium/components/external_intents/android/BUILD.gn
new file mode 100644
index 00000000000..2930dfaadd9
--- /dev/null
+++ b/chromium/components/external_intents/android/BUILD.gn
@@ -0,0 +1,43 @@
+# Copyright 2020 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.
+
+import("//build/config/android/rules.gni")
+
+android_library("java") {
+ sources = [
+ "java/src/org/chromium/components/external_intents/ExternalIntentsFeatureList.java",
+ "java/src/org/chromium/components/external_intents/ExternalIntentsSwitches.java",
+ "java/src/org/chromium/components/external_intents/ExternalNavigationDelegate.java",
+ "java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java",
+ "java/src/org/chromium/components/external_intents/ExternalNavigationParams.java",
+ "java/src/org/chromium/components/external_intents/RedirectHandler.java",
+ ]
+
+ annotation_processor_deps = [ "//base/android/jni_generator:jni_processor" ]
+ deps = [
+ "//base:base_java",
+ "//base:jni_java",
+ "//components/embedder_support/android:util_java",
+ "//content/public/android:content_java",
+ "//third_party/android_deps:androidx_annotation_annotation_java",
+ "//ui/android:ui_java",
+ "//url:gurl_java",
+ ]
+}
+
+generate_jni("jni_headers") {
+ sources = [ "java/src/org/chromium/components/external_intents/ExternalIntentsFeatureList.java" ]
+}
+
+static_library("android") {
+ sources = [
+ "external_intents_feature_list.cc",
+ "external_intents_feature_list.h",
+ ]
+
+ deps = [
+ ":jni_headers",
+ "//base",
+ ]
+}
diff --git a/chromium/components/external_intents/android/DEPS b/chromium/components/external_intents/android/DEPS
new file mode 100644
index 00000000000..d188a0d91fa
--- /dev/null
+++ b/chromium/components/external_intents/android/DEPS
@@ -0,0 +1,6 @@
+include_rules = [
+ "+components/embedder_support/android",
+
+ "-content/public/android/java",
+ "+content/public/android/java/src/org/chromium/content_public",
+]
diff --git a/chromium/components/external_intents/android/external_intents_feature_list.cc b/chromium/components/external_intents/android/external_intents_feature_list.cc
new file mode 100644
index 00000000000..e442aca9726
--- /dev/null
+++ b/chromium/components/external_intents/android/external_intents_feature_list.cc
@@ -0,0 +1,53 @@
+// Copyright 2020 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.
+
+#include "components/external_intents/android/external_intents_feature_list.h"
+
+#include <jni.h>
+#include <stddef.h>
+#include <string>
+
+#include "base/android/jni_string.h"
+#include "components/external_intents/android/jni_headers/ExternalIntentsFeatureList_jni.h"
+
+namespace external_intents {
+
+namespace {
+
+// Array of features exposed through the Java ExternalIntentsFeatureList API.
+const base::Feature* kFeaturesExposedToJava[] = {
+ &kIntentBlockExternalFormRedirectsNoGesture,
+};
+
+const base::Feature* FindFeatureExposedToJava(const std::string& feature_name) {
+ for (const auto* feature : kFeaturesExposedToJava) {
+ if (feature->name == feature_name)
+ return feature;
+ }
+ NOTREACHED()
+ << "Queried feature cannot be found in ExternalIntentsFeatureList: "
+ << feature_name;
+ return nullptr;
+}
+
+} // namespace
+
+// Alphabetical:
+const base::Feature kIntentBlockExternalFormRedirectsNoGesture{
+ "IntentBlockExternalFormRedirectsNoGesture",
+ base::FEATURE_DISABLED_BY_DEFAULT};
+
+static jboolean JNI_ExternalIntentsFeatureList_IsInitialized(JNIEnv* env) {
+ return !!base::FeatureList::GetInstance();
+}
+
+static jboolean JNI_ExternalIntentsFeatureList_IsEnabled(
+ JNIEnv* env,
+ const base::android::JavaParamRef<jstring>& jfeature_name) {
+ const base::Feature* feature = FindFeatureExposedToJava(
+ base::android::ConvertJavaStringToUTF8(env, jfeature_name));
+ return base::FeatureList::IsEnabled(*feature);
+}
+
+} // namespace external_intents
diff --git a/chromium/components/external_intents/android/external_intents_feature_list.h b/chromium/components/external_intents/android/external_intents_feature_list.h
new file mode 100644
index 00000000000..02388aafb0b
--- /dev/null
+++ b/chromium/components/external_intents/android/external_intents_feature_list.h
@@ -0,0 +1,17 @@
+// Copyright 2020 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.
+
+#ifndef COMPONENTS_EXTERNAL_INTENTS_ANDROID_EXTERNAL_INTENTS_FEATURE_LIST_H_
+#define COMPONENTS_EXTERNAL_INTENTS_ANDROID_EXTERNAL_INTENTS_FEATURE_LIST_H_
+
+#include "base/feature_list.h"
+
+namespace external_intents {
+
+// Alphabetical:
+extern const base::Feature kIntentBlockExternalFormRedirectsNoGesture;
+
+} // namespace external_intents
+
+#endif // COMPONENTS_EXTERNAL_INTENTS_ANDROID_EXTERNAL_INTENTS_FEATURE_LIST_H_
diff --git a/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatureList.java b/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatureList.java
new file mode 100644
index 00000000000..96cf47df894
--- /dev/null
+++ b/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsFeatureList.java
@@ -0,0 +1,70 @@
+// Copyright 2020 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.components.external_intents;
+
+import org.chromium.base.annotations.JNINamespace;
+import org.chromium.base.annotations.MainDex;
+import org.chromium.base.annotations.NativeMethods;
+import org.chromium.base.library_loader.LibraryLoader;
+
+/**
+ * Java accessor for base/feature_list.h state.
+ *
+ * This class provides methods to access values of feature flags registered in
+ * |kFeaturesExposedToJava| in components/external_intents/android/external_intents_feature_list.cc.
+ *
+ */
+@JNINamespace("external_intents")
+@MainDex
+public abstract class ExternalIntentsFeatureList {
+ /** Prevent instantiation. */
+ private ExternalIntentsFeatureList() {}
+
+ /**
+ * @return Whether the native FeatureList is initialized or not.
+ */
+ private static boolean isNativeInitialized() {
+ if (!LibraryLoader.getInstance().isInitialized()) return false;
+ // Even if the native library is loaded, the C++ FeatureList might not be initialized yet.
+ // In that case, accessing it will not immediately fail, but instead cause a crash later
+ // when it is initialized. Return whether the native FeatureList has been initialized,
+ // so the return value can be tested, or asserted for a more actionable stack trace
+ // on failure.
+ //
+ // The FeatureList is however guaranteed to be initialized by the time
+ // AsyncInitializationActivity#finishNativeInitialization is called.
+ return ExternalIntentsFeatureListJni.get().isInitialized();
+ }
+
+ /**
+ * Returns whether the specified feature is enabled or not.
+ *
+ * Note: Features queried through this API must be added to the array
+ * |kFeaturesExposedToJava| in
+ * components/external_intents/android/external_intents_feature_list.cc.
+ *
+ * Calling this has the side effect of bucketing this client, which may cause an experiment to
+ * be marked as active.
+ *
+ * Should be called only after native is loaded.
+ *
+ * @param featureName The name of the feature to query.
+ * @return Whether the feature is enabled or not.
+ */
+ public static boolean isEnabled(String featureName) {
+ assert isNativeInitialized();
+ return ExternalIntentsFeatureListJni.get().isEnabled(featureName);
+ }
+
+ /** Alphabetical: */
+ public static final String INTENT_BLOCK_EXTERNAL_FORM_REDIRECT_NO_GESTURE =
+ "IntentBlockExternalFormRedirectsNoGesture";
+
+ @NativeMethods
+ interface Natives {
+ boolean isInitialized();
+ boolean isEnabled(String featureName);
+ }
+}
diff --git a/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsSwitches.java b/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsSwitches.java
new file mode 100644
index 00000000000..67860830ce3
--- /dev/null
+++ b/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalIntentsSwitches.java
@@ -0,0 +1,17 @@
+// Copyright 2020 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.components.external_intents;
+
+/**
+ * Contains all of the command line switches for external intent launching.
+ */
+public abstract class ExternalIntentsSwitches {
+ /** Never forward URL requests to external intents. */
+ public static final String DISABLE_EXTERNAL_INTENT_REQUESTS =
+ "disable-external-intent-requests";
+
+ // Prevent instantiation.
+ private ExternalIntentsSwitches() {}
+}
diff --git a/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationDelegate.java b/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationDelegate.java
new file mode 100644
index 00000000000..f906d0eb00a
--- /dev/null
+++ b/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationDelegate.java
@@ -0,0 +1,195 @@
+// 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.components.external_intents;
+
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+
+import androidx.annotation.NonNull;
+
+import org.chromium.base.PackageManagerUtils;
+import org.chromium.components.external_intents.ExternalNavigationHandler.OverrideUrlLoadingResult;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A delegate for the class responsible for navigating to external applications from Chrome. Used
+ * by {@link ExternalNavigationHandler}.
+ */
+public interface ExternalNavigationDelegate {
+ /**
+ * See {@link PackageManagerUtils#queryIntentActivities(Intent, int)}
+ */
+ @NonNull
+ List<ResolveInfo> queryIntentActivities(Intent intent);
+
+ /**
+ * Determine if Chrome is the default or only handler for a given intent. If true, Chrome
+ * will handle the intent when started.
+ */
+ boolean willChromeHandleIntent(Intent intent);
+
+ /**
+ * Returns whether to disable forwarding URL requests to external intents for the passed-in URL.
+ */
+ boolean shouldDisableExternalIntentRequestsForUrl(String url);
+
+ /**
+ * Returns the number of specialized intent handlers in {@params infos}. Specialized intent
+ * handlers are intent handlers which handle only a few URLs (e.g. google maps or youtube).
+ */
+ int countSpecializedHandlers(List<ResolveInfo> infos);
+
+ /**
+ * Returns the subset of {@params infos} that are specialized intent handlers.
+ */
+ ArrayList<String> getSpecializedHandlers(List<ResolveInfo> infos);
+
+ /**
+ * Start an activity for the intent. Used for intents that must be handled externally.
+ * @param intent The intent we want to send.
+ * @param proxy Whether we need to proxy the intent through AuthenticatedProxyActivity (this is
+ * used by Instant Apps intents).
+ */
+ void startActivity(Intent intent, boolean proxy);
+
+ /**
+ * Start an activity for the intent. Used for intents that may be handled internally or
+ * externally. If the user chooses to handle the intent internally, this routine must return
+ * false.
+ * @param intent The intent we want to send.
+ * @param proxy Whether we need to proxy the intent through AuthenticatedProxyActivity (this is
+ * used by Instant Apps intents).
+ */
+ boolean startActivityIfNeeded(Intent intent, boolean proxy);
+
+ /**
+ * Display a dialog warning the user that they may be leaving Chrome by starting this
+ * intent. Give the user the opportunity to cancel the action. And if it is canceled, a
+ * navigation will happen in Chrome. Catches BadTokenExceptions caused by showing the dialog
+ * on certain devices. (crbug.com/782602)
+ * @param intent The intent for external application that will be sent.
+ * @param referrerUrl The referrer for the current navigation.
+ * @param fallbackUrl The URL to load if the user doesn't proceed with external intent.
+ * @param needsToCloseTab Whether the current tab has to be closed after the intent is sent.
+ * @param proxy Whether we need to proxy the intent through AuthenticatedProxyActivity (this is
+ * used by Instant Apps intents.
+ * @return True if the function returned error free, false if it threw an exception.
+ */
+ boolean startIncognitoIntent(Intent intent, String referrerUrl, String fallbackUrl,
+ boolean needsToCloseTab, boolean proxy);
+
+ /**
+ * @param url The requested url.
+ * @return Whether we should block the navigation and request file access before proceeding.
+ */
+ boolean shouldRequestFileAccess(String url);
+
+ /**
+ * Trigger a UI affordance that will ask the user to grant file access. After the access
+ * has been granted or denied, continue loading the specified file URL.
+ *
+ * @param intent The intent to continue loading the file URL.
+ * @param referrerUrl The HTTP referrer URL.
+ * @param needsToCloseTab Whether this action should close the current tab.
+ */
+ void startFileIntent(Intent intent, String referrerUrl, boolean needsToCloseTab);
+
+ /**
+ * Clobber the current tab and try not to pass an intent when it should be handled by Chrome
+ * so that we can deliver HTTP referrer information safely.
+ *
+ * @param url The new URL after clobbering the current tab.
+ * @param referrerUrl The HTTP referrer URL.
+ * @return OverrideUrlLoadingResult (if the tab has been clobbered, or we're launching an
+ * intent.)
+ */
+ @OverrideUrlLoadingResult
+ int clobberCurrentTab(String url, String referrerUrl);
+
+ /** Adds a window id to the intent, if necessary. */
+ void maybeSetWindowId(Intent intent);
+
+ /** Adds the package name of a specialized intent handler. */
+ void maybeRecordAppHandlersInIntent(Intent intent, List<ResolveInfo> info);
+
+ /** Records the pending referrer if desired. */
+ void maybeSetPendingReferrer(Intent intent, @NonNull String referrerUrl);
+
+ /**
+ * Adjusts any desired extras related to intents to instant apps based on the value of
+ * |insIntentToInstantApp}.
+ */
+ void maybeAdjustInstantAppExtras(Intent intent, boolean isIntentToInstantApp);
+
+ /** Invoked for intents with user gestures and records the user gesture if desired. */
+ void maybeSetUserGesture(Intent intent);
+
+ /**
+ * Records the pending incognito URL if desired. Called only if the
+ * navigation is occurring in the context of incognito mode.
+ */
+ void maybeSetPendingIncognitoUrl(Intent intent);
+
+ /**
+ * Determine if the Chrome app is in the foreground.
+ */
+ boolean isChromeAppInForeground();
+
+ /**
+ * @return Default SMS application's package name. Null if there isn't any.
+ */
+ String getDefaultSmsPackageName();
+
+ /**
+ * @return Whether the URL is a file download.
+ */
+ boolean isPdfDownload(String url);
+
+ /**
+ * Check if the URL should be handled by an instant app, or kick off an async request for an
+ * instant app banner.
+ * @param url The current URL.
+ * @param referrerUrl The referrer URL.
+ * @param isIncomingRedirect Whether we are handling an incoming redirect to an instant app.
+ * @return Whether we launched an instant app.
+ */
+ boolean maybeLaunchInstantApp(String url, String referrerUrl, boolean isIncomingRedirect);
+
+ /**
+ * @return whether this navigation is from the search results page.
+ */
+ boolean isSerpReferrer();
+
+ /**
+ * @return The previously committed URL from the WebContents.
+ */
+ String getPreviousUrl();
+
+ /**
+ * @param intent The intent to launch.
+ * @return Whether the Intent points to an app that we trust and that launched Chrome.
+ */
+ boolean isIntentForTrustedCallingApp(Intent intent);
+
+ /**
+ * @param intent The intent to launch.
+ * @return Whether the Intent points to an instant app.
+ */
+ boolean isIntentToInstantApp(Intent intent);
+
+ /**
+ * @param packageName The package to check.
+ * @return Whether the package is a valid WebAPK package.
+ */
+ boolean isValidWebApk(String packageName);
+
+ /**
+ * Gives the embedder a chance to handle the intent via the autofill assistant.
+ */
+ boolean handleWithAutofillAssistant(
+ ExternalNavigationParams params, Intent targetIntent, String browserFallbackUrl);
+}
diff --git a/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java b/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java
new file mode 100644
index 00000000000..db9e57a23ab
--- /dev/null
+++ b/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationHandler.java
@@ -0,0 +1,1155 @@
+// 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.components.external_intents;
+
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.provider.Browser;
+import android.text.TextUtils;
+import android.util.Pair;
+import android.webkit.WebView;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import org.chromium.base.CommandLine;
+import org.chromium.base.ContextUtils;
+import org.chromium.base.IntentUtils;
+import org.chromium.base.Log;
+import org.chromium.base.metrics.RecordHistogram;
+import org.chromium.base.metrics.RecordUserAction;
+import org.chromium.components.embedder_support.util.UrlConstants;
+import org.chromium.components.embedder_support.util.UrlUtilities;
+import org.chromium.content_public.common.ContentUrlConstants;
+import org.chromium.ui.base.PageTransition;
+import org.chromium.url.URI;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * 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
+ public 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
+ public 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;
+ }
+
+ // Standard Activity Actions, as defined by:
+ // https://developer.android.com/reference/android/content/Intent.html#standard-activity-actions
+ // These values are persisted in histograms. Please do not renumber.
+ @IntDef({StandardActions.MAIN, StandardActions.VIEW, StandardActions.ATTACH_DATA,
+ StandardActions.EDIT, StandardActions.PICK, StandardActions.CHOOSER,
+ StandardActions.GET_CONTENT, StandardActions.DIAL, StandardActions.CALL,
+ StandardActions.SEND, StandardActions.SENDTO, StandardActions.ANSWER,
+ StandardActions.INSERT, StandardActions.DELETE, StandardActions.RUN,
+ StandardActions.SYNC, StandardActions.PICK_ACTIVITY, StandardActions.SEARCH,
+ StandardActions.WEB_SEARCH, StandardActions.FACTORY_TEST, StandardActions.OTHER})
+ @Retention(RetentionPolicy.SOURCE)
+ @VisibleForTesting
+ public @interface StandardActions {
+ int MAIN = 0;
+ int VIEW = 1;
+ int ATTACH_DATA = 2;
+ int EDIT = 3;
+ int PICK = 4;
+ int CHOOSER = 5;
+ int GET_CONTENT = 6;
+ int DIAL = 7;
+ int CALL = 8;
+ int SEND = 9;
+ int SENDTO = 10;
+ int ANSWER = 11;
+ int INSERT = 12;
+ int DELETE = 13;
+ int RUN = 14;
+ int SYNC = 15;
+ int PICK_ACTIVITY = 16;
+ int SEARCH = 17;
+ int WEB_SEARCH = 18;
+ int FACTORY_TEST = 19;
+ int OTHER = 20;
+
+ int NUM_ENTRIES = 21;
+ }
+
+ @VisibleForTesting
+ public static final String INTENT_ACTION_HISTOGRAM =
+ "Android.Intent.OverrideUrlLoadingIntentAction";
+
+ 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;
+ }
+
+ /**
+ * 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 targetIntent;
+ // Perform generic parsing of the URI to turn it into an Intent.
+ try {
+ targetIntent = Intent.parseUri(params.getUrl(), Intent.URI_INTENT_SCHEME);
+ } catch (Exception ex) {
+ Log.w(TAG, "Bad URI %s", params.getUrl(), ex);
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ String browserFallbackUrl =
+ IntentUtils.safeGetStringExtra(targetIntent, EXTRA_BROWSER_FALLBACK_URL);
+ if (browserFallbackUrl != null
+ && !UrlUtilities.isValidForIntentFallbackNavigation(browserFallbackUrl)) {
+ browserFallbackUrl = null;
+ }
+ long time = SystemClock.elapsedRealtime();
+ @OverrideUrlLoadingResult
+ int result = shouldOverrideUrlLoadingInternal(params, targetIntent, browserFallbackUrl);
+ RecordHistogram.recordTimesHistogram(
+ "Android.StrictMode.OverrideUrlLoadingTime", SystemClock.elapsedRealtime() - time);
+
+ 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 && browserFallbackUrl != null
+ && (params.getRedirectHandler() == null
+ // For instance, if this is a chained fallback URL, we ignore it.
+ || !params.getRedirectHandler().shouldNotOverrideUrlLoading())) {
+ result = handleFallbackUrl(params, targetIntent, browserFallbackUrl);
+ }
+ if (DEBUG) printDebugShouldOverrideUrlLoadingResult(result);
+ return result;
+ }
+
+ private @OverrideUrlLoadingResult int handleFallbackUrl(
+ ExternalNavigationParams params, Intent targetIntent, String browserFallbackUrl) {
+ if (mDelegate.isIntentToInstantApp(targetIntent)) {
+ RecordHistogram.recordEnumeratedHistogram("Android.InstantApps.DirectInstantAppsIntent",
+ AiaIntent.FALLBACK_USED, AiaIntent.NUM_ENTRIES);
+ }
+ // Launch WebAPK if it can handle the URL.
+ try {
+ Intent intent = Intent.parseUri(browserFallbackUrl, Intent.URI_INTENT_SCHEME);
+ sanitizeQueryIntentActivitiesIntent(intent);
+ List<ResolveInfo> resolvingInfos = mDelegate.queryIntentActivities(intent);
+ if (!isAlreadyInTargetWebApk(resolvingInfos, params)
+ && launchWebApkIfSoleIntentHandler(resolvingInfos, intent)) {
+ return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
+ }
+ } catch (Exception e) {
+ if (DEBUG) Log.i(TAG, "Could not parse fallback url as intent");
+ }
+ return clobberCurrentTabWithFallbackUrl(browserFallbackUrl, params);
+ }
+
+ private void printDebugShouldOverrideUrlLoadingResult(int result) {
+ String resultString;
+ switch (result) {
+ case OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT:
+ resultString = "OVERRIDE_WITH_EXTERNAL_INTENT";
+ break;
+ case OverrideUrlLoadingResult.OVERRIDE_WITH_CLOBBERING_TAB:
+ resultString = "OVERRIDE_WITH_CLOBBERING_TAB";
+ break;
+ case OverrideUrlLoadingResult.OVERRIDE_WITH_ASYNC_ACTION:
+ resultString = "OVERRIDE_WITH_ASYNC_ACTION";
+ break;
+ case OverrideUrlLoadingResult.NO_OVERRIDE: // Fall through.
+ default:
+ resultString = "NO_OVERRIDE";
+ break;
+ }
+ Log.i(TAG, "shouldOverrideUrlLoading result: " + resultString);
+ }
+
+ 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;
+ }
+
+ /**
+ * http://crbug.com/441284 : Disallow firing external intent while Chrome is in the background.
+ */
+ private boolean blockExternalNavWhileBackgrounded(ExternalNavigationParams params) {
+ if (params.isApplicationMustBeInForeground() && !mDelegate.isChromeAppInForeground()) {
+ if (DEBUG) Log.i(TAG, "Chrome is not in foreground");
+ return true;
+ }
+ return false;
+ }
+
+ /** http://crbug.com/464669 : Disallow firing external intent from background tab. */
+ private boolean blockExternalNavFromBackgroundTab(ExternalNavigationParams params) {
+ if (params.isBackgroundTabNavigation()) {
+ if (DEBUG) Log.i(TAG, "Navigation in background tab");
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * http://crbug.com/164194 . A navigation forwards or backwards should never trigger the intent
+ * picker.
+ */
+ private boolean ignoreBackForwardNav(ExternalNavigationParams params) {
+ if ((params.getPageTransition() & PageTransition.FORWARD_BACK) != 0) {
+ if (DEBUG) Log.i(TAG, "Forward or back navigation");
+ return true;
+ }
+ return false;
+ }
+
+ /** http://crbug.com/605302 : Allow Chrome to handle all pdf file downloads. */
+ private boolean isInternalPdfDownload(
+ boolean isExternalProtocol, ExternalNavigationParams params) {
+ if (!isExternalProtocol && mDelegate.isPdfDownload(params.getUrl())) {
+ if (DEBUG) Log.i(TAG, "PDF downloads are now handled by Chrome");
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * If accessing a file URL, ensure that the user has granted the necessary file access
+ * to Chrome.
+ */
+ private boolean startFileIntentIfNecessary(
+ ExternalNavigationParams params, Intent targetIntent) {
+ if (params.getUrl().startsWith(UrlConstants.FILE_URL_SHORT_PREFIX)
+ && mDelegate.shouldRequestFileAccess(params.getUrl())) {
+ mDelegate.startFileIntent(targetIntent, params.getReferrerUrl(),
+ params.shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent());
+ if (DEBUG) Log.i(TAG, "Requesting filesystem access");
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isTypedRedirectToExternalProtocol(
+ ExternalNavigationParams params, int pageTransitionCore, boolean isExternalProtocol) {
+ boolean isTyped = (pageTransitionCore == PageTransition.TYPED)
+ || ((params.getPageTransition() & PageTransition.FROM_ADDRESS_BAR) != 0);
+ return isTyped && params.isRedirect() && isExternalProtocol;
+ }
+
+ /**
+ * http://crbug.com/659301: Don't stay in Chrome for Custom Tabs redirecting to Instant Apps.
+ */
+ private boolean handleCCTRedirectsToInstantApps(ExternalNavigationParams params,
+ boolean isExternalProtocol, boolean incomingIntentRedirect) {
+ RedirectHandler handler = params.getRedirectHandler();
+ if (handler == null) return false;
+ if (handler.isFromCustomTabIntent() && !isExternalProtocol && incomingIntentRedirect
+ && !handler.shouldNavigationTypeStayInApp()
+ && mDelegate.maybeLaunchInstantApp(
+ params.getUrl(), params.getReferrerUrl(), true)) {
+ if (DEBUG) {
+ Log.i(TAG, "Launching redirect to an instant app");
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private boolean redirectShouldStayInChrome(
+ ExternalNavigationParams params, boolean isExternalProtocol, Intent targetIntent) {
+ RedirectHandler handler = params.getRedirectHandler();
+ if (handler == null) return false;
+ boolean shouldStayInApp = handler.shouldStayInApp(
+ isExternalProtocol, mDelegate.isIntentForTrustedCallingApp(targetIntent));
+ if (shouldStayInApp || handler.shouldNotOverrideUrlLoading()) {
+ if (DEBUG) Log.i(TAG, "RedirectHandler decision");
+ return true;
+ }
+ return false;
+ }
+
+ /** Wrapper of check against the feature to support overriding for testing. */
+ @VisibleForTesting
+ public boolean blockExternalFormRedirectsWithoutGesture() {
+ return ExternalIntentsFeatureList.isEnabled(
+ ExternalIntentsFeatureList.INTENT_BLOCK_EXTERNAL_FORM_REDIRECT_NO_GESTURE);
+ }
+
+ /**
+ * 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.
+ */
+ private boolean preferToShowIntentPicker(ExternalNavigationParams params,
+ int pageTransitionCore, boolean isExternalProtocol, boolean isFormSubmit,
+ boolean linkNotFromIntent, boolean incomingIntentRedirect) {
+ // http://crbug.com/169549 : If you type in a URL that then redirects in server side to a
+ // link that cannot be rendered by the browser, we want to show the intent picker.
+ if (isTypedRedirectToExternalProtocol(params, pageTransitionCore, isExternalProtocol)) {
+ return true;
+ }
+ // 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 (!linkNotFromIntent && !incomingIntentRedirect && !isRedirectFromFormSubmit) {
+ if (DEBUG) Log.i(TAG, "Incoming intent (not a redirect)");
+ return false;
+ }
+ // 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 && !incomingIntentRedirect && !params.hasUserGesture()
+ && blockExternalFormRedirectsWithoutGesture()) {
+ if (DEBUG) {
+ Log.i(TAG,
+ "Incoming form intent attempting to redirect without "
+ + "user gesture");
+ }
+ return false;
+ }
+ // http://crbug/331571 : Do not override a navigation started from user typing.
+ if (params.getRedirectHandler() != null
+ && params.getRedirectHandler().isNavigationFromUserTyping()) {
+ if (DEBUG) Log.i(TAG, "Navigation from user typing");
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * http://crbug.com/159153: 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.
+ */
+ private boolean isLinkFromChromeInternalPage(ExternalNavigationParams params) {
+ if (params.getReferrerUrl() == null) return false;
+ if (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, "Link from an internal chrome:// page");
+ return true;
+ }
+ return false;
+ }
+
+ private boolean handleWtaiMcProtocol(ExternalNavigationParams params) {
+ if (!params.getUrl().startsWith(WTAI_MC_URL_PREFIX)) return false;
+ // wtai://wp/mc;number
+ // number=string(phone-number)
+ mDelegate.startActivity(
+ new Intent(Intent.ACTION_VIEW,
+ Uri.parse(WebView.SCHEME_TEL
+ + params.getUrl().substring(WTAI_MC_URL_PREFIX.length()))),
+ false);
+ if (DEBUG) Log.i(TAG, "wtai:// link handled");
+ RecordUserAction.record("Android.PhoneIntent");
+ return true;
+ }
+
+ private boolean isUnhandledWtaiProtocol(ExternalNavigationParams params) {
+ if (!params.getUrl().startsWith(WTAI_URL_PREFIX)) return false;
+ if (DEBUG) Log.i(TAG, "Unsupported wtai:// link");
+ return true;
+ }
+
+ /**
+ * The "about:", "chrome:", "chrome-native:", and "devtools:" schemes
+ * are internal to the browser; don't want these to be dispatched to other apps.
+ */
+ private boolean hasInternalScheme(
+ ExternalNavigationParams params, Intent targetIntent, boolean hasIntentScheme) {
+ String url;
+ if (hasIntentScheme) {
+ // TODO(https://crbug.com/783819): When this function is converted to GURL, we should
+ // also call fixUpUrl on this user-provided URL as the fixed-up URL is what we would end
+ // up navigating to.
+ url = targetIntent.getDataString();
+ if (url == null) return false;
+ } else {
+ url = params.getUrl();
+ }
+ if (url.startsWith(ContentUrlConstants.ABOUT_SCHEME)
+ || url.startsWith(UrlConstants.CHROME_URL_SHORT_PREFIX)
+ || url.startsWith(UrlConstants.CHROME_NATIVE_URL_SHORT_PREFIX)
+ || url.startsWith(UrlConstants.DEVTOOLS_URL_SHORT_PREFIX)) {
+ if (DEBUG) Log.i(TAG, "Navigating to a chrome-internal page");
+ return true;
+ }
+ return false;
+ }
+
+ /** The "content:" scheme is disabled in Clank. Do not try to start an activity. */
+ private boolean hasContentScheme(
+ ExternalNavigationParams params, Intent targetIntent, boolean hasIntentScheme) {
+ String url;
+ if (hasIntentScheme) {
+ url = targetIntent.getDataString();
+ if (url == null) return false;
+ } else {
+ url = params.getUrl();
+ }
+ if (!url.startsWith(UrlConstants.CONTENT_URL_SHORT_PREFIX)) return false;
+ if (DEBUG) Log.i(TAG, "Navigation to content: URL");
+ return true;
+ }
+
+ /**
+ * 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.
+ */
+ private boolean isYoutubePairingCode(ExternalNavigationParams params) {
+ // TODO(https://crbug.com/1009539): Replace this regex with proper URI parsing.
+ if (params.getUrl().matches(".*youtube\\.com(\\/.*)?\\?(.+&)?pairingCode=[^&].+")) {
+ if (DEBUG) Log.i(TAG, "YouTube URL with a pairing code");
+ return true;
+ }
+ return false;
+ }
+
+ private boolean externalIntentRequestsDisabledForUrl(ExternalNavigationParams params) {
+ // TODO(changwan): check if we need to handle URL even when external intent is off.
+ if (CommandLine.getInstance().hasSwitch(
+ ExternalIntentsSwitches.DISABLE_EXTERNAL_INTENT_REQUESTS)) {
+ Log.w(TAG, "External intent handling is disabled by a command-line flag.");
+ return true;
+ }
+
+ if (mDelegate.shouldDisableExternalIntentRequestsForUrl(params.getUrl())) {
+ if (DEBUG) Log.i(TAG, "Delegate disables external intent requests for URL.");
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * If the intent can't be resolved, we should fall back to the browserFallbackUrl, or try to
+ * find the app on the market if no fallback is provided.
+ */
+ private int handleUnresolvableIntent(
+ ExternalNavigationParams params, Intent targetIntent, String browserFallbackUrl) {
+ // Fallback URL will be handled by the caller of shouldOverrideUrlLoadingInternal.
+ if (browserFallbackUrl != null) return OverrideUrlLoadingResult.NO_OVERRIDE;
+ if (targetIntent.getPackage() != null) return handleWithMarketIntent(params, targetIntent);
+
+ if (DEBUG) Log.i(TAG, "Could not find an external activity to use");
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ private @OverrideUrlLoadingResult int handleWithMarketIntent(
+ ExternalNavigationParams params, Intent intent) {
+ String marketReferrer = IntentUtils.safeGetStringExtra(intent, EXTRA_MARKET_REFERRER);
+ if (TextUtils.isEmpty(marketReferrer)) {
+ marketReferrer = ContextUtils.getApplicationContext().getPackageName();
+ }
+ return sendIntentToMarket(intent.getPackage(), marketReferrer, params);
+ }
+
+ private boolean maybeSetSmsPackage(Intent targetIntent) {
+ final Uri uri = targetIntent.getData();
+ if (targetIntent.getPackage() == null && uri != null
+ && UrlConstants.SMS_SCHEME.equals(uri.getScheme())) {
+ List<ResolveInfo> resolvingInfos = mDelegate.queryIntentActivities(targetIntent);
+ targetIntent.setPackage(getDefaultSmsPackageName(resolvingInfos));
+ return true;
+ }
+ return false;
+ }
+
+ private void maybeRecordPhoneIntentMetrics(Intent targetIntent) {
+ final Uri uri = targetIntent.getData();
+ if (uri != null && UrlConstants.TEL_SCHEME.equals(uri.getScheme())
+ || (Intent.ACTION_DIAL.equals(targetIntent.getAction()))
+ || (Intent.ACTION_CALL.equals(targetIntent.getAction()))) {
+ RecordUserAction.record("Android.PhoneIntent");
+ }
+ }
+
+ /**
+ * In incognito mode, links that can be handled within the browser should just do so,
+ * without asking the user.
+ */
+ private boolean shouldStayInIncognito(
+ ExternalNavigationParams params, boolean isExternalProtocol) {
+ if (params.isIncognito() && !isExternalProtocol) {
+ if (DEBUG) Log.i(TAG, "Stay incognito");
+ return true;
+ }
+ return false;
+ }
+
+ private boolean fallBackToHandlingWithInstantApp(ExternalNavigationParams params,
+ boolean incomingIntentRedirect, boolean linkNotFromIntent) {
+ if (incomingIntentRedirect
+ && mDelegate.maybeLaunchInstantApp(
+ params.getUrl(), params.getReferrerUrl(), true)) {
+ if (DEBUG) Log.i(TAG, "Launching instant Apps redirect");
+ return true;
+ } else if (linkNotFromIntent && !params.isIncognito()
+ && mDelegate.maybeLaunchInstantApp(
+ params.getUrl(), params.getReferrerUrl(), false)) {
+ if (DEBUG) Log.i(TAG, "Launching instant Apps link");
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * This is the catch-all path for any intent that Chrome can handle that doesn't have a
+ * specialized external app handling it.
+ */
+ private @OverrideUrlLoadingResult int fallBackToHandlingInChrome() {
+ if (DEBUG) Log.i(TAG, "No specialized handler for URL");
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ /**
+ * 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
+ */
+ private boolean shouldStayWithinHost(ExternalNavigationParams params, boolean isLink,
+ boolean isFormSubmit, List<ResolveInfo> resolvingInfos, boolean isExternalProtocol) {
+ if (isExternalProtocol) return false;
+
+ // TODO(https://crbug.com/1009539): Replace this host parsing with a UrlUtilities or GURL
+ // function call.
+ String delegatePreviousUrl = mDelegate.getPreviousUrl();
+ String previousUriString =
+ delegatePreviousUrl != null ? delegatePreviousUrl : params.getReferrerUrl();
+ if (previousUriString == null || (!isLink && !isFormSubmit)) return false;
+
+ URI currentUri;
+ URI previousUri;
+
+ try {
+ currentUri = new URI(params.getUrl());
+ previousUri = new URI(previousUriString);
+ } catch (Exception e) {
+ return false;
+ }
+
+ if (currentUri == null || previousUri == null
+ || !TextUtils.equals(currentUri.getHost(), previousUri.getHost())) {
+ return false;
+ }
+
+ Intent previousIntent;
+ try {
+ previousIntent = Intent.parseUri(previousUriString, Intent.URI_INTENT_SCHEME);
+ } catch (Exception e) {
+ return false;
+ }
+
+ if (previousIntent != null
+ && resolversSubsetOf(
+ resolvingInfos, mDelegate.queryIntentActivities(previousIntent))) {
+ if (DEBUG) Log.i(TAG, "Same host, no new resolvers");
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * For security reasons, we disable all intent:// URLs to Instant Apps that are not coming from
+ * SERP.
+ */
+ private boolean preventDirectInstantAppsIntent(
+ boolean isDirectInstantAppsIntent, boolean shouldProxyForInstantApps) {
+ if (!isDirectInstantAppsIntent || shouldProxyForInstantApps) return false;
+ if (DEBUG) Log.i(TAG, "Intent URL to an Instant App");
+ RecordHistogram.recordEnumeratedHistogram("Android.InstantApps.DirectInstantAppsIntent",
+ AiaIntent.OTHER, AiaIntent.NUM_ENTRIES);
+ return true;
+ }
+
+ /**
+ * Prepare the intent to be sent. This function does not change the filtering for the intent,
+ * so the list if resolveInfos for the intent will be the same before and after this function.
+ */
+ private void prepareExternalIntent(Intent targetIntent, ExternalNavigationParams params,
+ List<ResolveInfo> resolvingInfos, boolean shouldProxyForInstantApps) {
+ // 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.
+ targetIntent.putExtra(Browser.EXTRA_APPLICATION_ID,
+ ContextUtils.getApplicationContext().getPackageName());
+ if (params.isOpenInNewTab()) targetIntent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true);
+ targetIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ // Ensure intents re-target potential caller activity when we run in CCT mode.
+ targetIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ mDelegate.maybeSetWindowId(targetIntent);
+ mDelegate.maybeRecordAppHandlersInIntent(targetIntent, resolvingInfos);
+
+ if (params.getReferrerUrl() != null) {
+ mDelegate.maybeSetPendingReferrer(targetIntent, params.getReferrerUrl());
+ }
+
+ if (params.isIncognito()) mDelegate.maybeSetPendingIncognitoUrl(targetIntent);
+
+ mDelegate.maybeAdjustInstantAppExtras(targetIntent, shouldProxyForInstantApps);
+
+ if (shouldProxyForInstantApps) {
+ RecordHistogram.recordEnumeratedHistogram("Android.InstantApps.DirectInstantAppsIntent",
+ AiaIntent.SERP, AiaIntent.NUM_ENTRIES);
+ }
+
+ if (params.hasUserGesture()) mDelegate.maybeSetUserGesture(targetIntent);
+ }
+
+ private @OverrideUrlLoadingResult int handleExternalIncognitoIntent(Intent targetIntent,
+ ExternalNavigationParams params, String browserFallbackUrl,
+ boolean shouldProxyForInstantApps) {
+ // This intent may leave Chrome. Warn the user that incognito does not carry over
+ // to apps out side of Chrome.
+ if (mDelegate.startIncognitoIntent(targetIntent, params.getReferrerUrl(),
+ browserFallbackUrl,
+ params.shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent(),
+ shouldProxyForInstantApps)) {
+ if (DEBUG) Log.i(TAG, "Incognito navigation out");
+ return OverrideUrlLoadingResult.OVERRIDE_WITH_ASYNC_ACTION;
+ }
+ if (DEBUG) Log.i(TAG, "Failed to show incognito alert dialog.");
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ /**
+ * If some third-party app launched Chrome with an intent, and the URL got redirected, and the
+ * user explicitly chose Chrome over other intent handlers, 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.
+ */
+ private boolean shouldKeepIntentRedirectInChrome(ExternalNavigationParams params,
+ boolean incomingIntentRedirect, List<ResolveInfo> resolvingInfos,
+ boolean isExternalProtocol) {
+ if (params.getRedirectHandler() != null && incomingIntentRedirect && !isExternalProtocol
+ && !params.getRedirectHandler().isFromCustomTabIntent()
+ && !params.getRedirectHandler().hasNewResolver(resolvingInfos)) {
+ if (DEBUG) Log.i(TAG, "Custom tab redirect no handled");
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns whether the activity belongs to a WebAPK and the URL is within the scope of the
+ * WebAPK. The WebAPK's main activity is a bouncer that redirects to WebApkActivity in Chrome.
+ * In order to avoid bouncing indefinitely, we should not override the navigation if we are
+ * currently showing the WebAPK (params#nativeClientPackageName()) that we will redirect to.
+ */
+ private boolean isAlreadyInTargetWebApk(
+ List<ResolveInfo> resolveInfos, ExternalNavigationParams params) {
+ String currentName = params.nativeClientPackageName();
+ if (currentName == null) return false;
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ ActivityInfo info = resolveInfo.activityInfo;
+ if (info != null && currentName.equals(info.packageName)) {
+ if (DEBUG) Log.i(TAG, "Already in WebAPK");
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean launchExternalIntent(Intent targetIntent, boolean shouldProxyForInstantApps) {
+ try {
+ if (!mDelegate.startActivityIfNeeded(targetIntent, shouldProxyForInstantApps)) {
+ if (DEBUG) Log.i(TAG, "The current Activity was the only targeted Activity.");
+ return false;
+ }
+ } catch (ActivityNotFoundException e) {
+ // The targeted app must have been uninstalled/disabled since we queried for Activities
+ // to handle this intent.
+ if (DEBUG) Log.i(TAG, "Activity not found.");
+ return false;
+ }
+ if (DEBUG) Log.i(TAG, "startActivityIfNeeded");
+ return true;
+ }
+
+ private boolean handleWithAutofillAssistant(
+ ExternalNavigationParams params, Intent targetIntent, String browserFallbackUrl) {
+ if (mDelegate.handleWithAutofillAssistant(params, targetIntent, browserFallbackUrl)) {
+ if (DEBUG) Log.i(TAG, "Handling with Assistant");
+ return true;
+ }
+ return false;
+ }
+
+ private @OverrideUrlLoadingResult int shouldOverrideUrlLoadingInternal(
+ ExternalNavigationParams params, Intent targetIntent,
+ @Nullable String browserFallbackUrl) {
+ sanitizeQueryIntentActivitiesIntent(targetIntent);
+
+ if (blockExternalNavWhileBackgrounded(params) || blockExternalNavFromBackgroundTab(params)
+ || ignoreBackForwardNav(params)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ if (handleWithAutofillAssistant(params, targetIntent, browserFallbackUrl)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ boolean isExternalProtocol = !UrlUtilities.isAcceptedScheme(params.getUrl());
+
+ if (isInternalPdfDownload(isExternalProtocol, params)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ // This check should happen for reloads, navigations, etc..., which is why
+ // it occurs before the subsequent blocks.
+ if (startFileIntentIfNecessary(params, targetIntent)) {
+ return OverrideUrlLoadingResult.OVERRIDE_WITH_ASYNC_ACTION;
+ }
+
+ // This should come after file intents, but before any returns of
+ // OVERRIDE_WITH_EXTERNAL_INTENT.
+ if (externalIntentRequestsDisabledForUrl(params)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ 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 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;
+
+ if (handleCCTRedirectsToInstantApps(params, isExternalProtocol, incomingIntentRedirect)) {
+ return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
+ } else if (redirectShouldStayInChrome(params, isExternalProtocol, targetIntent)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ if (!preferToShowIntentPicker(params, pageTransitionCore, isExternalProtocol, isFormSubmit,
+ linkNotFromIntent, incomingIntentRedirect)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ if (isLinkFromChromeInternalPage(params)) return OverrideUrlLoadingResult.NO_OVERRIDE;
+
+ if (handleWtaiMcProtocol(params)) {
+ return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
+ }
+ // TODO: handle other WTAI schemes.
+ if (isUnhandledWtaiProtocol(params)) return OverrideUrlLoadingResult.NO_OVERRIDE;
+
+ boolean hasIntentScheme = params.getUrl().startsWith(UrlConstants.INTENT_URL_SHORT_PREFIX)
+ || params.getUrl().startsWith(UrlConstants.APP_INTENT_URL_SHORT_PREFIX);
+ if (hasInternalScheme(params, targetIntent, hasIntentScheme)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ if (hasContentScheme(params, targetIntent, hasIntentScheme)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ if (isYoutubePairingCode(params)) return OverrideUrlLoadingResult.NO_OVERRIDE;
+
+ if (shouldStayInIncognito(params, isExternalProtocol)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ if (!maybeSetSmsPackage(targetIntent)) maybeRecordPhoneIntentMetrics(targetIntent);
+
+ if (hasIntentScheme) recordIntentActionMetrics(targetIntent);
+
+ Intent debugIntent = new Intent(targetIntent);
+ List<ResolveInfo> resolvingInfos = mDelegate.queryIntentActivities(targetIntent);
+ if (resolvingInfos.isEmpty()) {
+ return handleUnresolvableIntent(params, targetIntent, browserFallbackUrl);
+ }
+
+ if (browserFallbackUrl != null) targetIntent.removeExtra(EXTRA_BROWSER_FALLBACK_URL);
+
+ boolean hasSpecializedHandler = mDelegate.countSpecializedHandlers(resolvingInfos) > 0;
+ if (!isExternalProtocol && !hasSpecializedHandler) {
+ if (fallBackToHandlingWithInstantApp(
+ params, incomingIntentRedirect, linkNotFromIntent)) {
+ return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
+ }
+ return fallBackToHandlingInChrome();
+ }
+
+ // From this point on we should only have intents Chrome can't handle, or intents for apps
+ // with specialized handlers.
+
+ if (shouldStayWithinHost(
+ params, isLink, isFormSubmit, resolvingInfos, isExternalProtocol)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ boolean isDirectInstantAppsIntent =
+ isExternalProtocol && mDelegate.isIntentToInstantApp(targetIntent);
+ boolean shouldProxyForInstantApps = isDirectInstantAppsIntent && mDelegate.isSerpReferrer();
+ if (preventDirectInstantAppsIntent(isDirectInstantAppsIntent, shouldProxyForInstantApps)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ prepareExternalIntent(targetIntent, params, resolvingInfos, shouldProxyForInstantApps);
+ // As long as our intent resolution hasn't changed, resolvingInfos won't need to be
+ // re-computed as it won't have changed.
+ assert intentResolutionMatches(debugIntent, targetIntent);
+
+ if (params.isIncognito() && !mDelegate.willChromeHandleIntent(targetIntent)) {
+ return handleExternalIncognitoIntent(
+ targetIntent, params, browserFallbackUrl, shouldProxyForInstantApps);
+ }
+
+ if (shouldKeepIntentRedirectInChrome(
+ params, incomingIntentRedirect, resolvingInfos, isExternalProtocol)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ if (isAlreadyInTargetWebApk(resolvingInfos, params)) {
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ } else if (launchWebApkIfSoleIntentHandler(resolvingInfos, targetIntent)) {
+ return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
+ }
+ if (launchExternalIntent(targetIntent, shouldProxyForInstantApps)) {
+ return OverrideUrlLoadingResult.OVERRIDE_WITH_EXTERNAL_INTENT;
+ }
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ /**
+ * Sanitize intent to be passed to {@link ExternalNavigationDelegate#queryIntentActivities()}
+ * ensuring that web pages cannot bypass browser security.
+ */
+ private void sanitizeQueryIntentActivitiesIntent(Intent intent) {
+ intent.addCategory(Intent.CATEGORY_BROWSABLE);
+ intent.setComponent(null);
+ Intent selector = intent.getSelector();
+ if (selector != null) {
+ selector.addCategory(Intent.CATEGORY_BROWSABLE);
+ selector.setComponent(null);
+ }
+ }
+
+ /**
+ * @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, "Play Store not installed.");
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+
+ if (params.isIncognito()) {
+ if (!mDelegate.startIncognitoIntent(intent, params.getReferrerUrl(), null,
+
+ params.shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent(), false)) {
+ if (DEBUG) Log.i(TAG, "Failed to show incognito alert dialog.");
+ return OverrideUrlLoadingResult.NO_OVERRIDE;
+ }
+ if (DEBUG) Log.i(TAG, "Incognito intent to Play Store.");
+ return OverrideUrlLoadingResult.OVERRIDE_WITH_ASYNC_ACTION;
+ } else {
+ mDelegate.startActivity(intent, false);
+ if (DEBUG) Log.i(TAG, "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, "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().setShouldNotOverrideUrlLoadingOnCurrentRedirectChain();
+ }
+ if (DEBUG) Log.i(TAG, "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;
+ Intent intent;
+ try {
+ intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
+ } catch (URISyntaxException ex) {
+ // Ignore the error.
+ Log.w(TAG, "Bad URI %s", url, ex);
+ return false;
+ }
+ if (intent.getPackage() != null) return true;
+
+ List<ResolveInfo> resolvingInfos = mDelegate.queryIntentActivities(intent);
+ return resolvingInfos != null && !resolvingInfos.isEmpty();
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Launches WebAPK if the WebAPK is the sole non-browser handler for the given intent.
+ * @return Whether a WebAPK was launched.
+ */
+ private boolean launchWebApkIfSoleIntentHandler(
+ List<ResolveInfo> resolvingInfos, Intent targetIntent) {
+ ArrayList<String> packages = mDelegate.getSpecializedHandlers(resolvingInfos);
+ if (packages.size() != 1 || !mDelegate.isValidWebApk(packages.get(0))) return false;
+ Intent webApkIntent = new Intent(targetIntent);
+ webApkIntent.setPackage(packages.get(0));
+ try {
+ mDelegate.startActivity(webApkIntent, false);
+ if (DEBUG) Log.i(TAG, "Launched WebAPK");
+ return true;
+ } catch (ActivityNotFoundException e) {
+ // The WebApk must have been uninstalled/disabled since we queried for Activities to
+ // handle this intent.
+ if (DEBUG) Log.i(TAG, "WebAPK launch failed");
+ return false;
+ }
+ }
+
+ /**
+ * 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.isEmpty();
+ }
+
+ private static boolean intentResolutionMatches(Intent intent, Intent other) {
+ return intent.filterEquals(other)
+ && (intent.getSelector() == other.getSelector()
+ || intent.getSelector().filterEquals(other.getSelector()));
+ }
+
+ private void recordIntentActionMetrics(Intent intent) {
+ String action = intent.getAction();
+ @StandardActions
+ int standardAction;
+ if (TextUtils.isEmpty(action)) {
+ standardAction = StandardActions.VIEW;
+ } else {
+ standardAction = getStandardAction(action);
+ }
+ RecordHistogram.recordEnumeratedHistogram(
+ INTENT_ACTION_HISTOGRAM, standardAction, StandardActions.NUM_ENTRIES);
+ }
+
+ private @StandardActions int getStandardAction(String action) {
+ switch (action) {
+ case Intent.ACTION_MAIN:
+ return StandardActions.MAIN;
+ case Intent.ACTION_VIEW:
+ return StandardActions.VIEW;
+ case Intent.ACTION_ATTACH_DATA:
+ return StandardActions.ATTACH_DATA;
+ case Intent.ACTION_EDIT:
+ return StandardActions.EDIT;
+ case Intent.ACTION_PICK:
+ return StandardActions.PICK;
+ case Intent.ACTION_CHOOSER:
+ return StandardActions.CHOOSER;
+ case Intent.ACTION_GET_CONTENT:
+ return StandardActions.GET_CONTENT;
+ case Intent.ACTION_DIAL:
+ return StandardActions.DIAL;
+ case Intent.ACTION_CALL:
+ return StandardActions.CALL;
+ case Intent.ACTION_SEND:
+ return StandardActions.SEND;
+ case Intent.ACTION_SENDTO:
+ return StandardActions.SENDTO;
+ case Intent.ACTION_ANSWER:
+ return StandardActions.ANSWER;
+ case Intent.ACTION_INSERT:
+ return StandardActions.INSERT;
+ case Intent.ACTION_DELETE:
+ return StandardActions.DELETE;
+ case Intent.ACTION_RUN:
+ return StandardActions.RUN;
+ case Intent.ACTION_SYNC:
+ return StandardActions.SYNC;
+ case Intent.ACTION_PICK_ACTIVITY:
+ return StandardActions.PICK_ACTIVITY;
+ case Intent.ACTION_SEARCH:
+ return StandardActions.SEARCH;
+ case Intent.ACTION_WEB_SEARCH:
+ return StandardActions.WEB_SEARCH;
+ case Intent.ACTION_FACTORY_TEST:
+ return StandardActions.FACTORY_TEST;
+ default:
+ return StandardActions.OTHER;
+ }
+ }
+}
diff --git a/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationParams.java b/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationParams.java
new file mode 100644
index 00000000000..f4d40512867
--- /dev/null
+++ b/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationParams.java
@@ -0,0 +1,272 @@
+// 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.components.external_intents;
+
+/**
+ * A container object for passing navigation parameters to {@link ExternalNavigationHandler}.
+ */
+public class ExternalNavigationParams {
+ /** The URL which we are navigating to. */
+ private final String mUrl;
+
+ /** Whether we are currently in an incognito context. */
+ private final boolean mIsIncognito;
+
+ /** The referrer URL for the current navigation. */
+ private final String mReferrerUrl;
+
+ /** The page transition type for the current navigation. */
+ private final int mPageTransition;
+
+ /** Whether the current navigation is a redirect. */
+ private final boolean mIsRedirect;
+
+ /** Whether Chrome has to be in foreground for external navigation to occur. */
+ private final boolean mApplicationMustBeInForeground;
+
+ /** A redirect handler. */
+ private final RedirectHandler mRedirectHandler;
+
+ /** Whether the intent should force a new tab to open. */
+ private final boolean mOpenInNewTab;
+
+ /** Whether this navigation happens in background tab. */
+ private final boolean mIsBackgroundTabNavigation;
+
+ /** Whether this navigation happens in main frame. */
+ private final boolean mIsMainFrame;
+
+ /**
+ * The package name of the TWA or WebAPK within which the navigation is happening.
+ * Null if the navigation is not within one of these wrapping APKs.
+ */
+ private final String mNativeClientPackageName;
+
+ /** Whether this navigation is launched by user gesture. */
+ private final boolean mHasUserGesture;
+
+ /**
+ * Whether the current tab should be closed when an URL load was overridden and an
+ * intent launched.
+ */
+ private final boolean mShouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent;
+
+ private ExternalNavigationParams(String url, boolean isIncognito, String referrerUrl,
+ int pageTransition, boolean isRedirect, boolean appMustBeInForeground,
+ RedirectHandler redirectHandler, boolean openInNewTab,
+ boolean isBackgroundTabNavigation, boolean isMainFrame, String nativeClientPackageName,
+ boolean hasUserGesture,
+ boolean shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent) {
+ mUrl = url;
+ mIsIncognito = isIncognito;
+ mPageTransition = pageTransition;
+ mReferrerUrl = referrerUrl;
+ mIsRedirect = isRedirect;
+ mApplicationMustBeInForeground = appMustBeInForeground;
+ mRedirectHandler = redirectHandler;
+ mOpenInNewTab = openInNewTab;
+ mIsBackgroundTabNavigation = isBackgroundTabNavigation;
+ mIsMainFrame = isMainFrame;
+ mNativeClientPackageName = nativeClientPackageName;
+ mHasUserGesture = hasUserGesture;
+ mShouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent =
+ shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent;
+ }
+
+ /** @return The URL to potentially open externally. */
+ public String getUrl() {
+ return mUrl;
+ }
+
+ /** @return Whether we are currently in incognito mode. */
+ public boolean isIncognito() {
+ return mIsIncognito;
+ }
+
+ /** @return The referrer URL. */
+ public String getReferrerUrl() {
+ return mReferrerUrl;
+ }
+
+ /** @return The page transition for the current navigation. */
+ public int getPageTransition() {
+ return mPageTransition;
+ }
+
+ /** @return Whether the navigation is part of a redirect. */
+ public boolean isRedirect() {
+ return mIsRedirect;
+ }
+
+ /** @return Whether the application has to be in foreground to open the URL. */
+ public boolean isApplicationMustBeInForeground() {
+ return mApplicationMustBeInForeground;
+ }
+
+ /** @return The redirect handler. */
+ public RedirectHandler getRedirectHandler() {
+ return mRedirectHandler;
+ }
+
+ /**
+ * @return Whether the external navigation should be opened in a new tab if handled by Chrome
+ * through the intent picker.
+ */
+ public boolean isOpenInNewTab() {
+ return mOpenInNewTab;
+ }
+
+ /** @return Whether this navigation happens in background tab. */
+ public boolean isBackgroundTabNavigation() {
+ return mIsBackgroundTabNavigation;
+ }
+
+ /** @return Whether this navigation happens in main frame. */
+ public boolean isMainFrame() {
+ return mIsMainFrame;
+ }
+
+ /**
+ * @return The package name of the TWA or WebAPK within which the navigation is happening.
+ * Null if the navigation is not within one of these wrapping APKs.
+ */
+ public String nativeClientPackageName() {
+ return mNativeClientPackageName;
+ }
+
+ /** @return Whether this navigation is launched by user gesture. */
+ public boolean hasUserGesture() {
+ return mHasUserGesture;
+ }
+
+ /**
+ * @return Whether the current tab should be closed when an URL load was overridden and an
+ * intent launched.
+ */
+ public boolean shouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent() {
+ return mShouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent;
+ }
+
+ /** The builder for {@link ExternalNavigationParams} objects. */
+ public static class Builder {
+ /** The URL which we are navigating to. */
+ private String mUrl;
+
+ /** Whether we are currently in an incognito context. */
+ private boolean mIsIncognito;
+
+ /** The referrer URL for the current navigation. */
+ private String mReferrerUrl;
+
+ /** The page transition type for the current navigation. */
+ private int mPageTransition;
+
+ /** Whether the current navigation is a redirect. */
+ private boolean mIsRedirect;
+
+ /** Whether Chrome has to be in foreground for external navigation to occur. */
+ private boolean mApplicationMustBeInForeground;
+
+ /** A redirect handler. */
+ private RedirectHandler mRedirectHandler;
+
+ /** Whether the intent should force a new tab to open. */
+ private boolean mOpenInNewTab;
+
+ /** Whether this navigation happens in background tab. */
+ private boolean mIsBackgroundTabNavigation;
+
+ /** Whether this navigation happens in main frame. */
+ private boolean mIsMainFrame;
+
+ /**
+ * The package name of the TWA or WebAPK within which the navigation is happening.
+ * Null if the navigation is not within one of these wrapping APKs.
+ */
+ private String mNativeClientPackageName;
+
+ /** Whether this navigation is launched by user gesture. */
+ private boolean mHasUserGesture;
+
+ /**
+ * Whether the current tab should be closed when an URL load was overridden and an
+ * intent launched.
+ */
+ private boolean mShouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent;
+
+ public Builder(String url, boolean isIncognito) {
+ mUrl = url;
+ mIsIncognito = isIncognito;
+ }
+
+ public Builder(String url, boolean isIncognito, String referrer, int pageTransition,
+ boolean isRedirect) {
+ mUrl = url;
+ mIsIncognito = isIncognito;
+ mReferrerUrl = referrer;
+ mPageTransition = pageTransition;
+ mIsRedirect = isRedirect;
+ }
+
+ /** Specify whether the application must be in foreground to launch an external intent. */
+ public Builder setApplicationMustBeInForeground(boolean v) {
+ mApplicationMustBeInForeground = v;
+ return this;
+ }
+
+ /** Sets a tab redirect handler. */
+ public Builder setRedirectHandler(RedirectHandler handler) {
+ mRedirectHandler = handler;
+ return this;
+ }
+
+ /** Sets whether we want to open the intent URL in new tab, if handled by Chrome. */
+ public Builder setOpenInNewTab(boolean v) {
+ mOpenInNewTab = v;
+ return this;
+ }
+
+ /** Sets whether this navigation happens in background tab. */
+ public Builder setIsBackgroundTabNavigation(boolean v) {
+ mIsBackgroundTabNavigation = v;
+ return this;
+ }
+
+ /** Sets whether this navigation happens in main frame. */
+ public Builder setIsMainFrame(boolean v) {
+ mIsMainFrame = v;
+ return this;
+ }
+
+ /** Sets the package name of the TWA or WebAPK within which the navigation is happening. **/
+ public Builder setNativeClientPackageName(String v) {
+ mNativeClientPackageName = v;
+ return this;
+ }
+
+ /** Sets whether this navigation happens in main frame. */
+ public Builder setHasUserGesture(boolean v) {
+ mHasUserGesture = v;
+ return this;
+ }
+
+ /**
+ * Sets whether the current tab should be closed when an URL load was overridden and an
+ * intent launched.
+ */
+ public Builder setShouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent(boolean v) {
+ mShouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent = v;
+ return this;
+ }
+
+ /** @return A fully constructed {@link ExternalNavigationParams} object. */
+ public ExternalNavigationParams build() {
+ return new ExternalNavigationParams(mUrl, mIsIncognito, mReferrerUrl, mPageTransition,
+ mIsRedirect, mApplicationMustBeInForeground, mRedirectHandler, mOpenInNewTab,
+ mIsBackgroundTabNavigation, mIsMainFrame, mNativeClientPackageName,
+ mHasUserGesture, mShouldCloseContentsOnOverrideUrlLoadingAndLaunchIntent);
+ }
+ }
+}
diff --git a/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/RedirectHandler.java b/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/RedirectHandler.java
new file mode 100644
index 00000000000..a9aceff010c
--- /dev/null
+++ b/chromium/components/external_intents/android/java/src/org/chromium/components/external_intents/RedirectHandler.java
@@ -0,0 +1,63 @@
+// Copyright 2020 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.components.external_intents;
+
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+
+import java.util.List;
+
+/**
+ * This interface allows the embedder to supply the logic that ExternalNavigationHandler.java uses
+ * to determine effective navigation/redirect while handling a given intent.
+ */
+public interface RedirectHandler {
+ /**
+ * @return whether on effective intent redirect chain or not.
+ */
+ boolean isOnEffectiveIntentRedirectChain();
+
+ /**
+ * @param hasExternalProtocol whether the destination URI has an external protocol or not.
+ * @param isForTrustedCallingApp whether the app we would launch to is trusted and what launched
+ * this embedder.
+ * @return whether we should stay in this app or not.
+ */
+ boolean shouldStayInApp(boolean hasExternalProtocol, boolean isForTrustedCallingApp);
+
+ /**
+ * @return Whether the current navigation is of the type that should always stay in this app.
+ */
+ boolean shouldNavigationTypeStayInApp();
+
+ /**
+ * @return Whether this navigation is initiated by a Custom Tab {@link Intent}.
+ */
+ boolean isFromCustomTabIntent();
+
+ /**
+ * @return whether navigation is from a user's typing or not.
+ */
+ boolean isNavigationFromUserTyping();
+
+ /**
+ * Will cause shouldNotOverrideUrlLoading() to return true until a new user-initiated navigation
+ * occurs. The embedder implementation is responsible for enforcing these semantics.
+ */
+ void setShouldNotOverrideUrlLoadingOnCurrentRedirectChain();
+
+ /**
+ * @return whether we should stay in this app or not.
+ */
+ boolean shouldNotOverrideUrlLoading();
+
+ /**
+ * @return whether |resolvingInfos| contains a new resolver for the intent on which this
+ * redirect handler was operating, compared to the resolvers shown when the user chose this app
+ * to handle that intent. Relevant only if this app is one that handles incoming user
+ * intents.
+ */
+ boolean hasNewResolver(List<ResolveInfo> resolvingInfos);
+}