diff options
Diffstat (limited to 'src/org/chromium/webview_shell/WebViewBrowserActivity.java')
-rw-r--r-- | src/org/chromium/webview_shell/WebViewBrowserActivity.java | 295 |
1 files changed, 254 insertions, 41 deletions
diff --git a/src/org/chromium/webview_shell/WebViewBrowserActivity.java b/src/org/chromium/webview_shell/WebViewBrowserActivity.java index 1da563e..0a7b637 100644 --- a/src/org/chromium/webview_shell/WebViewBrowserActivity.java +++ b/src/org/chromium/webview_shell/WebViewBrowserActivity.java @@ -5,6 +5,8 @@ package org.chromium.webview_shell; import android.Manifest; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.app.Activity; import android.app.AlertDialog; import android.content.ActivityNotFoundException; @@ -18,39 +20,47 @@ import android.graphics.Color; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.os.StrictMode; +import android.print.PrintAttributes; +import android.print.PrintDocumentAdapter; +import android.print.PrintManager; import android.provider.Browser; import android.util.Log; import android.util.SparseArray; - +import android.view.Gravity; 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.WindowManager; import android.view.inputmethod.InputMethodManager; - import android.webkit.GeolocationPermissions; import android.webkit.PermissionRequest; +import android.webkit.TracingConfig; +import android.webkit.TracingController; import android.webkit.WebChromeClient; -import android.webkit.WebResourceRequest; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; - import android.widget.EditText; +import android.widget.FrameLayout; import android.widget.PopupMenu; import android.widget.TextView; +import android.widget.Toast; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; 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.Locale; +import java.util.concurrent.Executors; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -69,6 +79,12 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu // WebKit permissions with no corresponding Android permission can always be granted private static final String NO_ANDROID_PERMISSION = "NO_ANDROID_PERMISSION"; + // TODO(timav): Remove these variables after http://crbug.com/626202 is fixed. + // The Bundle key for WebView serialized state + private static final String SAVE_RESTORE_STATE_KEY = "WEBVIEW_CHROMIUM_STATE"; + // Maximal size of this state. + private static final int MAX_STATE_LENGTH = 300 * 1024; + // Map from WebKit permissions to Android permissions private static final HashMap<String, String> sPermissions; static { @@ -88,15 +104,18 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu private EditText mUrlBar; private WebView mWebView; + private View mFullscreenView; private String mWebViewVersion; + private boolean mEnableTracing; // 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; + private int mNextRequestKey; // Work around our wonky API by wrapping a geo permission prompt inside a regular // PermissionRequest. + @SuppressLint("NewApi") // GeoPermissionRequest class requires API level 21. private static class GeoPermissionRequest extends PermissionRequest { private String mOrigin; private GeolocationPermissions.Callback mCallback; @@ -106,20 +125,24 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu mCallback = callback; } + @Override public Uri getOrigin() { return Uri.parse(mOrigin); } + @Override public String[] getResources() { return new String[] { WebViewBrowserActivity.RESOURCE_GEO }; } + @Override public void grant(String[] resources) { assert resources.length == 1; assert WebViewBrowserActivity.RESOURCE_GEO.equals(resources[0]); mCallback.invoke(mOrigin, true, false); } + @Override public void deny() { mCallback.invoke(mOrigin, false, false); } @@ -127,6 +150,7 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu // For simplicity, also treat the read access needed for file:// URLs as a regular // PermissionRequest. + @SuppressLint("NewApi") // FilePermissionRequest class requires API level 21. private class FilePermissionRequest extends PermissionRequest { private String mOrigin; @@ -134,14 +158,17 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu mOrigin = origin; } + @Override public Uri getOrigin() { return Uri.parse(mOrigin); } + @Override public String[] getResources() { return new String[] { WebViewBrowserActivity.RESOURCE_FILE_URL }; } + @Override public void grant(String[] resources) { assert resources.length == 1; assert WebViewBrowserActivity.RESOURCE_FILE_URL.equals(resources[0]); @@ -149,20 +176,62 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu WebViewBrowserActivity.this.mWebView.loadUrl(mOrigin); } + @Override public void deny() { // womp womp } } + private static class TracingLogger extends FileOutputStream { + private long mByteCount; + private long mChunkCount; + private final Activity mActivity; + + public TracingLogger(String fileName, Activity activity) throws FileNotFoundException { + super(fileName); + mActivity = activity; + } + + @Override + public void write(byte[] chunk) throws IOException { + mByteCount += chunk.length; + mChunkCount++; + super.write(chunk); + } + + @Override + public void close() throws IOException { + super.close(); + showDialog(mByteCount); + } + + private void showDialog(long nbBytes) { + StringBuilder info = new StringBuilder(); + info.append("Tracing data written to file\n"); + info.append("number of bytes: " + nbBytes); + + mActivity.runOnUiThread(new Runnable() { + @Override + public void run() { + AlertDialog dialog = new AlertDialog.Builder(mActivity) + .setTitle("Tracing API") + .setMessage(info) + .setNeutralButton(" OK ", null) + .create(); + dialog.show(); + } + }); + } + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - WebView.setWebContentsDebuggingEnabled(true); - } + WebView.setWebContentsDebuggingEnabled(true); setContentView(R.layout.activity_webview_browser); mUrlBar = (EditText) findViewById(R.id.url_field); mUrlBar.setOnKeyListener(new OnKeyListener() { + @Override public boolean onKey(View view, int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { loadUrlFromUrlBar(view); @@ -172,13 +241,75 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu } }); + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectAll() + .penaltyLog() + .penaltyDeath() + .build()); + // Conspicuously omitted: detectCleartextNetwork() and detectFileUriExposure() to permit + // http:// and file:// origins. + StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() + .detectActivityLeaks() + .detectLeakedClosableObjects() + .detectLeakedRegistrationObjects() + .detectLeakedSqlLiteObjects() + .penaltyLog() + .penaltyDeath() + .build()); + createAndInitializeWebView(); String url = getUrlFromIntent(getIntent()); - if (url != null) { - setUrlBarText(url); - setUrlFail(false); - loadUrlFromUrlBar(mUrlBar); + if (url == null) { + mWebView.restoreState(savedInstanceState); + url = mWebView.getUrl(); + if (url != null) { + // If we have restored state, and that state includes + // a loaded URL, we reload. This allows us to keep the + // scroll offset, and also doesn't add an additional + // navigation history entry. + setUrlBarText(url); + // The immediately previous loadUrlFromurlbar must + // have got as far as calling loadUrl, so there is no + // URI parsing error at this point. + setUrlFail(false); + hideKeyboard(mUrlBar); + mWebView.reload(); + mWebView.requestFocus(); + return; + } + // Make sure to load a blank page to make it immediately inspectable with + // chrome://inspect. + url = "about:blank"; + } + setUrlBarText(url); + setUrlFail(false); + loadUrlFromUrlBar(mUrlBar); + } + + @Override + public void onSaveInstanceState(Bundle savedInstanceState) { + // Deliberately don't catch TransactionTooLargeException here. + mWebView.saveState(savedInstanceState); + + // TODO(timav): Remove this hack after http://crbug.com/626202 is fixed. + // Drop the saved state of it is too long since Android N and above + // can't handle large states without a crash. + byte[] webViewState = savedInstanceState.getByteArray(SAVE_RESTORE_STATE_KEY); + if (webViewState != null && webViewState.length > MAX_STATE_LENGTH) { + savedInstanceState.remove(SAVE_RESTORE_STATE_KEY); + String message = String.format( + Locale.US, "Can't save state: %dkb is too long", webViewState.length / 1024); + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onBackPressed() { + if (mWebView.canGoBack()) { + mWebView.goBack(); + } else { + super.onBackPressed(); } } @@ -210,18 +341,18 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu setUrlBarText(url); } + @SuppressWarnings("deprecation") // because we support api level 19 and up. @Override - public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { - String url = request.getUrl().toString(); + 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; } - boolean allowLaunchingApps = request.hasGesture() || request.isRedirect(); - return startBrowsingIntent(WebViewBrowserActivity.this, url, allowLaunchingApps); + return startBrowsingIntent(WebViewBrowserActivity.this, url); } + @SuppressWarnings("deprecation") // because we support api level 19 and up. @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { @@ -239,6 +370,13 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu @Override public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // Pre Lollipop versions (< api level 21) do not have PermissionRequest, + // hence grant here immediately. + callback.invoke(origin, true, false); + return; + } + onPermissionRequest(new GeoPermissionRequest(origin, callback)); } @@ -246,6 +384,28 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu public void onPermissionRequest(PermissionRequest request) { WebViewBrowserActivity.this.requestPermissionsForPage(request); } + + @Override + public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { + if (mFullscreenView != null) { + ((ViewGroup) mFullscreenView.getParent()).removeView(mFullscreenView); + } + mFullscreenView = view; + getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + getWindow().addContentView(mFullscreenView, + new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)); + } + + @Override + public void onHideCustomView() { + if (mFullscreenView == null) { + return; + } + getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + ((ViewGroup) mFullscreenView.getParent()).removeView(mFullscreenView); + mFullscreenView = null; + } }); mWebView = webview; @@ -256,14 +416,16 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu // WebKit permissions which can be granted because either they have no associated Android // permission or the associated Android permission has been granted + @TargetApi(Build.VERSION_CODES.M) private boolean canGrant(String webkitPermission) { String androidPermission = sPermissions.get(webkitPermission); - if (androidPermission == NO_ANDROID_PERMISSION) { + if (androidPermission.equals(NO_ANDROID_PERMISSION)) { return true; } return PackageManager.PERMISSION_GRANTED == checkSelfPermission(androidPermission); } + @SuppressLint("NewApi") // PermissionRequest#deny requires API level 21. private void requestPermissionsForPage(PermissionRequest request) { // Deny any unrecognized permissions. for (String webkitPermission : request.getResources()) { @@ -309,12 +471,14 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu } @Override + @SuppressLint("NewApi") // PermissionRequest#deny requires API level 21. 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); + mPendingRequests.delete(requestCode); for (String webkitPermission : request.getResources()) { if (!canGrant(webkitPermission)) { request.deny(); @@ -322,21 +486,14 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu } } 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; - } - + // Parse with android.net.Uri instead of java.net.URI because Uri does no validation. Rather + // than failing in the browser, let WebView handle weird URLs. WebView will escape illegal + // characters and display error pages for bad URLs like "blah://example.com". + if (Uri.parse(url).getScheme() == null) url = "http://" + url; setUrlBarText(url); setUrlFail(false); loadUrl(url); @@ -347,10 +504,12 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu PopupMenu popup = new PopupMenu(this, v); popup.setOnMenuItemClickListener(this); popup.inflate(R.menu.main_menu); + popup.getMenu().findItem(R.id.menu_enable_tracing).setChecked(mEnableTracing); popup.show(); } @Override + @SuppressLint("NewApi") // TracingController related methods require API level 28. public boolean onMenuItemClick(MenuItem item) { switch(item.getItemId()) { case R.id.menu_reset_webview: @@ -362,6 +521,42 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu } createAndInitializeWebView(); return true; + case R.id.menu_clear_cache: + if (mWebView != null) { + mWebView.clearCache(true); + } + return true; + case R.id.menu_enable_tracing: + mEnableTracing = !mEnableTracing; + item.setChecked(mEnableTracing); + TracingController tracingController = TracingController.getInstance(); + if (mEnableTracing) { + tracingController.start( + new TracingConfig.Builder() + .addCategories(TracingConfig.CATEGORIES_WEB_DEVELOPER) + .setTracingMode(TracingConfig.RECORD_CONTINUOUSLY) + .build()); + } else { + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); + String outFileName = getFilesDir() + "/webview_tracing.json"; + try { + tracingController.stop(new TracingLogger(outFileName, this), + Executors.newSingleThreadExecutor()); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + StrictMode.setThreadPolicy(oldPolicy); + } + return true; + case R.id.start_animation_activity: + startActivity(new Intent(this, WebViewAnimationTestActivity.class)); + return true; + case R.id.menu_print: + PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); + String jobName = "WebViewShell document"; + PrintDocumentAdapter printAdapter = mWebView.createPrintDocumentAdapter(jobName); + printManager.print(jobName, printAdapter, new PrintAttributes.Builder().build()); + return true; case R.id.menu_about: about(); hideKeyboard(mUrlBar); @@ -371,18 +566,33 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu } } + // setGeolocationDatabasePath deprecated in api level 24, + // but we still use it because we support api level 19 and up. + @SuppressWarnings("deprecation") private void initializeSettings(WebSettings settings) { + File appcache = null; + File geolocation = null; + + StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); + appcache = getDir("appcache", 0); + geolocation = getDir("geolocation", 0); + StrictMode.setThreadPolicy(oldPolicy); + 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.setAppCachePath(appcache.getPath()); + settings.setGeolocationDatabasePath(geolocation.getPath()); settings.setAppCacheEnabled(true); settings.setGeolocationEnabled(true); settings.setDatabaseEnabled(true); settings.setDomStorageEnabled(true); + + // Default layout behavior for chrome on android. + settings.setUseWideViewPort(true); + settings.setLoadWithOverviewMode(true); + settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING); } private void about() { @@ -404,7 +614,7 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu .setPositiveButton("OK", null) .create(); dialog.show(); - dialog.getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); + dialog.getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } // Returns true is a method has no arguments and returns either a boolean or a String. @@ -461,8 +671,7 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu + ")" + "(.*)"); - private static boolean startBrowsingIntent(Context context, String url, - boolean allowLaunchingApps) { + private static boolean startBrowsingIntent(Context context, String url) { Intent intent; // Perform generic parsing of the URI to turn it into an Intent. try { @@ -492,12 +701,16 @@ public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu // same application can be opened in the same tab. intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); try { - if (allowLaunchingApps) { - context.startActivity(intent); - } + context.startActivity(intent); return true; } catch (ActivityNotFoundException ex) { Log.w(TAG, "No application can handle " + 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 " + url); } return false; } |