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:

committed by
Chromium LUCI CQ

parent
f3753a2522
commit
eabf09f81e
chrome
android
javatests
src
org
chromium
chrome
browser
externalnav
test
data
android
components
components_strings.grdexternal_intents_strings.grdp
external_intents
android
external_intents_strings_grdp
IDS_EXTERNAL_NAVIGATION_CONTINUE_TO_ACTION.png.sha1IDS_EXTERNAL_NAVIGATION_CONTINUE_TO_DESCRIPTION.png.sha1IDS_EXTERNAL_NAVIGATION_CONTINUE_TO_TITLE.png.sha1
messages
android
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;
|
||||
|
12
components/external_intents_strings.grdp
Normal file
12
components/external_intents_strings.grdp
Normal file
@ -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>
|
1
components/external_intents_strings_grdp/IDS_EXTERNAL_NAVIGATION_CONTINUE_TO_ACTION.png.sha1
Normal file
1
components/external_intents_strings_grdp/IDS_EXTERNAL_NAVIGATION_CONTINUE_TO_ACTION.png.sha1
Normal file
@ -0,0 +1 @@
|
||||
157f661704da8b36b570c9e9262054cbd7ed564a
|
1
components/external_intents_strings_grdp/IDS_EXTERNAL_NAVIGATION_CONTINUE_TO_DESCRIPTION.png.sha1
Normal file
1
components/external_intents_strings_grdp/IDS_EXTERNAL_NAVIGATION_CONTINUE_TO_DESCRIPTION.png.sha1
Normal file
@ -0,0 +1 @@
|
||||
157f661704da8b36b570c9e9262054cbd7ed564a
|
1
components/external_intents_strings_grdp/IDS_EXTERNAL_NAVIGATION_CONTINUE_TO_TITLE.png.sha1
Normal file
1
components/external_intents_strings_grdp/IDS_EXTERNAL_NAVIGATION_CONTINUE_TO_TITLE.png.sha1
Normal file
@ -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"/>
|
||||
|
Reference in New Issue
Block a user