1 // Copyright 2015 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.webview_shell; 6 7 import android.Manifest; 8 import android.annotation.SuppressLint; 9 import android.annotation.TargetApi; 10 import android.app.Activity; 11 import android.app.AlertDialog; 12 import android.content.ActivityNotFoundException; 13 import android.content.Context; 14 import android.content.Intent; 15 import android.content.IntentFilter; 16 import android.content.pm.PackageManager; 17 import android.content.pm.ResolveInfo; 18 import android.graphics.Bitmap; 19 import android.graphics.Color; 20 import android.net.Uri; 21 import android.os.Build; 22 import android.os.Bundle; 23 import android.os.StrictMode; 24 import android.print.PrintAttributes; 25 import android.print.PrintDocumentAdapter; 26 import android.print.PrintManager; 27 import android.provider.Browser; 28 import android.util.Log; 29 import android.util.SparseArray; 30 import android.view.Gravity; 31 import android.view.KeyEvent; 32 import android.view.MenuItem; 33 import android.view.View; 34 import android.view.View.OnKeyListener; 35 import android.view.ViewGroup; 36 import android.view.ViewGroup.LayoutParams; 37 import android.view.WindowManager; 38 import android.view.inputmethod.InputMethodManager; 39 import android.webkit.GeolocationPermissions; 40 import android.webkit.PermissionRequest; 41 import android.webkit.TracingConfig; 42 import android.webkit.TracingController; 43 import android.webkit.WebChromeClient; 44 import android.webkit.WebSettings; 45 import android.webkit.WebView; 46 import android.webkit.WebViewClient; 47 import android.widget.EditText; 48 import android.widget.FrameLayout; 49 import android.widget.PopupMenu; 50 import android.widget.TextView; 51 import android.widget.Toast; 52 53 import java.io.File; 54 import java.io.FileNotFoundException; 55 import java.io.FileOutputStream; 56 import java.io.IOException; 57 import java.lang.reflect.InvocationTargetException; 58 import java.lang.reflect.Method; 59 import java.util.ArrayList; 60 import java.util.HashMap; 61 import java.util.List; 62 import java.util.Locale; 63 import java.util.concurrent.Executors; 64 import java.util.regex.Matcher; 65 import java.util.regex.Pattern; 66 67 /** 68 * This activity is designed for starting a "mini-browser" for manual testing of WebView. 69 * It takes an optional URL as an argument, and displays the page. There is a URL bar 70 * on top of the webview for manually specifying URLs to load. 71 */ 72 public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenuItemClickListener { 73 private static final String TAG = "WebViewShell"; 74 75 // Our imaginary Android permission to associate with the WebKit geo permission 76 private static final String RESOURCE_GEO = "RESOURCE_GEO"; 77 // Our imaginary WebKit permission to request when loading a file:// URL 78 private static final String RESOURCE_FILE_URL = "RESOURCE_FILE_URL"; 79 // WebKit permissions with no corresponding Android permission can always be granted 80 private static final String NO_ANDROID_PERMISSION = "NO_ANDROID_PERMISSION"; 81 82 // TODO(timav): Remove these variables after http://crbug.com/626202 is fixed. 83 // The Bundle key for WebView serialized state 84 private static final String SAVE_RESTORE_STATE_KEY = "WEBVIEW_CHROMIUM_STATE"; 85 // Maximal size of this state. 86 private static final int MAX_STATE_LENGTH = 300 * 1024; 87 88 // Map from WebKit permissions to Android permissions 89 private static final HashMap<String, String> sPermissions; 90 static { 91 sPermissions = new HashMap<String, String>(); sPermissions.put(RESOURCE_GEO, Manifest.permission.ACCESS_FINE_LOCATION)92 sPermissions.put(RESOURCE_GEO, Manifest.permission.ACCESS_FINE_LOCATION); sPermissions.put(RESOURCE_FILE_URL, Manifest.permission.READ_EXTERNAL_STORAGE)93 sPermissions.put(RESOURCE_FILE_URL, Manifest.permission.READ_EXTERNAL_STORAGE); sPermissions.put(PermissionRequest.RESOURCE_AUDIO_CAPTURE, Manifest.permission.RECORD_AUDIO)94 sPermissions.put(PermissionRequest.RESOURCE_AUDIO_CAPTURE, 95 Manifest.permission.RECORD_AUDIO); sPermissions.put(PermissionRequest.RESOURCE_MIDI_SYSEX, NO_ANDROID_PERMISSION)96 sPermissions.put(PermissionRequest.RESOURCE_MIDI_SYSEX, NO_ANDROID_PERMISSION); sPermissions.put(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID, NO_ANDROID_PERMISSION)97 sPermissions.put(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID, NO_ANDROID_PERMISSION); sPermissions.put(PermissionRequest.RESOURCE_VIDEO_CAPTURE, Manifest.permission.CAMERA)98 sPermissions.put(PermissionRequest.RESOURCE_VIDEO_CAPTURE, 99 Manifest.permission.CAMERA); 100 } 101 102 private static final Pattern WEBVIEW_VERSION_PATTERN = 103 Pattern.compile("(Chrome/)([\\d\\.]+)\\s"); 104 105 private EditText mUrlBar; 106 private WebView mWebView; 107 private View mFullscreenView; 108 private String mWebViewVersion; 109 private boolean mEnableTracing; 110 111 // Each time we make a request, store it here with an int key. onRequestPermissionsResult will 112 // look up the request in order to grant the approprate permissions. 113 private SparseArray<PermissionRequest> mPendingRequests = new SparseArray<PermissionRequest>(); 114 private int mNextRequestKey; 115 116 // Work around our wonky API by wrapping a geo permission prompt inside a regular 117 // PermissionRequest. 118 @SuppressLint("NewApi") // GeoPermissionRequest class requires API level 21. 119 private static class GeoPermissionRequest extends PermissionRequest { 120 private String mOrigin; 121 private GeolocationPermissions.Callback mCallback; 122 GeoPermissionRequest(String origin, GeolocationPermissions.Callback callback)123 public GeoPermissionRequest(String origin, GeolocationPermissions.Callback callback) { 124 mOrigin = origin; 125 mCallback = callback; 126 } 127 128 @Override getOrigin()129 public Uri getOrigin() { 130 return Uri.parse(mOrigin); 131 } 132 133 @Override getResources()134 public String[] getResources() { 135 return new String[] { WebViewBrowserActivity.RESOURCE_GEO }; 136 } 137 138 @Override grant(String[] resources)139 public void grant(String[] resources) { 140 assert resources.length == 1; 141 assert WebViewBrowserActivity.RESOURCE_GEO.equals(resources[0]); 142 mCallback.invoke(mOrigin, true, false); 143 } 144 145 @Override deny()146 public void deny() { 147 mCallback.invoke(mOrigin, false, false); 148 } 149 } 150 151 // For simplicity, also treat the read access needed for file:// URLs as a regular 152 // PermissionRequest. 153 @SuppressLint("NewApi") // FilePermissionRequest class requires API level 21. 154 private class FilePermissionRequest extends PermissionRequest { 155 private String mOrigin; 156 FilePermissionRequest(String origin)157 public FilePermissionRequest(String origin) { 158 mOrigin = origin; 159 } 160 161 @Override getOrigin()162 public Uri getOrigin() { 163 return Uri.parse(mOrigin); 164 } 165 166 @Override getResources()167 public String[] getResources() { 168 return new String[] { WebViewBrowserActivity.RESOURCE_FILE_URL }; 169 } 170 171 @Override grant(String[] resources)172 public void grant(String[] resources) { 173 assert resources.length == 1; 174 assert WebViewBrowserActivity.RESOURCE_FILE_URL.equals(resources[0]); 175 // Try again now that we have read access. 176 WebViewBrowserActivity.this.mWebView.loadUrl(mOrigin); 177 } 178 179 @Override deny()180 public void deny() { 181 // womp womp 182 } 183 } 184 185 private static class TracingLogger extends FileOutputStream { 186 private long mByteCount; 187 private long mChunkCount; 188 private final Activity mActivity; 189 TracingLogger(String fileName, Activity activity)190 public TracingLogger(String fileName, Activity activity) throws FileNotFoundException { 191 super(fileName); 192 mActivity = activity; 193 } 194 195 @Override write(byte[] chunk)196 public void write(byte[] chunk) throws IOException { 197 mByteCount += chunk.length; 198 mChunkCount++; 199 super.write(chunk); 200 } 201 202 @Override close()203 public void close() throws IOException { 204 super.close(); 205 showDialog(mByteCount); 206 } 207 showDialog(long nbBytes)208 private void showDialog(long nbBytes) { 209 StringBuilder info = new StringBuilder(); 210 info.append("Tracing data written to file\n"); 211 info.append("number of bytes: " + nbBytes); 212 213 mActivity.runOnUiThread(new Runnable() { 214 @Override 215 public void run() { 216 AlertDialog dialog = new AlertDialog.Builder(mActivity) 217 .setTitle("Tracing API") 218 .setMessage(info) 219 .setNeutralButton(" OK ", null) 220 .create(); 221 dialog.show(); 222 } 223 }); 224 } 225 } 226 227 @Override onCreate(Bundle savedInstanceState)228 public void onCreate(Bundle savedInstanceState) { 229 super.onCreate(savedInstanceState); 230 WebView.setWebContentsDebuggingEnabled(true); 231 setContentView(R.layout.activity_webview_browser); 232 mUrlBar = (EditText) findViewById(R.id.url_field); 233 mUrlBar.setOnKeyListener(new OnKeyListener() { 234 @Override 235 public boolean onKey(View view, int keyCode, KeyEvent event) { 236 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) { 237 loadUrlFromUrlBar(view); 238 return true; 239 } 240 return false; 241 } 242 }); 243 244 StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() 245 .detectAll() 246 .penaltyLog() 247 .penaltyDeath() 248 .build()); 249 // Conspicuously omitted: detectCleartextNetwork() and detectFileUriExposure() to permit 250 // http:// and file:// origins. 251 StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() 252 .detectActivityLeaks() 253 .detectLeakedClosableObjects() 254 .detectLeakedRegistrationObjects() 255 .detectLeakedSqlLiteObjects() 256 .penaltyLog() 257 .penaltyDeath() 258 .build()); 259 260 createAndInitializeWebView(); 261 262 String url = getUrlFromIntent(getIntent()); 263 if (url == null) { 264 mWebView.restoreState(savedInstanceState); 265 url = mWebView.getUrl(); 266 if (url != null) { 267 // If we have restored state, and that state includes 268 // a loaded URL, we reload. This allows us to keep the 269 // scroll offset, and also doesn't add an additional 270 // navigation history entry. 271 setUrlBarText(url); 272 // The immediately previous loadUrlFromurlbar must 273 // have got as far as calling loadUrl, so there is no 274 // URI parsing error at this point. 275 setUrlFail(false); 276 hideKeyboard(mUrlBar); 277 mWebView.reload(); 278 mWebView.requestFocus(); 279 return; 280 } 281 // Make sure to load a blank page to make it immediately inspectable with 282 // chrome://inspect. 283 url = "about:blank"; 284 } 285 setUrlBarText(url); 286 setUrlFail(false); 287 loadUrlFromUrlBar(mUrlBar); 288 } 289 290 @Override onSaveInstanceState(Bundle savedInstanceState)291 public void onSaveInstanceState(Bundle savedInstanceState) { 292 // Deliberately don't catch TransactionTooLargeException here. 293 mWebView.saveState(savedInstanceState); 294 295 // TODO(timav): Remove this hack after http://crbug.com/626202 is fixed. 296 // Drop the saved state of it is too long since Android N and above 297 // can't handle large states without a crash. 298 byte[] webViewState = savedInstanceState.getByteArray(SAVE_RESTORE_STATE_KEY); 299 if (webViewState != null && webViewState.length > MAX_STATE_LENGTH) { 300 savedInstanceState.remove(SAVE_RESTORE_STATE_KEY); 301 String message = String.format( 302 Locale.US, "Can't save state: %dkb is too long", webViewState.length / 1024); 303 Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); 304 } 305 } 306 307 @Override onBackPressed()308 public void onBackPressed() { 309 if (mWebView.canGoBack()) { 310 mWebView.goBack(); 311 } else { 312 super.onBackPressed(); 313 } 314 } 315 getContainer()316 ViewGroup getContainer() { 317 return (ViewGroup) findViewById(R.id.container); 318 } 319 createAndInitializeWebView()320 private void createAndInitializeWebView() { 321 WebView webview = new WebView(this); 322 WebSettings settings = webview.getSettings(); 323 initializeSettings(settings); 324 325 Matcher matcher = WEBVIEW_VERSION_PATTERN.matcher(settings.getUserAgentString()); 326 if (matcher.find()) { 327 mWebViewVersion = matcher.group(2); 328 } else { 329 mWebViewVersion = "-"; 330 } 331 setTitle(getResources().getString(R.string.title_activity_browser) + " " + mWebViewVersion); 332 333 webview.setWebViewClient(new WebViewClient() { 334 @Override 335 public void onPageStarted(WebView view, String url, Bitmap favicon) { 336 setUrlBarText(url); 337 } 338 339 @Override 340 public void onPageFinished(WebView view, String url) { 341 setUrlBarText(url); 342 } 343 344 @SuppressWarnings("deprecation") // because we support api level 19 and up. 345 @Override 346 public boolean shouldOverrideUrlLoading(WebView webView, String url) { 347 // "about:" and "chrome:" schemes are internal to Chromium; 348 // don't want these to be dispatched to other apps. 349 if (url.startsWith("about:") || url.startsWith("chrome:")) { 350 return false; 351 } 352 return startBrowsingIntent(WebViewBrowserActivity.this, url); 353 } 354 355 @SuppressWarnings("deprecation") // because we support api level 19 and up. 356 @Override 357 public void onReceivedError(WebView view, int errorCode, String description, 358 String failingUrl) { 359 setUrlFail(true); 360 } 361 }); 362 363 webview.setWebChromeClient(new WebChromeClient() { 364 @Override 365 public Bitmap getDefaultVideoPoster() { 366 return Bitmap.createBitmap( 367 new int[] {Color.TRANSPARENT}, 1, 1, Bitmap.Config.ARGB_8888); 368 } 369 370 @Override 371 public void onGeolocationPermissionsShowPrompt(String origin, 372 GeolocationPermissions.Callback callback) { 373 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { 374 // Pre Lollipop versions (< api level 21) do not have PermissionRequest, 375 // hence grant here immediately. 376 callback.invoke(origin, true, false); 377 return; 378 } 379 380 onPermissionRequest(new GeoPermissionRequest(origin, callback)); 381 } 382 383 @Override 384 public void onPermissionRequest(PermissionRequest request) { 385 WebViewBrowserActivity.this.requestPermissionsForPage(request); 386 } 387 388 @Override 389 public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) { 390 if (mFullscreenView != null) { 391 ((ViewGroup) mFullscreenView.getParent()).removeView(mFullscreenView); 392 } 393 mFullscreenView = view; 394 getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 395 getWindow().addContentView(mFullscreenView, 396 new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 397 ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)); 398 } 399 400 @Override 401 public void onHideCustomView() { 402 if (mFullscreenView == null) { 403 return; 404 } 405 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 406 ((ViewGroup) mFullscreenView.getParent()).removeView(mFullscreenView); 407 mFullscreenView = null; 408 } 409 }); 410 411 mWebView = webview; 412 getContainer().addView( 413 webview, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); 414 setUrlBarText(""); 415 } 416 417 // WebKit permissions which can be granted because either they have no associated Android 418 // permission or the associated Android permission has been granted 419 @TargetApi(Build.VERSION_CODES.M) canGrant(String webkitPermission)420 private boolean canGrant(String webkitPermission) { 421 String androidPermission = sPermissions.get(webkitPermission); 422 if (androidPermission.equals(NO_ANDROID_PERMISSION)) { 423 return true; 424 } 425 return PackageManager.PERMISSION_GRANTED == checkSelfPermission(androidPermission); 426 } 427 428 @SuppressLint("NewApi") // PermissionRequest#deny requires API level 21. requestPermissionsForPage(PermissionRequest request)429 private void requestPermissionsForPage(PermissionRequest request) { 430 // Deny any unrecognized permissions. 431 for (String webkitPermission : request.getResources()) { 432 if (!sPermissions.containsKey(webkitPermission)) { 433 Log.w(TAG, "Unrecognized WebKit permission: " + webkitPermission); 434 request.deny(); 435 return; 436 } 437 } 438 439 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 440 request.grant(request.getResources()); 441 return; 442 } 443 444 // Find what Android permissions we need before we can grant these WebKit permissions. 445 ArrayList<String> androidPermissionsNeeded = new ArrayList<String>(); 446 for (String webkitPermission : request.getResources()) { 447 if (!canGrant(webkitPermission)) { 448 // We already checked for unrecognized permissions, and canGrant will skip over 449 // NO_ANDROID_PERMISSION cases, so this is guaranteed to be a regular Android 450 // permission. 451 String androidPermission = sPermissions.get(webkitPermission); 452 androidPermissionsNeeded.add(androidPermission); 453 } 454 } 455 456 // If there are no such Android permissions, grant the WebKit permissions immediately. 457 if (androidPermissionsNeeded.isEmpty()) { 458 request.grant(request.getResources()); 459 return; 460 } 461 462 // Otherwise, file a new request 463 if (mNextRequestKey == Integer.MAX_VALUE) { 464 Log.e(TAG, "Too many permission requests"); 465 return; 466 } 467 int requestCode = mNextRequestKey; 468 mNextRequestKey++; 469 mPendingRequests.append(requestCode, request); 470 requestPermissions(androidPermissionsNeeded.toArray(new String[0]), requestCode); 471 } 472 473 @Override 474 @SuppressLint("NewApi") // PermissionRequest#deny requires API level 21. onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults)475 public void onRequestPermissionsResult(int requestCode, 476 String permissions[], int[] grantResults) { 477 // Verify that we can now grant all the requested permissions. Note that although grant() 478 // takes a list of permissions, grant() is actually all-or-nothing. If there are any 479 // requested permissions not included in the granted permissions, all will be denied. 480 PermissionRequest request = mPendingRequests.get(requestCode); 481 mPendingRequests.delete(requestCode); 482 for (String webkitPermission : request.getResources()) { 483 if (!canGrant(webkitPermission)) { 484 request.deny(); 485 return; 486 } 487 } 488 request.grant(request.getResources()); 489 } 490 loadUrlFromUrlBar(View view)491 public void loadUrlFromUrlBar(View view) { 492 String url = mUrlBar.getText().toString(); 493 // Parse with android.net.Uri instead of java.net.URI because Uri does no validation. Rather 494 // than failing in the browser, let WebView handle weird URLs. WebView will escape illegal 495 // characters and display error pages for bad URLs like "blah://example.com". 496 if (Uri.parse(url).getScheme() == null) url = "http://" + url; 497 setUrlBarText(url); 498 setUrlFail(false); 499 loadUrl(url); 500 hideKeyboard(mUrlBar); 501 } 502 showPopup(View v)503 public void showPopup(View v) { 504 PopupMenu popup = new PopupMenu(this, v); 505 popup.setOnMenuItemClickListener(this); 506 popup.inflate(R.menu.main_menu); 507 popup.getMenu().findItem(R.id.menu_enable_tracing).setChecked(mEnableTracing); 508 popup.show(); 509 } 510 511 @Override 512 @SuppressLint("NewApi") // TracingController related methods require API level 28. onMenuItemClick(MenuItem item)513 public boolean onMenuItemClick(MenuItem item) { 514 switch(item.getItemId()) { 515 case R.id.menu_reset_webview: 516 if (mWebView != null) { 517 ViewGroup container = getContainer(); 518 container.removeView(mWebView); 519 mWebView.destroy(); 520 mWebView = null; 521 } 522 createAndInitializeWebView(); 523 return true; 524 case R.id.menu_clear_cache: 525 if (mWebView != null) { 526 mWebView.clearCache(true); 527 } 528 return true; 529 case R.id.menu_enable_tracing: 530 mEnableTracing = !mEnableTracing; 531 item.setChecked(mEnableTracing); 532 TracingController tracingController = TracingController.getInstance(); 533 if (mEnableTracing) { 534 tracingController.start( 535 new TracingConfig.Builder() 536 .addCategories(TracingConfig.CATEGORIES_WEB_DEVELOPER) 537 .setTracingMode(TracingConfig.RECORD_CONTINUOUSLY) 538 .build()); 539 } else { 540 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); 541 String outFileName = getFilesDir() + "/webview_tracing.json"; 542 try { 543 tracingController.stop(new TracingLogger(outFileName, this), 544 Executors.newSingleThreadExecutor()); 545 } catch (FileNotFoundException e) { 546 throw new RuntimeException(e); 547 } 548 StrictMode.setThreadPolicy(oldPolicy); 549 } 550 return true; 551 case R.id.start_animation_activity: 552 startActivity(new Intent(this, WebViewAnimationTestActivity.class)); 553 return true; 554 case R.id.menu_print: 555 PrintManager printManager = (PrintManager) getSystemService(Context.PRINT_SERVICE); 556 String jobName = "WebViewShell document"; 557 PrintDocumentAdapter printAdapter = mWebView.createPrintDocumentAdapter(jobName); 558 printManager.print(jobName, printAdapter, new PrintAttributes.Builder().build()); 559 return true; 560 case R.id.menu_about: 561 about(); 562 hideKeyboard(mUrlBar); 563 return true; 564 default: 565 return false; 566 } 567 } 568 569 // setGeolocationDatabasePath deprecated in api level 24, 570 // but we still use it because we support api level 19 and up. 571 @SuppressWarnings("deprecation") initializeSettings(WebSettings settings)572 private void initializeSettings(WebSettings settings) { 573 File appcache = null; 574 File geolocation = null; 575 576 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites(); 577 appcache = getDir("appcache", 0); 578 geolocation = getDir("geolocation", 0); 579 StrictMode.setThreadPolicy(oldPolicy); 580 581 settings.setJavaScriptEnabled(true); 582 583 // configure local storage apis and their database paths. 584 settings.setAppCachePath(appcache.getPath()); 585 settings.setGeolocationDatabasePath(geolocation.getPath()); 586 587 settings.setAppCacheEnabled(true); 588 settings.setGeolocationEnabled(true); 589 settings.setDatabaseEnabled(true); 590 settings.setDomStorageEnabled(true); 591 592 // Default layout behavior for chrome on android. 593 settings.setUseWideViewPort(true); 594 settings.setLoadWithOverviewMode(true); 595 settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING); 596 } 597 about()598 private void about() { 599 WebSettings settings = mWebView.getSettings(); 600 StringBuilder summary = new StringBuilder(); 601 summary.append("WebView version : " + mWebViewVersion + "\n"); 602 603 for (Method method : settings.getClass().getMethods()) { 604 if (!methodIsSimpleInspector(method)) continue; 605 try { 606 summary.append(method.getName() + " : " + method.invoke(settings) + "\n"); 607 } catch (IllegalAccessException e) { 608 } catch (InvocationTargetException e) { } 609 } 610 611 AlertDialog dialog = new AlertDialog.Builder(this) 612 .setTitle(getResources().getString(R.string.menu_about)) 613 .setMessage(summary) 614 .setPositiveButton("OK", null) 615 .create(); 616 dialog.show(); 617 dialog.getWindow().setLayout(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); 618 } 619 620 // Returns true is a method has no arguments and returns either a boolean or a String. methodIsSimpleInspector(Method method)621 private boolean methodIsSimpleInspector(Method method) { 622 Class<?> returnType = method.getReturnType(); 623 return ((returnType.equals(boolean.class) || returnType.equals(String.class)) 624 && method.getParameterTypes().length == 0); 625 } 626 loadUrl(String url)627 private void loadUrl(String url) { 628 // Request read access if necessary 629 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M 630 && "file".equals(Uri.parse(url).getScheme()) 631 && PackageManager.PERMISSION_DENIED 632 == checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE)) { 633 requestPermissionsForPage(new FilePermissionRequest(url)); 634 } 635 636 // If it is file:// and we don't have permission, they'll get the "Webpage not available" 637 // "net::ERR_ACCESS_DENIED" page. When we get permission, FilePermissionRequest.grant() 638 // will reload. 639 mWebView.loadUrl(url); 640 mWebView.requestFocus(); 641 } 642 setUrlBarText(String url)643 private void setUrlBarText(String url) { 644 mUrlBar.setText(url, TextView.BufferType.EDITABLE); 645 } 646 setUrlFail(boolean fail)647 private void setUrlFail(boolean fail) { 648 mUrlBar.setTextColor(fail ? Color.RED : Color.BLACK); 649 } 650 651 /** 652 * Hides the keyboard. 653 * @param view The {@link View} that is currently accepting input. 654 * @return Whether the keyboard was visible before. 655 */ hideKeyboard(View view)656 private static boolean hideKeyboard(View view) { 657 InputMethodManager imm = (InputMethodManager) view.getContext().getSystemService( 658 Context.INPUT_METHOD_SERVICE); 659 return imm.hideSoftInputFromWindow(view.getWindowToken(), 0); 660 } 661 getUrlFromIntent(Intent intent)662 private static String getUrlFromIntent(Intent intent) { 663 return intent != null ? intent.getDataString() : null; 664 } 665 666 static final Pattern BROWSER_URI_SCHEMA = Pattern.compile( 667 "(?i)" // switch on case insensitive matching 668 + "(" // begin group for schema 669 + "(?:http|https|file):\\/\\/" 670 + "|(?:inline|data|about|chrome|javascript):" 671 + ")" 672 + "(.*)"); 673 startBrowsingIntent(Context context, String url)674 private static boolean startBrowsingIntent(Context context, String url) { 675 Intent intent; 676 // Perform generic parsing of the URI to turn it into an Intent. 677 try { 678 intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); 679 } catch (Exception ex) { 680 Log.w(TAG, "Bad URI " + url, ex); 681 return false; 682 } 683 // Check for regular URIs that WebView supports by itself, but also 684 // check if there is a specialized app that had registered itself 685 // for this kind of an intent. 686 Matcher m = BROWSER_URI_SCHEMA.matcher(url); 687 if (m.matches() && !isSpecializedHandlerAvailable(context, intent)) { 688 return false; 689 } 690 // Sanitize the Intent, ensuring web pages can not bypass browser 691 // security (only access to BROWSABLE activities). 692 intent.addCategory(Intent.CATEGORY_BROWSABLE); 693 intent.setComponent(null); 694 Intent selector = intent.getSelector(); 695 if (selector != null) { 696 selector.addCategory(Intent.CATEGORY_BROWSABLE); 697 selector.setComponent(null); 698 } 699 700 // Pass the package name as application ID so that the intent from the 701 // same application can be opened in the same tab. 702 intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); 703 try { 704 context.startActivity(intent); 705 return true; 706 } catch (ActivityNotFoundException ex) { 707 Log.w(TAG, "No application can handle " + url); 708 } catch (SecurityException ex) { 709 // This can happen if the Activity is exported="true", guarded by a permission, and sets 710 // up an intent filter matching this intent. This is a valid configuration for an 711 // Activity, so instead of crashing, we catch the exception and do nothing. See 712 // https://crbug.com/808494 and https://crbug.com/889300. 713 Log.w(TAG, "SecurityException when starting intent for " + url); 714 } 715 return false; 716 } 717 718 /** 719 * Search for intent handlers that are specific to the scheme of the URL in the intent. 720 */ isSpecializedHandlerAvailable(Context context, Intent intent)721 private static boolean isSpecializedHandlerAvailable(Context context, Intent intent) { 722 PackageManager pm = context.getPackageManager(); 723 List<ResolveInfo> handlers = pm.queryIntentActivities(intent, 724 PackageManager.GET_RESOLVED_FILTER); 725 if (handlers == null || handlers.size() == 0) { 726 return false; 727 } 728 for (ResolveInfo resolveInfo : handlers) { 729 if (!isNullOrGenericHandler(resolveInfo.filter)) { 730 return true; 731 } 732 } 733 return false; 734 } 735 isNullOrGenericHandler(IntentFilter filter)736 private static boolean isNullOrGenericHandler(IntentFilter filter) { 737 return filter == null 738 || (filter.countDataAuthorities() == 0 && filter.countDataPaths() == 0); 739 } 740 } 741