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