OLD | NEW |
| (Empty) |
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.app.Activity; | |
9 import android.app.AlertDialog; | |
10 import android.content.ActivityNotFoundException; | |
11 import android.content.Context; | |
12 import android.content.Intent; | |
13 import android.content.IntentFilter; | |
14 import android.content.pm.PackageManager; | |
15 import android.content.pm.ResolveInfo; | |
16 import android.graphics.Bitmap; | |
17 import android.graphics.Color; | |
18 import android.net.Uri; | |
19 import android.os.Build; | |
20 import android.os.Bundle; | |
21 import android.provider.Browser; | |
22 import android.util.SparseArray; | |
23 | |
24 import android.view.KeyEvent; | |
25 import android.view.MenuItem; | |
26 import android.view.View; | |
27 import android.view.View.OnKeyListener; | |
28 import android.view.ViewGroup; | |
29 import android.view.ViewGroup.LayoutParams; | |
30 import android.view.inputmethod.InputMethodManager; | |
31 | |
32 import android.webkit.GeolocationPermissions; | |
33 import android.webkit.PermissionRequest; | |
34 import android.webkit.WebChromeClient; | |
35 import android.webkit.WebSettings; | |
36 import android.webkit.WebView; | |
37 import android.webkit.WebViewClient; | |
38 | |
39 import android.widget.EditText; | |
40 import android.widget.PopupMenu; | |
41 import android.widget.TextView; | |
42 | |
43 import org.chromium.base.Log; | |
44 | |
45 import java.lang.reflect.InvocationTargetException; | |
46 import java.lang.reflect.Method; | |
47 | |
48 import java.net.URI; | |
49 import java.net.URISyntaxException; | |
50 | |
51 import java.util.ArrayList; | |
52 import java.util.HashMap; | |
53 import java.util.List; | |
54 import java.util.regex.Matcher; | |
55 import java.util.regex.Pattern; | |
56 | |
57 /** | |
58 * This activity is designed for starting a "mini-browser" for manual testing of
WebView. | |
59 * It takes an optional URL as an argument, and displays the page. There is a UR
L bar | |
60 * on top of the webview for manually specifying URLs to load. | |
61 */ | |
62 public class WebViewBrowserActivity extends Activity implements PopupMenu.OnMenu
ItemClickListener { | |
63 private static final String TAG = "WebViewShell"; | |
64 | |
65 // Our imaginary Android permission to associate with the WebKit geo permiss
ion | |
66 private static final String RESOURCE_GEO = "RESOURCE_GEO"; | |
67 // Our imaginary WebKit permission to request when loading a file:// URL | |
68 private static final String RESOURCE_FILE_URL = "RESOURCE_FILE_URL"; | |
69 // WebKit permissions with no corresponding Android permission can always be
granted | |
70 private static final String NO_ANDROID_PERMISSION = "NO_ANDROID_PERMISSION"; | |
71 | |
72 // Map from WebKit permissions to Android permissions | |
73 private static final HashMap<String, String> sPermissions; | |
74 static { | |
75 sPermissions = new HashMap<String, String>(); | |
76 sPermissions.put(RESOURCE_GEO, Manifest.permission.ACCESS_FINE_LOCATION)
; | |
77 sPermissions.put(RESOURCE_FILE_URL, Manifest.permission.READ_EXTERNAL_ST
ORAGE); | |
78 sPermissions.put(PermissionRequest.RESOURCE_AUDIO_CAPTURE, | |
79 Manifest.permission.RECORD_AUDIO); | |
80 sPermissions.put(PermissionRequest.RESOURCE_MIDI_SYSEX, NO_ANDROID_PERMI
SSION); | |
81 sPermissions.put(PermissionRequest.RESOURCE_PROTECTED_MEDIA_ID, NO_ANDRO
ID_PERMISSION); | |
82 sPermissions.put(PermissionRequest.RESOURCE_VIDEO_CAPTURE, | |
83 Manifest.permission.CAMERA); | |
84 } | |
85 | |
86 private static final Pattern WEBVIEW_VERSION_PATTERN = | |
87 Pattern.compile("(Chrome/)([\\d\\.]+)\\s"); | |
88 | |
89 private EditText mUrlBar; | |
90 private WebView mWebView; | |
91 private String mWebViewVersion; | |
92 | |
93 // Each time we make a request, store it here with an int key. onRequestPerm
issionsResult will | |
94 // look up the request in order to grant the approprate permissions. | |
95 private SparseArray<PermissionRequest> mPendingRequests = new SparseArray<Pe
rmissionRequest>(); | |
96 private int mNextRequestKey = 0; | |
97 | |
98 // Work around our wonky API by wrapping a geo permission prompt inside a re
gular | |
99 // PermissionRequest. | |
100 private static class GeoPermissionRequest extends PermissionRequest { | |
101 private String mOrigin; | |
102 private GeolocationPermissions.Callback mCallback; | |
103 | |
104 public GeoPermissionRequest(String origin, GeolocationPermissions.Callba
ck callback) { | |
105 mOrigin = origin; | |
106 mCallback = callback; | |
107 } | |
108 | |
109 public Uri getOrigin() { | |
110 return Uri.parse(mOrigin); | |
111 } | |
112 | |
113 public String[] getResources() { | |
114 return new String[] { WebViewBrowserActivity.RESOURCE_GEO }; | |
115 } | |
116 | |
117 public void grant(String[] resources) { | |
118 assert resources.length == 1; | |
119 assert WebViewBrowserActivity.RESOURCE_GEO.equals(resources[0]); | |
120 mCallback.invoke(mOrigin, true, false); | |
121 } | |
122 | |
123 public void deny() { | |
124 mCallback.invoke(mOrigin, false, false); | |
125 } | |
126 } | |
127 | |
128 // For simplicity, also treat the read access needed for file:// URLs as a r
egular | |
129 // PermissionRequest. | |
130 private class FilePermissionRequest extends PermissionRequest { | |
131 private String mOrigin; | |
132 | |
133 public FilePermissionRequest(String origin) { | |
134 mOrigin = origin; | |
135 } | |
136 | |
137 public Uri getOrigin() { | |
138 return Uri.parse(mOrigin); | |
139 } | |
140 | |
141 public String[] getResources() { | |
142 return new String[] { WebViewBrowserActivity.RESOURCE_FILE_URL }; | |
143 } | |
144 | |
145 public void grant(String[] resources) { | |
146 assert resources.length == 1; | |
147 assert WebViewBrowserActivity.RESOURCE_FILE_URL.equals(resources[0])
; | |
148 // Try again now that we have read access. | |
149 WebViewBrowserActivity.this.mWebView.loadUrl(mOrigin); | |
150 } | |
151 | |
152 public void deny() { | |
153 // womp womp | |
154 } | |
155 } | |
156 | |
157 @Override | |
158 public void onCreate(Bundle savedInstanceState) { | |
159 super.onCreate(savedInstanceState); | |
160 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { | |
161 WebView.setWebContentsDebuggingEnabled(true); | |
162 } | |
163 setContentView(R.layout.activity_webview_browser); | |
164 mUrlBar = (EditText) findViewById(R.id.url_field); | |
165 mUrlBar.setOnKeyListener(new OnKeyListener() { | |
166 public boolean onKey(View view, int keyCode, KeyEvent event) { | |
167 if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == Ke
yEvent.ACTION_UP) { | |
168 loadUrlFromUrlBar(view); | |
169 return true; | |
170 } | |
171 return false; | |
172 } | |
173 }); | |
174 | |
175 createAndInitializeWebView(); | |
176 | |
177 String url = getUrlFromIntent(getIntent()); | |
178 if (url != null) { | |
179 setUrlBarText(url); | |
180 setUrlFail(false); | |
181 loadUrlFromUrlBar(mUrlBar); | |
182 } | |
183 } | |
184 | |
185 ViewGroup getContainer() { | |
186 return (ViewGroup) findViewById(R.id.container); | |
187 } | |
188 | |
189 private void createAndInitializeWebView() { | |
190 WebView webview = new WebView(this); | |
191 WebSettings settings = webview.getSettings(); | |
192 initializeSettings(settings); | |
193 | |
194 Matcher matcher = WEBVIEW_VERSION_PATTERN.matcher(settings.getUserAgentS
tring()); | |
195 if (matcher.find()) { | |
196 mWebViewVersion = matcher.group(2); | |
197 } else { | |
198 mWebViewVersion = "-"; | |
199 } | |
200 setTitle(getResources().getString(R.string.title_activity_browser) + " "
+ mWebViewVersion); | |
201 | |
202 webview.setWebViewClient(new WebViewClient() { | |
203 @Override | |
204 public void onPageStarted(WebView view, String url, Bitmap favicon)
{ | |
205 setUrlBarText(url); | |
206 } | |
207 | |
208 @Override | |
209 public void onPageFinished(WebView view, String url) { | |
210 setUrlBarText(url); | |
211 } | |
212 | |
213 @Override | |
214 public boolean shouldOverrideUrlLoading(WebView webView, String url)
{ | |
215 // "about:" and "chrome:" schemes are internal to Chromium; | |
216 // don't want these to be dispatched to other apps. | |
217 if (url.startsWith("about:") || url.startsWith("chrome:")) { | |
218 return false; | |
219 } | |
220 return startBrowsingIntent(WebViewBrowserActivity.this, url); | |
221 } | |
222 | |
223 @Override | |
224 public void onReceivedError(WebView view, int errorCode, String desc
ription, | |
225 String failingUrl) { | |
226 setUrlFail(true); | |
227 } | |
228 }); | |
229 | |
230 webview.setWebChromeClient(new WebChromeClient() { | |
231 @Override | |
232 public Bitmap getDefaultVideoPoster() { | |
233 return Bitmap.createBitmap( | |
234 new int[] {Color.TRANSPARENT}, 1, 1, Bitmap.Config.ARGB_
8888); | |
235 } | |
236 | |
237 @Override | |
238 public void onGeolocationPermissionsShowPrompt(String origin, | |
239 GeolocationPermissions.Callback callback) { | |
240 onPermissionRequest(new GeoPermissionRequest(origin, callback)); | |
241 } | |
242 | |
243 @Override | |
244 public void onPermissionRequest(PermissionRequest request) { | |
245 WebViewBrowserActivity.this.requestPermissionsForPage(request); | |
246 } | |
247 }); | |
248 | |
249 mWebView = webview; | |
250 getContainer().addView( | |
251 webview, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParam
s.MATCH_PARENT)); | |
252 setUrlBarText(""); | |
253 } | |
254 | |
255 // WebKit permissions which can be granted because either they have no assoc
iated Android | |
256 // permission or the associated Android permission has been granted | |
257 private boolean canGrant(String webkitPermission) { | |
258 String androidPermission = sPermissions.get(webkitPermission); | |
259 if (androidPermission == NO_ANDROID_PERMISSION) { | |
260 return true; | |
261 } | |
262 return PackageManager.PERMISSION_GRANTED == checkSelfPermission(androidP
ermission); | |
263 } | |
264 | |
265 private void requestPermissionsForPage(PermissionRequest request) { | |
266 // Deny any unrecognized permissions. | |
267 for (String webkitPermission : request.getResources()) { | |
268 if (!sPermissions.containsKey(webkitPermission)) { | |
269 Log.w(TAG, "Unrecognized WebKit permission: " + webkitPermission
); | |
270 request.deny(); | |
271 return; | |
272 } | |
273 } | |
274 | |
275 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { | |
276 request.grant(request.getResources()); | |
277 return; | |
278 } | |
279 | |
280 // Find what Android permissions we need before we can grant these WebKi
t permissions. | |
281 ArrayList<String> androidPermissionsNeeded = new ArrayList<String>(); | |
282 for (String webkitPermission : request.getResources()) { | |
283 if (!canGrant(webkitPermission)) { | |
284 // We already checked for unrecognized permissions, and canGrant
will skip over | |
285 // NO_ANDROID_PERMISSION cases, so this is guaranteed to be a re
gular Android | |
286 // permission. | |
287 String androidPermission = sPermissions.get(webkitPermission); | |
288 androidPermissionsNeeded.add(androidPermission); | |
289 } | |
290 } | |
291 | |
292 // If there are no such Android permissions, grant the WebKit permission
s immediately. | |
293 if (androidPermissionsNeeded.isEmpty()) { | |
294 request.grant(request.getResources()); | |
295 return; | |
296 } | |
297 | |
298 // Otherwise, file a new request | |
299 if (mNextRequestKey == Integer.MAX_VALUE) { | |
300 Log.e(TAG, "Too many permission requests"); | |
301 return; | |
302 } | |
303 int requestCode = mNextRequestKey; | |
304 mNextRequestKey++; | |
305 mPendingRequests.append(requestCode, request); | |
306 requestPermissions(androidPermissionsNeeded.toArray(new String[0]), requ
estCode); | |
307 } | |
308 | |
309 @Override | |
310 public void onRequestPermissionsResult(int requestCode, | |
311 String permissions[], int[] grantResults) { | |
312 // Verify that we can now grant all the requested permissions. Note that
although grant() | |
313 // takes a list of permissions, grant() is actually all-or-nothing. If t
here are any | |
314 // requested permissions not included in the granted permissions, all wi
ll be denied. | |
315 PermissionRequest request = mPendingRequests.get(requestCode); | |
316 for (String webkitPermission : request.getResources()) { | |
317 if (!canGrant(webkitPermission)) { | |
318 request.deny(); | |
319 return; | |
320 } | |
321 } | |
322 request.grant(request.getResources()); | |
323 mPendingRequests.delete(requestCode); | |
324 } | |
325 | |
326 public void loadUrlFromUrlBar(View view) { | |
327 String url = mUrlBar.getText().toString(); | |
328 try { | |
329 URI uri = new URI(url); | |
330 url = (uri.getScheme() == null) ? "http://" + uri.toString() : uri.t
oString(); | |
331 } catch (URISyntaxException e) { | |
332 String message = "<html><body>URISyntaxException: " + e.getMessage()
+ "</body></html>"; | |
333 mWebView.loadData(message, "text/html", "UTF-8"); | |
334 setUrlFail(true); | |
335 return; | |
336 } | |
337 | |
338 setUrlBarText(url); | |
339 setUrlFail(false); | |
340 loadUrl(url); | |
341 hideKeyboard(mUrlBar); | |
342 } | |
343 | |
344 public void showPopup(View v) { | |
345 PopupMenu popup = new PopupMenu(this, v); | |
346 popup.setOnMenuItemClickListener(this); | |
347 popup.inflate(R.menu.main_menu); | |
348 popup.show(); | |
349 } | |
350 | |
351 @Override | |
352 public boolean onMenuItemClick(MenuItem item) { | |
353 switch(item.getItemId()) { | |
354 case R.id.menu_reset_webview: | |
355 if (mWebView != null) { | |
356 ViewGroup container = getContainer(); | |
357 container.removeView(mWebView); | |
358 mWebView.destroy(); | |
359 mWebView = null; | |
360 } | |
361 createAndInitializeWebView(); | |
362 return true; | |
363 case R.id.menu_clear_cache: | |
364 if (mWebView != null) { | |
365 mWebView.clearCache(true); | |
366 } | |
367 return true; | |
368 case R.id.menu_about: | |
369 about(); | |
370 hideKeyboard(mUrlBar); | |
371 return true; | |
372 default: | |
373 return false; | |
374 } | |
375 } | |
376 | |
377 private void initializeSettings(WebSettings settings) { | |
378 settings.setJavaScriptEnabled(true); | |
379 | |
380 // configure local storage apis and their database paths. | |
381 settings.setAppCachePath(getDir("appcache", 0).getPath()); | |
382 settings.setGeolocationDatabasePath(getDir("geolocation", 0).getPath()); | |
383 settings.setDatabasePath(getDir("databases", 0).getPath()); | |
384 | |
385 settings.setAppCacheEnabled(true); | |
386 settings.setGeolocationEnabled(true); | |
387 settings.setDatabaseEnabled(true); | |
388 settings.setDomStorageEnabled(true); | |
389 } | |
390 | |
391 private void about() { | |
392 WebSettings settings = mWebView.getSettings(); | |
393 StringBuilder summary = new StringBuilder(); | |
394 summary.append("WebView version : " + mWebViewVersion + "\n"); | |
395 | |
396 for (Method method : settings.getClass().getMethods()) { | |
397 if (!methodIsSimpleInspector(method)) continue; | |
398 try { | |
399 summary.append(method.getName() + " : " + method.invoke(settings
) + "\n"); | |
400 } catch (IllegalAccessException e) { | |
401 } catch (InvocationTargetException e) { } | |
402 } | |
403 | |
404 AlertDialog dialog = new AlertDialog.Builder(this) | |
405 .setTitle(getResources().getString(R.string.menu_about)) | |
406 .setMessage(summary) | |
407 .setPositiveButton("OK", null) | |
408 .create(); | |
409 dialog.show(); | |
410 dialog.getWindow().setLayout(LayoutParams.FILL_PARENT, LayoutParams.FILL
_PARENT); | |
411 } | |
412 | |
413 // Returns true is a method has no arguments and returns either a boolean or
a String. | |
414 private boolean methodIsSimpleInspector(Method method) { | |
415 Class<?> returnType = method.getReturnType(); | |
416 return ((returnType.equals(boolean.class) || returnType.equals(String.cl
ass)) | |
417 && method.getParameterTypes().length == 0); | |
418 } | |
419 | |
420 private void loadUrl(String url) { | |
421 // Request read access if necessary | |
422 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M | |
423 && "file".equals(Uri.parse(url).getScheme()) | |
424 && PackageManager.PERMISSION_DENIED | |
425 == checkSelfPermission(Manifest.permission.READ_EXTERNAL
_STORAGE)) { | |
426 requestPermissionsForPage(new FilePermissionRequest(url)); | |
427 } | |
428 | |
429 // If it is file:// and we don't have permission, they'll get the "Webpa
ge not available" | |
430 // "net::ERR_ACCESS_DENIED" page. When we get permission, FilePermission
Request.grant() | |
431 // will reload. | |
432 mWebView.loadUrl(url); | |
433 mWebView.requestFocus(); | |
434 } | |
435 | |
436 private void setUrlBarText(String url) { | |
437 mUrlBar.setText(url, TextView.BufferType.EDITABLE); | |
438 } | |
439 | |
440 private void setUrlFail(boolean fail) { | |
441 mUrlBar.setTextColor(fail ? Color.RED : Color.BLACK); | |
442 } | |
443 | |
444 /** | |
445 * Hides the keyboard. | |
446 * @param view The {@link View} that is currently accepting input. | |
447 * @return Whether the keyboard was visible before. | |
448 */ | |
449 private static boolean hideKeyboard(View view) { | |
450 InputMethodManager imm = (InputMethodManager) view.getContext().getSyste
mService( | |
451 Context.INPUT_METHOD_SERVICE); | |
452 return imm.hideSoftInputFromWindow(view.getWindowToken(), 0); | |
453 } | |
454 | |
455 private static String getUrlFromIntent(Intent intent) { | |
456 return intent != null ? intent.getDataString() : null; | |
457 } | |
458 | |
459 static final Pattern BROWSER_URI_SCHEMA = Pattern.compile( | |
460 "(?i)" // switch on case insensitive matching | |
461 + "(" // begin group for schema | |
462 + "(?:http|https|file):\\/\\/" | |
463 + "|(?:inline|data|about|chrome|javascript):" | |
464 + ")" | |
465 + "(.*)"); | |
466 | |
467 private static boolean startBrowsingIntent(Context context, String url) { | |
468 Intent intent; | |
469 // Perform generic parsing of the URI to turn it into an Intent. | |
470 try { | |
471 intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); | |
472 } catch (Exception ex) { | |
473 Log.w(TAG, "Bad URI %s", url, ex); | |
474 return false; | |
475 } | |
476 // Check for regular URIs that WebView supports by itself, but also | |
477 // check if there is a specialized app that had registered itself | |
478 // for this kind of an intent. | |
479 Matcher m = BROWSER_URI_SCHEMA.matcher(url); | |
480 if (m.matches() && !isSpecializedHandlerAvailable(context, intent)) { | |
481 return false; | |
482 } | |
483 // Sanitize the Intent, ensuring web pages can not bypass browser | |
484 // security (only access to BROWSABLE activities). | |
485 intent.addCategory(Intent.CATEGORY_BROWSABLE); | |
486 intent.setComponent(null); | |
487 Intent selector = intent.getSelector(); | |
488 if (selector != null) { | |
489 selector.addCategory(Intent.CATEGORY_BROWSABLE); | |
490 selector.setComponent(null); | |
491 } | |
492 | |
493 // Pass the package name as application ID so that the intent from the | |
494 // same application can be opened in the same tab. | |
495 intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); | |
496 try { | |
497 context.startActivity(intent); | |
498 return true; | |
499 } catch (ActivityNotFoundException ex) { | |
500 Log.w(TAG, "No application can handle %s", url); | |
501 } | |
502 return false; | |
503 } | |
504 | |
505 /** | |
506 * Search for intent handlers that are specific to the scheme of the URL in
the intent. | |
507 */ | |
508 private static boolean isSpecializedHandlerAvailable(Context context, Intent
intent) { | |
509 PackageManager pm = context.getPackageManager(); | |
510 List<ResolveInfo> handlers = pm.queryIntentActivities(intent, | |
511 PackageManager.GET_RESOLVED_FILTER); | |
512 if (handlers == null || handlers.size() == 0) { | |
513 return false; | |
514 } | |
515 for (ResolveInfo resolveInfo : handlers) { | |
516 if (!isNullOrGenericHandler(resolveInfo.filter)) { | |
517 return true; | |
518 } | |
519 } | |
520 return false; | |
521 } | |
522 | |
523 private static boolean isNullOrGenericHandler(IntentFilter filter) { | |
524 return filter == null | |
525 || (filter.countDataAuthorities() == 0 && filter.countDataPaths(
) == 0); | |
526 } | |
527 } | |
OLD | NEW |