0

Message for blocked external navigation

In some cases where External Navigation is blocked it makes sense to
prompt the user to ask if they would like to leave Chrome. The most
common case for this is login forms that take more than 5s to complete
using an XHR or similar. Other cases like bookmarks that redirect to an
app or similar, which we don't want to have automatically leave Chrome,
can also ask the user if they would like to leave Chrome.

These cases should be extremely rare, so the Message should very rarely
be shown, but in cases where it does get shown it gives the user an
escape hatch for what would otherwise be a broken experience.

Bug: 1320502
Change-Id: I7d3f1bc1ab7e49364cd375156c5ee31f7494e768
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3606516
Reviewed-by: Yaron Friedman <yfriedman@chromium.org>
Reviewed-by: Ted Choc <tedchoc@chromium.org>
Commit-Queue: Michael Thiessen <mthiesse@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1004832}
This commit is contained in:
Michael Thiessen
2022-05-18 17:36:19 +00:00
committed by Chromium LUCI CQ
parent f3753a2522
commit eabf09f81e
16 changed files with 379 additions and 48 deletions
chrome
android
javatests
src
org
chromium
chrome
browser
test
components
tools/metrics/histograms

@ -74,6 +74,12 @@ import org.chromium.components.external_intents.ExternalNavigationHandler.Overri
import org.chromium.components.external_intents.ExternalNavigationHandler.OverrideUrlLoadingResultType;
import org.chromium.components.external_intents.InterceptNavigationDelegateImpl;
import org.chromium.components.external_intents.RedirectHandler;
import org.chromium.components.messages.MessageBannerProperties;
import org.chromium.components.messages.MessageDispatcher;
import org.chromium.components.messages.MessageDispatcherProvider;
import org.chromium.components.messages.MessageIdentifier;
import org.chromium.components.messages.MessageStateHandler;
import org.chromium.components.messages.MessagesTestHelper;
import org.chromium.content_public.browser.GlobalRenderFrameHostId;
import org.chromium.content_public.browser.LifecycleState;
import org.chromium.content_public.browser.LoadUrlParams;
@ -85,6 +91,7 @@ import org.chromium.content_public.browser.test.util.TouchCommon;
import org.chromium.net.test.EmbeddedTestServer;
import org.chromium.net.test.util.TestWebServer;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.url.GURL;
import java.util.Arrays;
@ -112,6 +119,8 @@ public class UrlOverridingTest {
private static final String BASE_PATH = "/chrome/test/data/android/url_overriding/";
private static final String NAVIGATION_FROM_TIMEOUT_PAGE =
BASE_PATH + "navigation_from_timer.html";
private static final String NAVIGATION_FROM_TIMEOUT_WITH_FALLBACK_PAGE =
BASE_PATH + "navigation_from_timer_with_fallback.html";
private static final String NAVIGATION_FROM_TIMEOUT_PARENT_FRAME_PAGE =
BASE_PATH + "navigation_from_timer_parent_frame.html";
private static final String NAVIGATION_FROM_USER_GESTURE_PAGE =
@ -153,6 +162,8 @@ public class UrlOverridingTest {
BASE_PATH + "navigation_from_prerender.html";
private static final String NAVIGATION_FROM_FENCED_FRAME =
BASE_PATH + "navigation_from_fenced_frame.html";
private static final String NAVIGATION_FROM_LONG_TIMEOUT =
BASE_PATH + "navigation_from_long_timeout.html";
private static final String OTHER_BROWSER_PACKAGE = "com.other.browser";
private static final String NON_BROWSER_PACKAGE = "not.a.browser";
@ -311,22 +322,30 @@ public class UrlOverridingTest {
});
}
private void loadUrlAndWaitForIntentUrl(
private @OverrideUrlLoadingResultType int loadUrlAndWaitForIntentUrl(
final String url, boolean needClick, boolean shouldLaunchExternalIntent) {
loadUrlAndWaitForIntentUrl(url, needClick, false, shouldLaunchExternalIntent, url, true);
return loadUrlAndWaitForIntentUrl(
url, needClick, false, shouldLaunchExternalIntent, url, true);
}
private void loadUrlAndWaitForIntentUrl(final String url, boolean needClick,
boolean createsNewTab, final boolean shouldLaunchExternalIntent,
private @OverrideUrlLoadingResultType int loadUrlAndWaitForIntentUrl(final String url,
boolean shouldLaunchExternalIntent, String expectedFinalUrl,
@PageTransition int transition) {
return loadUrlAndWaitForIntentUrl(url, false, false, shouldLaunchExternalIntent,
expectedFinalUrl, true, null, transition);
}
private @OverrideUrlLoadingResultType int loadUrlAndWaitForIntentUrl(final String url,
boolean needClick, boolean createsNewTab, final boolean shouldLaunchExternalIntent,
final String expectedFinalUrl, final boolean shouldFailNavigation) {
loadUrlAndWaitForIntentUrl(url, needClick, createsNewTab, shouldLaunchExternalIntent,
expectedFinalUrl, shouldFailNavigation, null);
return loadUrlAndWaitForIntentUrl(url, needClick, createsNewTab, shouldLaunchExternalIntent,
expectedFinalUrl, shouldFailNavigation, null, PageTransition.LINK);
}
private void loadUrlAndWaitForIntentUrl(final String url, boolean needClick,
boolean createsNewTab, final boolean shouldLaunchExternalIntent,
final String expectedFinalUrl, final boolean shouldFailNavigation,
String clickTargetId) {
private @OverrideUrlLoadingResultType int loadUrlAndWaitForIntentUrl(final String url,
boolean needClick, boolean createsNewTab, final boolean shouldLaunchExternalIntent,
final String expectedFinalUrl, final boolean shouldFailNavigation, String clickTargetId,
@PageTransition int transition) {
final CallbackHelper finishCallback = new CallbackHelper();
final CallbackHelper failCallback = new CallbackHelper();
final CallbackHelper destroyedCallback = new CallbackHelper();
@ -345,6 +364,11 @@ public class UrlOverridingTest {
Callback<Pair<GURL, OverrideUrlLoadingResult>> resultCallback =
(Pair<GURL, OverrideUrlLoadingResult> result) -> {
if (result.first.getSpec().equals(url)) return;
// Ignore the NO_OVERRIDE that comes asynchronously after clobbering the tab.
if (lastResultValue.get() == OverrideUrlLoadingResultType.OVERRIDE_WITH_CLOBBERING_TAB
&& result.second.getResultType() == OverrideUrlLoadingResultType.NO_OVERRIDE) {
return;
}
lastResultValue.set(result.second.getResultType());
};
@ -377,7 +401,7 @@ public class UrlOverridingTest {
finishCallback.waitForCallback(0, 1, 20, TimeUnit.SECONDS);
} catch (TimeoutException ex) {
Assert.fail();
return;
return OverrideUrlLoadingResultType.NO_OVERRIDE;
}
}
@ -389,7 +413,7 @@ public class UrlOverridingTest {
DOMUtils.clickNode(mActivityTestRule.getWebContents(), clickTargetId);
} catch (TimeoutException e) {
Assert.fail("Failed to click on the target node.");
return;
return OverrideUrlLoadingResultType.NO_OVERRIDE;
}
}
}
@ -399,7 +423,7 @@ public class UrlOverridingTest {
failCallback.waitForCallback(0, 1, 20, TimeUnit.SECONDS);
} catch (TimeoutException ex) {
Assert.fail("Haven't received navigation failure of intents.");
return;
return OverrideUrlLoadingResultType.NO_OVERRIDE;
}
}
@ -415,7 +439,7 @@ public class UrlOverridingTest {
destroyedCallback.waitForCallback(0, 1, 20, TimeUnit.SECONDS);
} catch (TimeoutException ex) {
Assert.fail("Intercepted new tab wasn't destroyed.");
return;
return OverrideUrlLoadingResultType.NO_OVERRIDE;
}
}
}
@ -429,7 +453,7 @@ public class UrlOverridingTest {
finishCallback.waitForCallback(1, 1, 20, TimeUnit.SECONDS);
} catch (TimeoutException ex) {
Assert.fail("Fallback URL is not loaded");
return;
return OverrideUrlLoadingResultType.NO_OVERRIDE;
}
}
}
@ -462,6 +486,8 @@ public class UrlOverridingTest {
Assert.assertEquals(1 + (hasFallbackUrl ? 1 : 0), finishCallback.getCallCount());
Assert.assertEquals(shouldFailNavigation ? 1 : 0, failCallback.getCallCount());
return lastResultValue.get();
}
private static InterceptNavigationDelegateImpl getInterceptNavigationDelegate(Tab tab) {
@ -469,6 +495,32 @@ public class UrlOverridingTest {
() -> InterceptNavigationDelegateTabHelper.get(tab));
}
private PropertyModel getCurrentExternalNavigationMessage() throws Exception {
return TestThreadUtils.runOnUiThreadBlocking(() -> {
MessageDispatcher messageDispatcher = MessageDispatcherProvider.from(
mActivityTestRule.getActivity().getWindowAndroid());
List<MessageStateHandler> messages = MessagesTestHelper.getEnqueuedMessages(
messageDispatcher, MessageIdentifier.EXTERNAL_NAVIGATION);
if (messages.isEmpty()) return null;
Assert.assertEquals(1, messages.size());
return MessagesTestHelper.getCurrentMessage(messages.get(0));
});
}
private void assertMessagePresent() throws Exception {
PackageManager pm = ContextUtils.getApplicationContext().getPackageManager();
ApplicationInfo selfInfo = ContextUtils.getApplicationContext().getApplicationInfo();
CharSequence selfLabel = pm.getApplicationLabel(selfInfo);
PropertyModel message = getCurrentExternalNavigationMessage();
Assert.assertNotNull(message);
Assert.assertThat(message.get(MessageBannerProperties.TITLE),
Matchers.containsString(selfLabel.toString()));
Assert.assertThat(message.get(MessageBannerProperties.DESCRIPTION).toString(),
Matchers.containsString(selfLabel.toString()));
Assert.assertNotNull(message.get(MessageBannerProperties.ICON));
}
@Test
@SmallTest
public void testNavigationFromTimer() {
@ -570,7 +622,9 @@ public class UrlOverridingTest {
+ Base64.encodeToString(base64FallbackUrl, Base64.URL_SAFE));
// Fallback URL from a subframe will not trigger main or sub frame navigation.
loadUrlAndWaitForIntentUrl(originalUrl, true, false);
@OverrideUrlLoadingResultType
int result = loadUrlAndWaitForIntentUrl(originalUrl, true, false);
Assert.assertEquals(OverrideUrlLoadingResultType.NO_OVERRIDE, result);
}
@Test
@ -586,7 +640,7 @@ public class UrlOverridingTest {
public void testOpenWindowFromLinkUserGesture() {
mActivityTestRule.startMainActivityOnBlankPage();
loadUrlAndWaitForIntentUrl(mTestServer.getURL(OPEN_WINDOW_FROM_LINK_USER_GESTURE_PAGE),
true, true, true, null, true, "link");
true, true, true, null, true, "link", PageTransition.LINK);
}
@Test
@ -594,7 +648,7 @@ public class UrlOverridingTest {
public void testOpenWindowFromSvgUserGesture() {
mActivityTestRule.startMainActivityOnBlankPage();
loadUrlAndWaitForIntentUrl(mTestServer.getURL(OPEN_WINDOW_FROM_SVG_USER_GESTURE_PAGE), true,
true, true, null, true, "link");
true, true, null, true, "link", PageTransition.LINK);
}
@Test
@ -690,7 +744,8 @@ public class UrlOverridingTest {
public void testIntentURIWithFileSchemeDoesNothing() throws TimeoutException {
mActivityTestRule.startMainActivityOnBlankPage();
String originalUrl = mTestServer.getURL(NAVIGATION_TO_FILE_SCHEME_FROM_INTENT_URI);
loadUrlAndWaitForIntentUrl(originalUrl, true, true, false, null, true, "scheme_file");
loadUrlAndWaitForIntentUrl(
originalUrl, true, true, false, null, true, "scheme_file", PageTransition.LINK);
}
@Test
@ -698,8 +753,8 @@ public class UrlOverridingTest {
public void testIntentURIWithMixedCaseFileSchemeDoesNothing() throws TimeoutException {
mActivityTestRule.startMainActivityOnBlankPage();
String originalUrl = mTestServer.getURL(NAVIGATION_TO_FILE_SCHEME_FROM_INTENT_URI);
loadUrlAndWaitForIntentUrl(
originalUrl, true, true, false, null, true, "scheme_mixed_case_file");
loadUrlAndWaitForIntentUrl(originalUrl, true, true, false, null, true,
"scheme_mixed_case_file", PageTransition.LINK);
}
@Test
@ -707,7 +762,8 @@ public class UrlOverridingTest {
public void testIntentURIWithNoSchemeDoesNothing() throws TimeoutException {
mActivityTestRule.startMainActivityOnBlankPage();
String originalUrl = mTestServer.getURL(NAVIGATION_TO_FILE_SCHEME_FROM_INTENT_URI);
loadUrlAndWaitForIntentUrl(originalUrl, true, true, false, null, true, "null_scheme");
loadUrlAndWaitForIntentUrl(
originalUrl, true, true, false, null, true, "null_scheme", PageTransition.LINK);
}
@Test
@ -715,15 +771,18 @@ public class UrlOverridingTest {
public void testIntentURIWithEmptySchemeDoesNothing() throws TimeoutException {
mActivityTestRule.startMainActivityOnBlankPage();
String originalUrl = mTestServer.getURL(NAVIGATION_TO_FILE_SCHEME_FROM_INTENT_URI);
loadUrlAndWaitForIntentUrl(originalUrl, true, true, false, null, true, "empty_scheme");
loadUrlAndWaitForIntentUrl(
originalUrl, true, true, false, null, true, "empty_scheme", PageTransition.LINK);
}
@Test
@LargeTest
public void testSubframeLoadCannotLaunchPlayApp() throws TimeoutException {
mActivityTestRule.startMainActivityOnBlankPage();
loadUrlAndWaitForIntentUrl(
@OverrideUrlLoadingResultType
int result = loadUrlAndWaitForIntentUrl(
mTestServer.getURL(SUBFRAME_REDIRECT_WITH_PLAY_FALLBACK), false, false);
Assert.assertEquals(OverrideUrlLoadingResultType.NO_OVERRIDE, result);
}
private void runRedirectToOtherBrowserTest(Instrumentation.ActivityResult chooserResult) {
@ -881,11 +940,16 @@ public class UrlOverridingTest {
// Page redirects to intent: URL.
finishCallback.waitForCallback(2);
// With RedirectHandler state cleared, this should be treated as a navigation without a
// user gesture, and so should not allow external navigation.
Assert.assertEquals(OverrideUrlLoadingResultType.NO_OVERRIDE, lastResultValue.get());
// user gesture, which will use a Message to ask the user if they would like to follow the
// external navigation.
Assert.assertEquals(
OverrideUrlLoadingResultType.OVERRIDE_WITH_ASYNC_ACTION, lastResultValue.get());
Assert.assertTrue(mLastNavigationHandle.get().getUrl().getSpec().startsWith("intent://"));
syncHelper.notifyCalled();
Assert.assertNotNull(getCurrentExternalNavigationMessage());
}
@Test
@ -1022,4 +1086,51 @@ public class UrlOverridingTest {
Criteria.checkThat(monitor.getHits(), Matchers.is(1));
}, 10000L, CriteriaHelper.DEFAULT_POLLING_INTERVAL);
}
@Test
@LargeTest
public void testExternalNavigationMessage() throws Exception {
mActivityTestRule.startMainActivityOnBlankPage();
GURL url = new GURL(mTestServer.getURL(NAVIGATION_FROM_LONG_TIMEOUT));
@OverrideUrlLoadingResultType
int result = loadUrlAndWaitForIntentUrl(url.getSpec(), true, false);
Assert.assertEquals(OverrideUrlLoadingResultType.OVERRIDE_WITH_ASYNC_ACTION, result);
assertMessagePresent();
}
@Test
@LargeTest
public void testRedirectFromBookmark() throws Exception {
mActivityTestRule.startMainActivityOnBlankPage();
String url = mTestServer.getURL(NAVIGATION_FROM_TIMEOUT_PAGE);
@OverrideUrlLoadingResultType
int result = loadUrlAndWaitForIntentUrl(url, false, null, PageTransition.AUTO_BOOKMARK);
Assert.assertEquals(OverrideUrlLoadingResultType.OVERRIDE_WITH_ASYNC_ACTION, result);
assertMessagePresent();
}
@Test
@LargeTest
public void testRedirectFromBookmarkWithFallback() throws Exception {
mActivityTestRule.startMainActivityOnBlankPage();
String fallbackUrl = mTestServer.getURL(FALLBACK_LANDING_PATH);
String originalUrl = mTestServer.getURL(NAVIGATION_FROM_TIMEOUT_WITH_FALLBACK_PAGE
+ "?replace_text="
+ Base64.encodeToString(
ApiCompatibilityUtils.getBytesUtf8("PARAM_FALLBACK_URL"), Base64.URL_SAFE)
+ ":"
+ Base64.encodeToString(
ApiCompatibilityUtils.getBytesUtf8(fallbackUrl), Base64.URL_SAFE));
@OverrideUrlLoadingResultType
int result = loadUrlAndWaitForIntentUrl(
originalUrl, false, fallbackUrl, PageTransition.AUTO_BOOKMARK);
Assert.assertEquals(OverrideUrlLoadingResultType.OVERRIDE_WITH_CLOBBERING_TAB, result);
Assert.assertNull(getCurrentExternalNavigationMessage());
}
}

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0" />
<script>
function waitAndOpenApp() {
window.setTimeout(function () {
window.location.replace(
'intent://test/#Intent;scheme=externalappscheme;end'
);
}, 6000);
};
</script>
</head>
<body style='height:10000px;' onclick='waitAndOpenApp();'>
Click page to open App!!
</body>
</html>

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<script>
function openHello() {
window.location = 'intent://test/#Intent;scheme=externalappscheme;S.browser_fallback_url=PARAM_FALLBACK_URL;end';
};
setTimeout(openHello, 2000)
</script>
</head>
<body>
Welcome to Hello.
</body>
</html>

@ -345,6 +345,7 @@
</if>
<if expr="is_android">
<part file="android_system_error_page_strings.grdp" />
<part file="external_intents_strings.grdp" />
</if>
<if expr="is_ios">
<part file="management_ios_strings.grdp" />

@ -23,7 +23,9 @@ android_library("java") {
"//base:jni_java",
"//build/android:build_java",
"//components/embedder_support/android:util_java",
"//components/messages/android:java",
"//components/navigation_interception/android:navigation_interception_java",
"//components/strings:components_strings_grd",
"//components/url_formatter/android:url_formatter_java",
"//components/webapk/android/libs/client:java",
"//content/public/android:content_java",

@ -1,5 +1,6 @@
include_rules = [
"+components/embedder_support/android",
"+components/messages",
"+components/navigation_interception",
"+components/webapk/android/libs/client",
"+content/public/browser",

@ -19,6 +19,7 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.StrictMode;
import android.os.SystemClock;
@ -47,22 +48,31 @@ import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.task.PostTask;
import org.chromium.build.BuildConfig;
import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.embedder_support.util.UrlUtilities;
import org.chromium.components.embedder_support.util.UrlUtilitiesJni;
import org.chromium.components.external_intents.ExternalNavigationDelegate.IntentToAutofillAllowingAppResult;
import org.chromium.components.messages.MessageBannerProperties;
import org.chromium.components.messages.MessageDispatcher;
import org.chromium.components.messages.MessageDispatcherProvider;
import org.chromium.components.messages.MessageIdentifier;
import org.chromium.components.messages.MessageScopeType;
import org.chromium.components.messages.PrimaryActionClickBehavior;
import org.chromium.components.webapk.lib.client.ChromeWebApkHostSignature;
import org.chromium.components.webapk.lib.client.WebApkValidator;
import org.chromium.content_public.browser.LoadUrlParams;
import org.chromium.content_public.browser.NavigationController;
import org.chromium.content_public.browser.NavigationEntry;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.ContentUrlConstants;
import org.chromium.content_public.common.Referrer;
import org.chromium.network.mojom.ReferrerPolicy;
import org.chromium.ui.base.MimeTypeUtils;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modelutil.PropertyModel;
import org.chromium.ui.permissions.PermissionCallback;
import org.chromium.url.GURL;
@ -178,16 +188,52 @@ public class ExternalNavigationHandler {
// Used to ensure we only call queryIntentActivities when we really need to.
protected class QueryIntentActivitiesSupplier extends LazySupplier<List<ResolveInfo>> {
final Intent mIntent;
Intent mIntentCopy;
public QueryIntentActivitiesSupplier(Intent intent) {
super(() -> queryIntentActivities(intent));
mIntent = intent;
}
@Nullable
@Override
public List<ResolveInfo> get() {
// If the intent filter changes the previously supplied result will no longer be valid.
if (BuildConfig.ENABLE_ASSERTS) {
if (mIntentCopy != null) {
assert intentResolutionMatches(mIntent, mIntentCopy);
} else {
mIntentCopy = new Intent(mIntent);
}
}
return super.get();
}
}
protected static class ResolveActivitySupplier extends LazySupplier<ResolveInfo> {
final Intent mIntent;
Intent mIntentCopy;
public ResolveActivitySupplier(Intent intent) {
super(()
-> PackageManagerUtils.resolveActivity(
intent, PackageManager.MATCH_DEFAULT_ONLY));
mIntent = intent;
}
@Nullable
@Override
public ResolveInfo get() {
// If the intent filter changes the previously supplied result will no longer be valid.
if (BuildConfig.ENABLE_ASSERTS) {
if (mIntentCopy != null) {
assert intentResolutionMatches(mIntent, mIntentCopy);
} else {
mIntentCopy = new Intent(mIntent);
}
}
return super.get();
}
}
@ -247,11 +293,13 @@ public class ExternalNavigationHandler {
@OverrideUrlLoadingAsyncActionType
int mAsyncActionType;
OverrideUrlLoadingResult(@OverrideUrlLoadingResultType int resultType) {
boolean mCanAskUserToLaunchApp;
private OverrideUrlLoadingResult(@OverrideUrlLoadingResultType int resultType) {
this(resultType, OverrideUrlLoadingAsyncActionType.NO_ASYNC_ACTION);
}
OverrideUrlLoadingResult(@OverrideUrlLoadingResultType int resultType,
private OverrideUrlLoadingResult(@OverrideUrlLoadingResultType int resultType,
@OverrideUrlLoadingAsyncActionType int asyncActionType) {
// The async action type should be set only for async actions...
assert (asyncActionType == OverrideUrlLoadingAsyncActionType.NO_ASYNC_ACTION
@ -265,6 +313,13 @@ public class ExternalNavigationHandler {
mAsyncActionType = asyncActionType;
}
private OverrideUrlLoadingResult(
@OverrideUrlLoadingResultType int resultType, boolean canAskUser) {
this(resultType);
assert resultType == OverrideUrlLoadingResultType.NO_OVERRIDE;
mCanAskUserToLaunchApp = canAskUser;
}
public @OverrideUrlLoadingResultType int getResultType() {
return mResultType;
}
@ -273,18 +328,50 @@ public class ExternalNavigationHandler {
return mAsyncActionType;
}
public boolean canAskUserToLaunchApp() {
assert mResultType == OverrideUrlLoadingResultType.NO_OVERRIDE;
return mCanAskUserToLaunchApp;
}
/**
* Use this result when an asynchronous action needs to be carried out before deciding
* whether to block the external navigation.
*/
public static OverrideUrlLoadingResult forAsyncAction(
@OverrideUrlLoadingAsyncActionType int asyncActionType) {
return new OverrideUrlLoadingResult(
OverrideUrlLoadingResultType.OVERRIDE_WITH_ASYNC_ACTION, asyncActionType);
}
/**
* Use this result when we would like to block an external navigation without prompting the
* user asking them whether would like to launch an app, or when the navigation does not
* target an app.
*/
public static OverrideUrlLoadingResult forNoOverride() {
return new OverrideUrlLoadingResult(OverrideUrlLoadingResultType.NO_OVERRIDE);
}
/**
* Use this result when it might make sense to prompt the user with a message asking them
* if they would like to launch the targeted app when we block an external navigation.
*/
public static OverrideUrlLoadingResult canPromptForExternalIntent() {
return new OverrideUrlLoadingResult(OverrideUrlLoadingResultType.NO_OVERRIDE, true);
}
/**
* Use this result when the current external navigation should be blocked and a new
* navigation will be started in the Tab, clobbering the previous one.
*/
public static OverrideUrlLoadingResult forClobberingTab() {
return new OverrideUrlLoadingResult(
OverrideUrlLoadingResultType.OVERRIDE_WITH_CLOBBERING_TAB);
}
/**
* Use this result when an external app has been launched as a result of the navigation.
*/
public static OverrideUrlLoadingResult forExternalIntent() {
return new OverrideUrlLoadingResult(
OverrideUrlLoadingResultType.OVERRIDE_WITH_EXTERNAL_INTENT);
@ -332,8 +419,13 @@ public class ExternalNavigationHandler {
MutableBoolean canLaunchExternalFallbackResult = new MutableBoolean();
long time = SystemClock.elapsedRealtime();
OverrideUrlLoadingResult result = shouldOverrideUrlLoadingInternal(
params, targetIntent, browserFallbackUrl, canLaunchExternalFallbackResult);
QueryIntentActivitiesSupplier resolvingInfos =
new QueryIntentActivitiesSupplier(targetIntent);
ResolveActivitySupplier resolveActivity = new ResolveActivitySupplier(targetIntent);
OverrideUrlLoadingResult result =
shouldOverrideUrlLoadingInternal(params, targetIntent, browserFallbackUrl,
resolvingInfos, resolveActivity, canLaunchExternalFallbackResult);
assert canLaunchExternalFallbackResult.get() != null;
RecordHistogram.recordTimesHistogram(
"Android.StrictMode.OverrideUrlLoadingTime", SystemClock.elapsedRealtime() - time);
@ -351,6 +443,14 @@ public class ExternalNavigationHandler {
result = handleFallbackUrl(params, targetIntent, browserFallbackUrl,
canLaunchExternalFallbackResult.get());
}
if (result.getResultType() == OverrideUrlLoadingResultType.NO_OVERRIDE
&& result.mCanAskUserToLaunchApp
&& maybeAskToLaunchApp(params, targetIntent, resolvingInfos, resolveActivity)) {
result = OverrideUrlLoadingResult.forAsyncAction(
OverrideUrlLoadingAsyncActionType.UI_GATING_INTENT_LAUNCH);
}
if (DEBUG) printDebugShouldOverrideUrlLoadingResultType(result);
return result;
}
@ -362,7 +462,7 @@ public class ExternalNavigationHandler {
&& params.getRedirectHandler().isOnNavigation()
// For instance, if this is a chained fallback URL, we ignore it.
&& params.getRedirectHandler().shouldNotOverrideUrlLoading())) {
return OverrideUrlLoadingResult.forNoOverride();
return OverrideUrlLoadingResult.canPromptForExternalIntent();
}
if (mDelegate.isIntentToInstantApp(targetIntent)) {
@ -423,6 +523,71 @@ public class ExternalNavigationHandler {
return clobberCurrentTab(browserFallbackUrl, params.getReferrerUrl());
}
private boolean maybeAskToLaunchApp(ExternalNavigationParams params, Intent targetIntent,
QueryIntentActivitiesSupplier resolvingInfos, ResolveActivitySupplier resolveActivity) {
// Don't prompt for URLs the browser supports, just load them in browser.
if (UrlUtilities.isAcceptedScheme(params.getUrl())) return false;
ResolveInfo intentResolveInfo = resolveActivity.get();
// No app can resolve the intent, don't prompt.
if (intentResolveInfo == null || intentResolveInfo.activityInfo == null) return false;
// If the |resolvingInfos| from queryIntentActivities don't contain the result of
// resolveActivity, it means there's no default handler for the intent and it's resolving to
// the ResolverActivity. This means we can't know which app will be launched and can't
// convey that to the user. We also don't want to just allow the chooser dialog to be shown
// when the external navigation was otherwise blocked. In this case, we should just continue
// to block the navigation, and sites hoping to prompt the user when navigation fails should
// make sure to correctly target their app.
if (!resolversSubsetOf(Arrays.asList(intentResolveInfo), resolvingInfos.get())) {
return false;
}
MessageDispatcher messageDispatcher =
MessageDispatcherProvider.from(mDelegate.getWindowAndroid());
WebContents webContents = mDelegate.getWebContents();
if (messageDispatcher == null || webContents == null) return false;
String packageName = intentResolveInfo.activityInfo.packageName;
PackageManager pm = mDelegate.getContext().getPackageManager();
ApplicationInfo applicationInfo = null;
try {
applicationInfo = pm.getApplicationInfo(packageName, 0);
} catch (NameNotFoundException e) {
return false;
}
Drawable icon = pm.getApplicationLogo(applicationInfo);
if (icon == null) icon = pm.getApplicationIcon(applicationInfo);
CharSequence label = pm.getApplicationLabel(applicationInfo);
Resources res = mDelegate.getContext().getResources();
String title = res.getString(R.string.external_navigation_continue_to_title, label);
String description =
res.getString(R.string.external_navigation_continue_to_description, label);
String action = res.getString(R.string.external_navigation_continue_to_action);
PropertyModel message =
new PropertyModel.Builder(MessageBannerProperties.ALL_KEYS)
.with(MessageBannerProperties.MESSAGE_IDENTIFIER,
MessageIdentifier.EXTERNAL_NAVIGATION)
.with(MessageBannerProperties.TITLE, title)
.with(MessageBannerProperties.DESCRIPTION, description)
.with(MessageBannerProperties.ICON, icon)
.with(MessageBannerProperties.PRIMARY_BUTTON_TEXT, action)
.with(MessageBannerProperties.ICON_TINT_COLOR,
MessageBannerProperties.TINT_NONE)
.with(MessageBannerProperties.ON_PRIMARY_ACTION,
() -> {
startActivity(targetIntent, false);
return PrimaryActionClickBehavior.DISMISS_IMMEDIATELY;
})
.build();
messageDispatcher.enqueueMessage(message, webContents, MessageScopeType.NAVIGATION, false);
return true;
}
private void printDebugShouldOverrideUrlLoadingResultType(OverrideUrlLoadingResult result) {
String resultString;
switch (result.getResultType()) {
@ -1320,6 +1485,7 @@ public class ExternalNavigationHandler {
private OverrideUrlLoadingResult shouldOverrideUrlLoadingInternal(
ExternalNavigationParams params, Intent targetIntent, GURL browserFallbackUrl,
QueryIntentActivitiesSupplier resolvingInfos, ResolveActivitySupplier resolveActivity,
MutableBoolean canLaunchExternalFallbackResult) {
recordIntentSelectorMetrics(params.getUrl(), targetIntent);
sanitizeQueryIntentActivitiesIntent(targetIntent);
@ -1372,20 +1538,19 @@ public class ExternalNavigationHandler {
if (handleCCTRedirectsToInstantApps(params, isExternalProtocol, incomingIntentRedirect)) {
return OverrideUrlLoadingResult.forExternalIntent();
} else if (redirectShouldStayInApp(params, isExternalProtocol, targetIntent)) {
return OverrideUrlLoadingResult.forNoOverride();
return OverrideUrlLoadingResult.canPromptForExternalIntent();
}
if (!maybeSetSmsPackage(targetIntent)) maybeRecordPhoneIntentMetrics(targetIntent);
Intent debugIntent = new Intent(targetIntent);
QueryIntentActivitiesSupplier resolvingInfos =
new QueryIntentActivitiesSupplier(targetIntent);
if (!preferToShowIntentPicker(params, pageTransitionCore, isExternalProtocol, isFormSubmit,
incomingIntentRedirect, isFromIntent, resolvingInfos)) {
return OverrideUrlLoadingResult.forNoOverride();
return OverrideUrlLoadingResult.canPromptForExternalIntent();
}
if (isLinkFromChromeInternalPage(params)) return OverrideUrlLoadingResult.forNoOverride();
if (isLinkFromChromeInternalPage(params)) {
return OverrideUrlLoadingResult.forNoOverride();
}
if (handleWtaiMcProtocol(params)) {
return OverrideUrlLoadingResult.forExternalIntent();
@ -1399,7 +1564,9 @@ public class ExternalNavigationHandler {
return OverrideUrlLoadingResult.forNoOverride();
}
if (isYoutubePairingCode(params.getUrl())) return OverrideUrlLoadingResult.forNoOverride();
if (isYoutubePairingCode(params.getUrl())) {
return OverrideUrlLoadingResult.forNoOverride();
}
if (shouldStayInIncognito(params, isExternalProtocol)) {
return OverrideUrlLoadingResult.forNoOverride();
@ -1445,9 +1612,6 @@ public class ExternalNavigationHandler {
prepareExternalIntent(
targetIntent, params, resolvingInfos.get(), 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()) {
return handleIncognitoIntent(params, targetIntent, intentDataUrl, resolvingInfos.get(),
@ -1465,7 +1629,6 @@ public class ExternalNavigationHandler {
return OverrideUrlLoadingResult.forExternalIntent();
}
ResolveActivitySupplier resolveActivity = new ResolveActivitySupplier(targetIntent);
boolean requiresIntentChooser = false;
if (!mDelegate.maybeSetTargetPackage(targetIntent)) {
requiresIntentChooser = isViewIntentToOtherBrowser(
@ -1474,11 +1637,11 @@ public class ExternalNavigationHandler {
if (shouldAvoidShowingDisambiguationPrompt(
isExternalProtocol, targetIntent, resolvingInfos, resolveActivity)) {
return OverrideUrlLoadingResult.forNoOverride();
return OverrideUrlLoadingResult.canPromptForExternalIntent();
}
return startActivity(targetIntent, shouldProxyForInstantApps, requiresIntentChooser,
resolvingInfos.get(), resolveActivity, browserFallbackUrl, intentDataUrl,
resolvingInfos, resolveActivity, browserFallbackUrl, intentDataUrl,
params.getReferrerUrl());
}
@ -1825,7 +1988,7 @@ public class ExternalNavigationHandler {
* @returns The OverrideUrlLoadingResult for starting (or not starting) the Activity.
*/
protected OverrideUrlLoadingResult startActivity(Intent intent, boolean proxy,
boolean requiresIntentChooser, List<ResolveInfo> resolvingInfos,
boolean requiresIntentChooser, QueryIntentActivitiesSupplier resolvingInfos,
ResolveActivitySupplier resolveActivity, GURL browserFallbackUrl, GURL intentDataUrl,
GURL referrerUrl) {
// Only touches disk on Kitkat. See http://crbug.com/617725 for more context.
@ -1877,7 +2040,7 @@ public class ExternalNavigationHandler {
@SuppressWarnings("UseCompatLoadingForDrawables")
private OverrideUrlLoadingResult startActivityWithChooser(final Intent intent,
List<ResolveInfo> resolvingInfos, ResolveActivitySupplier resolveActivity,
QueryIntentActivitiesSupplier resolvingInfos, ResolveActivitySupplier resolveActivity,
GURL browserFallbackUrl, GURL intentDataUrl, GURL referrerUrl, Context context) {
ResolveInfo intentResolveInfo = resolveActivity.get();
// If this is null, then the intent was only previously matching
@ -1889,7 +2052,7 @@ public class ExternalNavigationHandler {
// will already get the option to choose the target app (as there will be multiple options)
// and we don't need to do anything. Otherwise we have to make a fake option in the chooser
// dialog that loads the URL in the embedding app.
if (!resolversSubsetOf(Arrays.asList(intentResolveInfo), resolvingInfos)) {
if (!resolversSubsetOf(Arrays.asList(intentResolveInfo), resolvingInfos.get())) {
return doStartActivity(intent, context);
}

@ -2656,7 +2656,7 @@ public class ExternalNavigationHandlerTest {
@Override
protected OverrideUrlLoadingResult startActivity(Intent intent, boolean proxy,
boolean requiresIntentChooser, List<ResolveInfo> resolvingInfos,
boolean requiresIntentChooser, QueryIntentActivitiesSupplier resolvingInfos,
ResolveActivitySupplier resolveActivity, GURL browserFallbackUrl,
GURL intentDataUrl, GURL referrerUrl) {
mStartActivityIntent = intent;

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<grit-part>
<message name="IDS_EXTERNAL_NAVIGATION_CONTINUE_TO_TITLE" desc="Title for the Continue To App Message." formatter_data="android_java">
Continue to <ph name="APP_NAME">%1s<ex>Youtube</ex></ph>?
</message>
<message name="IDS_EXTERNAL_NAVIGATION_CONTINUE_TO_DESCRIPTION" desc="Description for the Continue To App Message." formatter_data="android_java">
This site wants to open the <ph name="APP_NAME">%1s<ex>Youtube</ex></ph> app
</message>
<message name="IDS_EXTERNAL_NAVIGATION_CONTINUE_TO_ACTION" desc="Action for the Continue To App Message." formatter_data="android_java">
Continue
</message>
</grit-part>

@ -0,0 +1 @@
157f661704da8b36b570c9e9262054cbd7ed564a

@ -0,0 +1 @@
157f661704da8b36b570c9e9262054cbd7ed564a

@ -0,0 +1 @@
157f661704da8b36b570c9e9262054cbd7ed564a

@ -141,6 +141,8 @@ public class MessagesMetrics {
return "Translate";
case MessageIdentifier.OFFER_NOTIFICATION:
return "OfferNotification";
case MessageIdentifier.EXTERNAL_NAVIGATION:
return "ExternalNavigation";
default:
return "Unknown";
}

@ -103,6 +103,7 @@ enum class MessageIdentifier {
ABOUT_THIS_SITE = 28,
TRANSLATE = 29,
OFFER_NOTIFICATION = 30,
EXTERNAL_NAVIGATION = 31,
// Insert new values before this line.
COUNT

@ -64034,6 +64034,7 @@ Called by update_use_counter_css.py.-->
<int value="28" label="AboutThisSite"/>
<int value="29" label="Translate"/>
<int value="30" label="OfferNotification"/>
<int value="31" label="ExternalNavigation"/>
</enum>
<enum name="MessageLoopProblems">

@ -82,6 +82,7 @@ chromium-metrics-reviews@google.com.
<variant name=".AutoDarkWebContents"/>
<variant name=".ChromeSurvey"/>
<variant name=".DownloadProgress"/>
<variant name=".ExternalNavigation"/>
<variant name=".GeneratedPasswordSaved"/>
<variant name=".InstallableAmbientBadge"/>
<variant name=".InstantApps"/>