Commit a6b8cc0f authored by Colin Blundell's avatar Colin Blundell Committed by Commit Bot

WebLayer: Initial implementation of intent handling

This CL provides an initial implementation of intent handling in
WebLayer. Specifically, it matches Android WebView's implementation. To
do so, it brings in WebView's default behavior as implemented by
aw_content_browser_client.cc::ShouldOverrideUrlLoading() and
AwContentsClient.java:sendBrowsingIntent(), augmenting that default
behavior with the Android WebView shell's internal handling of
browser-specific URIs as implemented in
WebViewBrowserActivity.java:startBrowsingIntent().

One exception to the above is that unlike WebView, this implementation
starts the activity with the FLAG_ACTIVITY_NEW_TASK flag set. It is
necessary to set this flag because the application context, which is
used here to start the activity for the intent, isn't an Activity.
Setting this flag also matches Chrome's behavior for launching external
intents.

We explored an alternative implementation wherein we would plumb the
Activity that is available in BrowserFragmentImpl.java all the way to
TabImpl.java via BrowserViewController.java, and then have
TabImpl.java implement the handling of external intents. However, we
preferred going with this implementation as it's simpler.

To test, install the Alipay app, go to alipay.com in WebLayer Shell,
tap "Open", and verify that the Alipay app is opened.

I tried to add instrumentation tests but ran into blockers described
at crbug.com/1029710.

Change-Id: Ibc5ea1ce22875e85bc8f915856d754f52ed760e2
Bug: 1028745
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1942337
Commit-Queue: Colin Blundell <blundell@chromium.org>
Reviewed-by: default avatarTobias Sargeant <tobiasjs@chromium.org>
Cr-Commit-Position: refs/heads/master@{#720468}
parent 02f36d7c
...@@ -46,11 +46,14 @@ ...@@ -46,11 +46,14 @@
#if defined(OS_ANDROID) #if defined(OS_ANDROID)
#include "base/android/bundle_utils.h" #include "base/android/bundle_utils.h"
#include "base/android/jni_android.h"
#include "base/android/jni_string.h"
#include "base/android/path_utils.h" #include "base/android/path_utils.h"
#include "components/crash/content/browser/crash_handler_host_linux.h" #include "components/crash/content/browser/crash_handler_host_linux.h"
#include "ui/base/resource/resource_bundle_android.h" #include "ui/base/resource/resource_bundle_android.h"
#include "weblayer/browser/android_descriptors.h" #include "weblayer/browser/android_descriptors.h"
#include "weblayer/browser/devtools_manager_delegate_android.h" #include "weblayer/browser/devtools_manager_delegate_android.h"
#include "weblayer/browser/java/jni/ExternalNavigationHandler_jni.h"
#include "weblayer/browser/safe_browsing/safe_browsing_service.h" #include "weblayer/browser/safe_browsing/safe_browsing_service.h"
#endif #endif
...@@ -358,4 +361,64 @@ void ContentBrowserClientImpl::GetAdditionalMappedFilesForChildProcess( ...@@ -358,4 +361,64 @@ void ContentBrowserClientImpl::GetAdditionalMappedFilesForChildProcess(
} }
#endif // defined(OS_LINUX) || defined(OS_ANDROID) #endif // defined(OS_LINUX) || defined(OS_ANDROID)
#if defined(OS_ANDROID)
bool ContentBrowserClientImpl::ShouldOverrideUrlLoading(
int frame_tree_node_id,
bool browser_initiated,
const GURL& gurl,
const std::string& request_method,
bool has_user_gesture,
bool is_redirect,
bool is_main_frame,
ui::PageTransition transition,
bool* ignore_navigation) {
*ignore_navigation = false;
// Only GETs can be overridden.
if (request_method != "GET")
return true;
bool application_initiated =
browser_initiated || transition & ui::PAGE_TRANSITION_FORWARD_BACK;
// Don't offer application-initiated navigations unless it's a redirect.
if (application_initiated && !is_redirect)
return true;
// For HTTP schemes, only top-level navigations can be overridden. Similarly,
// WebView Classic lets app override only top level about:blank navigations.
// So we filter out non-top about:blank navigations here.
if (!is_main_frame &&
(gurl.SchemeIs(url::kHttpScheme) || gurl.SchemeIs(url::kHttpsScheme) ||
gurl.SchemeIs(url::kAboutScheme)))
return true;
content::WebContents* web_contents =
content::WebContents::FromFrameTreeNodeId(frame_tree_node_id);
if (web_contents == nullptr)
return true;
JNIEnv* env = base::android::AttachCurrentThread();
base::string16 url = base::UTF8ToUTF16(gurl.possibly_invalid_spec());
base::android::ScopedJavaLocalRef<jstring> jurl =
base::android::ConvertUTF16ToJavaString(env, url);
*ignore_navigation = Java_ExternalNavigationHandler_shouldOverrideUrlLoading(
env, jurl, has_user_gesture, is_redirect, is_main_frame);
if (base::android::HasException(env)) {
// Tell the chromium message loop to not perform any tasks after the
// current one - we want to make sure we return to Java cleanly without
// first making any new JNI calls.
base::MessageLoopCurrentForUI::Get()->Abort();
// If we crashed we don't want to continue the navigation.
*ignore_navigation = true;
return false;
}
return true;
}
#endif
} // namespace weblayer } // namespace weblayer
...@@ -78,6 +78,18 @@ class ContentBrowserClientImpl : public content::ContentBrowserClient { ...@@ -78,6 +78,18 @@ class ContentBrowserClientImpl : public content::ContentBrowserClient {
content::PosixFileDescriptorInfo* mappings) override; content::PosixFileDescriptorInfo* mappings) override;
#endif // defined(OS_LINUX) || defined(OS_ANDROID) #endif // defined(OS_LINUX) || defined(OS_ANDROID)
#if defined(OS_ANDROID)
bool ShouldOverrideUrlLoading(int frame_tree_node_id,
bool browser_initiated,
const GURL& gurl,
const std::string& request_method,
bool has_user_gesture,
bool is_redirect,
bool is_main_frame,
ui::PageTransition transition,
bool* ignore_navigation) override;
#endif
private: private:
MainParams* params_; MainParams* params_;
......
...@@ -33,6 +33,7 @@ android_library("java") { ...@@ -33,6 +33,7 @@ android_library("java") {
"org/chromium/weblayer_private/ContentViewRenderView.java", "org/chromium/weblayer_private/ContentViewRenderView.java",
"org/chromium/weblayer_private/DownloadCallbackProxy.java", "org/chromium/weblayer_private/DownloadCallbackProxy.java",
"org/chromium/weblayer_private/ErrorPageCallbackProxy.java", "org/chromium/weblayer_private/ErrorPageCallbackProxy.java",
"org/chromium/weblayer_private/ExternalNavigationHandler.java",
"org/chromium/weblayer_private/ActionModeCallback.java", "org/chromium/weblayer_private/ActionModeCallback.java",
"org/chromium/weblayer_private/FullscreenCallbackProxy.java", "org/chromium/weblayer_private/FullscreenCallbackProxy.java",
"org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java", "org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java",
...@@ -84,6 +85,7 @@ generate_jni("jni") { ...@@ -84,6 +85,7 @@ generate_jni("jni") {
"org/chromium/weblayer_private/ContentViewRenderView.java", "org/chromium/weblayer_private/ContentViewRenderView.java",
"org/chromium/weblayer_private/DownloadCallbackProxy.java", "org/chromium/weblayer_private/DownloadCallbackProxy.java",
"org/chromium/weblayer_private/ErrorPageCallbackProxy.java", "org/chromium/weblayer_private/ErrorPageCallbackProxy.java",
"org/chromium/weblayer_private/ExternalNavigationHandler.java",
"org/chromium/weblayer_private/FullscreenCallbackProxy.java", "org/chromium/weblayer_private/FullscreenCallbackProxy.java",
"org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java", "org/chromium/weblayer_private/LocaleChangedBroadcastReceiver.java",
"org/chromium/weblayer_private/NavigationControllerImpl.java", "org/chromium/weblayer_private/NavigationControllerImpl.java",
......
// Copyright 2019 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.weblayer_private;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.provider.Browser;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A class that handles navigations that should be transformed to intents. Logic taken primarly from
* //android_webview's AwContentsClient.java:sendBrowsingIntent(), with some additional logic
* from //android_webview's WebViewBrowserActivity.java:startBrowsingIntent().
*/
@JNINamespace("weblayer")
public class ExternalNavigationHandler {
private static final String TAG = "ExternalNavHandler";
static final Pattern BROWSER_URI_SCHEMA =
Pattern.compile("(?i)" // switch on case insensitive matching
+ "(" // begin group for schema
+ "(?:http|https|file)://"
+ "|(?:inline|data|about|chrome|javascript):"
+ ")"
+ ".*");
@CalledByNative
private static boolean shouldOverrideUrlLoading(
String url, boolean hasUserGesture, boolean isRedirect, boolean isMainFrame) {
// Check for regular URIs that WebLayer supports by itself.
// TODO(blundell): Port over WebViewBrowserActivity's
// isSpecializedHandlerAvailable() check that checks whether there's an app for handling
// the scheme?
Matcher m = BROWSER_URI_SCHEMA.matcher(url);
if (m.matches()) {
return false;
}
if (!hasUserGesture && !isRedirect) {
Log.w(TAG, "Denied starting an intent without a user gesture, URI %s", url);
return true;
}
Intent intent;
// Perform generic parsing of the URI to turn it into an Intent.
try {
intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
} catch (Exception ex) {
Log.w(TAG, "Bad URI %s", url, ex);
return false;
}
// Sanitize the Intent, ensuring web pages can not bypass browser
// security (only access to BROWSABLE activities).
intent.addCategory(Intent.CATEGORY_BROWSABLE);
// Ensure that startActivity() succeeds even if the application context
// isn't an Activity. This also matches Chrome's behavior (see
// //chrome's ExternalNavigationHandler.java:PrepareExternalIntent()).
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setComponent(null);
Intent selector = intent.getSelector();
if (selector != null) {
selector.addCategory(Intent.CATEGORY_BROWSABLE);
selector.setComponent(null);
}
Context context = ContextUtils.getApplicationContext();
// Pass the package name as application ID so that the intent from the
// same application can be opened in the same tab.
intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
try {
context.startActivity(intent);
return true;
} catch (ActivityNotFoundException ex) {
Log.w(TAG, "No application can handle %s", url);
} catch (SecurityException ex) {
// This can happen if the Activity is exported="true", guarded by a permission, and sets
// up an intent filter matching this intent. This is a valid configuration for an
// Activity, so instead of crashing, we catch the exception and do nothing. See
// https://crbug.com/808494 and https://crbug.com/889300.
Log.w(TAG, "SecurityException when starting intent for %s", url);
}
return false;
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment