summaryrefslogtreecommitdiffstats
path: root/android_webview/tools/system_webview_shell/apk/src/org/chromium/webview_shell/WebViewBrowserActivity.java
diff options
context:
space:
mode:
Diffstat (limited to 'android_webview/tools/system_webview_shell/apk/src/org/chromium/webview_shell/WebViewBrowserActivity.java')
-rw-r--r--android_webview/tools/system_webview_shell/apk/src/org/chromium/webview_shell/WebViewBrowserActivity.java527
1 files changed, 527 insertions, 0 deletions
diff --git a/android_webview/tools/system_webview_shell/apk/src/org/chromium/webview_shell/WebViewBrowserActivity.java b/android_webview/tools/system_webview_shell/apk/src/org/chromium/webview_shell/WebViewBrowserActivity.java
new file mode 100644
index 0000000..2aee9e3
--- /dev/null
+++ b/android_webview/tools/system_webview_shell/apk/src/org/chromium/webview_shell/WebViewBrowserActivity.java
@@ -0,0 +1,527 @@
+// 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.webview_shell;
+
+import android.Manifest;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.util.SparseArray;
+
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnKeyListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.inputmethod.InputMethodManager;
+
+import android.webkit.GeolocationPermissions;
+import android.webkit.PermissionRequest;
+import android.webkit.WebChromeClient;
+import android.webkit.WebSettings;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+import android.widget.EditText;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+
+import org.chromium.base.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This activity is designed for starting a "mini-browser" for manual testing of WebView.
+ * It takes an optional URL as an argument, and displays the page. There is a URL bar
+ * on top of the webview for manually specifying URLs to load.
+ */
+public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenuItemClickListener {
+ private static final String TAG = "WebViewShell";
+
+ // Our imaginary Android permission to associate with the WebKit geo permission
+ private static final String RESOURCE_GEO = "RESOURCE_GEO";
+ // Our imaginary WebKit permission to request when loading a file:// URL
+ private static final String RESOURCE_FILE_URL = "RESOURCE_FILE_URL";
+ // WebKit permissions with no corresponding Android permission can always be granted
+ private static final String NO_ANDROID_PERMISSION = "NO_ANDROID_PERMISSION";
+
+ // Map from WebKit permissions to Android permissions
+ private static final HashMap<String, String> sPermissions;
+ static {
+ sPermissions = new HashMap<String, String>();
+ sPermissions.put(RESOURCE_GEO, Manifest.permission.ACCESS_FINE_LOCATION);
+ sPermissions.put(RESOURCE_FILE_URL, Manifest.permission.READ_EXTERNAL_STORAGE);
+ sPermissions.put(PermissionRequest.RESOURCE_AUDIO_CAPTURE,
+ Manifest.permission.RECORD_AUDIO);
+ sPermissions.put(PermissionRequest.RESOURCE_MIDI_SYSEX, NO_ANDROID_PERMISSION);
+ sPermissions.put(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID, NO_ANDROID_PERMISSION);
+ sPermissions.put(PermissionRequest.RESOURCE_VIDEO_CAPTURE,
+ Manifest.permission.CAMERA);
+ }
+
+ private static final Pattern WEBVIEW_VERSION_PATTERN =
+ Pattern.compile("(Chrome/)([\\d\\.]+)\\s");
+
+ private EditText mUrlBar;
+ private WebView mWebView;
+ private String mWebViewVersion;
+
+ // Each time we make a request, store it here with an int key. onRequestPermissionsResult will
+ // look up the request in order to grant the approprate permissions.
+ private SparseArray<PermissionRequest> mPendingRequests = new SparseArray<PermissionRequest>();
+ private int mNextRequestKey = 0;
+
+ // Work around our wonky API by wrapping a geo permission prompt inside a regular
+ // PermissionRequest.
+ private static class GeoPermissionRequest extends PermissionRequest {
+ private String mOrigin;
+ private GeolocationPermissions.Callback mCallback;
+
+ public GeoPermissionRequest(String origin, GeolocationPermissions.Callback callback) {
+ mOrigin = origin;
+ mCallback = callback;
+ }
+
+ public Uri getOrigin() {
+ return Uri.parse(mOrigin);
+ }
+
+ public String[] getResources() {
+ return new String[] { WebViewBrowserActivity.RESOURCE_GEO };
+ }
+
+ public void grant(String[] resources) {
+ assert resources.length == 1;
+ assert WebViewBrowserActivity.RESOURCE_GEO.equals(resources[0]);
+ mCallback.invoke(mOrigin, true, false);
+ }
+
+ public void deny() {
+ mCallback.invoke(mOrigin, false, false);
+ }
+ }
+
+ // For simplicity, also treat the read access needed for file:// URLs as a regular
+ // PermissionRequest.
+ private class FilePermissionRequest extends PermissionRequest {
+ private String mOrigin;
+
+ public FilePermissionRequest(String origin) {
+ mOrigin = origin;
+ }
+
+ public Uri getOrigin() {
+ return Uri.parse(mOrigin);
+ }
+
+ public String[] getResources() {
+ return new String[] { WebViewBrowserActivity.RESOURCE_FILE_URL };
+ }
+
+ public void grant(String[] resources) {
+ assert resources.length == 1;
+ assert WebViewBrowserActivity.RESOURCE_FILE_URL.equals(resources[0]);
+ // Try again now that we have read access.
+ WebViewBrowserActivity.this.mWebView.loadUrl(mOrigin);
+ }
+
+ public void deny() {
+ // womp womp
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ WebView.setWebContentsDebuggingEnabled(true);
+ }
+ setContentView(R.layout.activity_webview_browser);
+ mUrlBar = (EditText) findViewById(R.id.url_field);
+ mUrlBar.setOnKeyListener(new OnKeyListener() {
+ public boolean onKey(View view, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
+ loadUrlFromUrlBar(view);
+ return true;
+ }
+ return false;
+ }
+ });
+
+ createAndInitializeWebView();
+
+ String url = getUrlFromIntent(getIntent());
+ if (url != null) {
+ setUrlBarText(url);
+ setUrlFail(false);
+ loadUrlFromUrlBar(mUrlBar);
+ }
+ }
+
+ ViewGroup getContainer() {
+ return (ViewGroup) findViewById(R.id.container);
+ }
+
+ private void createAndInitializeWebView() {
+ WebView webview = new WebView(this);
+ WebSettings settings = webview.getSettings();
+ initializeSettings(settings);
+
+ Matcher matcher = WEBVIEW_VERSION_PATTERN.matcher(settings.getUserAgentString());
+ if (matcher.find()) {
+ mWebViewVersion = matcher.group(2);
+ } else {
+ mWebViewVersion = "-";
+ }
+ setTitle(getResources().getString(R.string.title_activity_browser) + " " + mWebViewVersion);
+
+ webview.setWebViewClient(new WebViewClient() {
+ @Override
+ public void onPageStarted(WebView view, String url, Bitmap favicon) {
+ setUrlBarText(url);
+ }
+
+ @Override
+ public void onPageFinished(WebView view, String url) {
+ setUrlBarText(url);
+ }
+
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView webView, String url) {
+ // "about:" and "chrome:" schemes are internal to Chromium;
+ // don't want these to be dispatched to other apps.
+ if (url.startsWith("about:") || url.startsWith("chrome:")) {
+ return false;
+ }
+ return startBrowsingIntent(WebViewBrowserActivity.this, url);
+ }
+
+ @Override
+ public void onReceivedError(WebView view, int errorCode, String description,
+ String failingUrl) {
+ setUrlFail(true);
+ }
+ });
+
+ webview.setWebChromeClient(new WebChromeClient() {
+ @Override
+ public Bitmap getDefaultVideoPoster() {
+ return Bitmap.createBitmap(
+ new int[] {Color.TRANSPARENT}, 1, 1, Bitmap.Config.ARGB_8888);
+ }
+
+ @Override
+ public void onGeolocationPermissionsShowPrompt(String origin,
+ GeolocationPermissions.Callback callback) {
+ onPermissionRequest(new GeoPermissionRequest(origin, callback));
+ }
+
+ @Override
+ public void onPermissionRequest(PermissionRequest request) {
+ WebViewBrowserActivity.this.requestPermissionsForPage(request);
+ }
+ });
+
+ mWebView = webview;
+ getContainer().addView(
+ webview, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
+ setUrlBarText("");
+ }
+
+ // WebKit permissions which can be granted because either they have no associated Android
+ // permission or the associated Android permission has been granted
+ private boolean canGrant(String webkitPermission) {
+ String androidPermission = sPermissions.get(webkitPermission);
+ if (androidPermission == NO_ANDROID_PERMISSION) {
+ return true;
+ }
+ return PackageManager.PERMISSION_GRANTED == checkSelfPermission(androidPermission);
+ }
+
+ private void requestPermissionsForPage(PermissionRequest request) {
+ // Deny any unrecognized permissions.
+ for (String webkitPermission : request.getResources()) {
+ if (!sPermissions.containsKey(webkitPermission)) {
+ Log.w(TAG, "Unrecognized WebKit permission: " + webkitPermission);
+ request.deny();
+ return;
+ }
+ }
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
+ request.grant(request.getResources());
+ return;
+ }
+
+ // Find what Android permissions we need before we can grant these WebKit permissions.
+ ArrayList<String> androidPermissionsNeeded = new ArrayList<String>();
+ for (String webkitPermission : request.getResources()) {
+ if (!canGrant(webkitPermission)) {
+ // We already checked for unrecognized permissions, and canGrant will skip over
+ // NO_ANDROID_PERMISSION cases, so this is guaranteed to be a regular Android
+ // permission.
+ String androidPermission = sPermissions.get(webkitPermission);
+ androidPermissionsNeeded.add(androidPermission);
+ }
+ }
+
+ // If there are no such Android permissions, grant the WebKit permissions immediately.
+ if (androidPermissionsNeeded.isEmpty()) {
+ request.grant(request.getResources());
+ return;
+ }
+
+ // Otherwise, file a new request
+ if (mNextRequestKey == Integer.MAX_VALUE) {
+ Log.e(TAG, "Too many permission requests");
+ return;
+ }
+ int requestCode = mNextRequestKey;
+ mNextRequestKey++;
+ mPendingRequests.append(requestCode, request);
+ requestPermissions(androidPermissionsNeeded.toArray(new String[0]), requestCode);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ String permissions[], int[] grantResults) {
+ // Verify that we can now grant all the requested permissions. Note that although grant()
+ // takes a list of permissions, grant() is actually all-or-nothing. If there are any
+ // requested permissions not included in the granted permissions, all will be denied.
+ PermissionRequest request = mPendingRequests.get(requestCode);
+ for (String webkitPermission : request.getResources()) {
+ if (!canGrant(webkitPermission)) {
+ request.deny();
+ return;
+ }
+ }
+ request.grant(request.getResources());
+ mPendingRequests.delete(requestCode);
+ }
+
+ public void loadUrlFromUrlBar(View view) {
+ String url = mUrlBar.getText().toString();
+ try {
+ URI uri = new URI(url);
+ url = (uri.getScheme() == null) ? "http://" + uri.toString() : uri.toString();
+ } catch (URISyntaxException e) {
+ String message = "<html><body>URISyntaxException: " + e.getMessage() + "</body></html>";
+ mWebView.loadData(message, "text/html", "UTF-8");
+ setUrlFail(true);
+ return;
+ }
+
+ setUrlBarText(url);
+ setUrlFail(false);
+ loadUrl(url);
+ hideKeyboard(mUrlBar);
+ }
+
+ public void showPopup(View v) {
+ PopupMenu popup = new PopupMenu(this, v);
+ popup.setOnMenuItemClickListener(this);
+ popup.inflate(R.menu.main_menu);
+ popup.show();
+ }
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ switch(item.getItemId()) {
+ case R.id.menu_reset_webview:
+ if (mWebView != null) {
+ ViewGroup container = getContainer();
+ container.removeView(mWebView);
+ mWebView.destroy();
+ mWebView = null;
+ }
+ createAndInitializeWebView();
+ return true;
+ case R.id.menu_clear_cache:
+ if (mWebView != null) {
+ mWebView.clearCache(true);
+ }
+ return true;
+ case R.id.menu_about:
+ about();
+ hideKeyboard(mUrlBar);
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ private void initializeSettings(WebSettings settings) {
+ settings.setJavaScriptEnabled(true);
+
+ // configure local storage apis and their database paths.
+ settings.setAppCachePath(getDir("appcache", 0).getPath());
+ settings.setGeolocationDatabasePath(getDir("geolocation", 0).getPath());
+ settings.setDatabasePath(getDir("databases", 0).getPath());
+
+ settings.setAppCacheEnabled(true);
+ settings.setGeolocationEnabled(true);
+ settings.setDatabaseEnabled(true);
+ settings.setDomStorageEnabled(true);
+ }
+
+ private void about() {
+ WebSettings settings = mWebView.getSettings();
+ StringBuilder summary = new StringBuilder();
+ summary.append("WebView version : " + mWebViewVersion + "\n");
+
+ for (Method method : settings.getClass().getMethods()) {
+ if (!methodIsSimpleInspector(method)) continue;
+ try {
+ summary.append(method.getName() + " : " + method.invoke(settings) + "\n");
+ } catch (IllegalAccessException e) {
+ } catch (InvocationTargetException e) { }
+ }
+
+ AlertDialog dialog = new AlertDialog.Builder(this)
+ .setTitle(getResources().getString(R.string.menu_about))
+ .setMessage(summary)
+ .setPositiveButton("OK", null)
+ .create();
+ dialog.show();
+ dialog.getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
+ }
+
+ // Returns true is a method has no arguments and returns either a boolean or a String.
+ private boolean methodIsSimpleInspector(Method method) {
+ Class<?> returnType = method.getReturnType();
+ return ((returnType.equals(boolean.class) || returnType.equals(String.class))
+ && method.getParameterTypes().length == 0);
+ }
+
+ private void loadUrl(String url) {
+ // Request read access if necessary
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+ && "file".equals(Uri.parse(url).getScheme())
+ && PackageManager.PERMISSION_DENIED
+ == checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) {
+ requestPermissionsForPage(new FilePermissionRequest(url));
+ }
+
+ // If it is file:// and we don't have permission, they'll get the "Webpage not available"
+ // "net::ERR_ACCESS_DENIED" page. When we get permission, FilePermissionRequest.grant()
+ // will reload.
+ mWebView.loadUrl(url);
+ mWebView.requestFocus();
+ }
+
+ private void setUrlBarText(String url) {
+ mUrlBar.setText(url, TextView.BufferType.EDITABLE);
+ }
+
+ private void setUrlFail(boolean fail) {
+ mUrlBar.setTextColor(fail ? Color.RED : Color.BLACK);
+ }
+
+ /**
+ * Hides the keyboard.
+ * @param view The {@link View} that is currently accepting input.
+ * @return Whether the keyboard was visible before.
+ */
+ private static boolean hideKeyboard(View view) {
+ InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService(
+ Context.INPUT_METHOD_SERVICE);
+ return imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+
+ private static String getUrlFromIntent(Intent intent) {
+ return intent != null ? intent.getDataString() : null;
+ }
+
+ 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):"
+ + ")"
+ + "(.*)");
+
+ private static boolean startBrowsingIntent(Context context, String url) {
+ 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;
+ }
+ // Check for regular URIs that WebView supports by itself, but also
+ // check if there is a specialized app that had registered itself
+ // for this kind of an intent.
+ Matcher m = BROWSER_URI_SCHEMA.matcher(url);
+ if (m.matches() && !isSpecializedHandlerAvailable(context, intent)) {
+ return false;
+ }
+ // Sanitize the Intent, ensuring web pages can not bypass browser
+ // security (only access to BROWSABLE activities).
+ intent.addCategory(Intent.CATEGORY_BROWSABLE);
+ intent.setComponent(null);
+ Intent selector = intent.getSelector();
+ if (selector != null) {
+ selector.addCategory(Intent.CATEGORY_BROWSABLE);
+ selector.setComponent(null);
+ }
+
+ // 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);
+ }
+ return false;
+ }
+
+ /**
+ * Search for intent handlers that are specific to the scheme of the URL in the intent.
+ */
+ private static boolean isSpecializedHandlerAvailable(Context context, Intent intent) {
+ PackageManager pm = context.getPackageManager();
+ List<ResolveInfo> handlers = pm.queryIntentActivities(intent,
+ PackageManager.GET_RESOLVED_FILTER);
+ if (handlers == null || handlers.size() == 0) {
+ return false;
+ }
+ for (ResolveInfo resolveInfo : handlers) {
+ if (!isNullOrGenericHandler(resolveInfo.filter)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isNullOrGenericHandler(IntentFilter filter) {
+ return filter == null
+ || (filter.countDataAuthorities() == 0 && filter.countDataPaths() == 0);
+ }
+}