diff options
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.java | 527 |
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); + } +} |