WebViewContentsClientAdapter.java revision 782fea892aaa4a0867547697d49f6a1dd265d437
1/* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.webview.chromium; 18 19import android.content.ActivityNotFoundException; 20import android.content.Context; 21import android.content.Intent; 22import android.graphics.Bitmap; 23import android.graphics.Picture; 24import android.net.http.SslError; 25import android.os.Handler; 26import android.os.Looper; 27import android.os.Message; 28import android.provider.Browser; 29import android.util.Log; 30import android.view.KeyEvent; 31import android.view.View; 32import android.webkit.ConsoleMessage; 33import android.webkit.DownloadListener; 34import android.webkit.GeolocationPermissions; 35import android.webkit.JsPromptResult; 36import android.webkit.JsResult; 37import android.webkit.SslErrorHandler; 38import android.webkit.ValueCallback; 39import android.webkit.WebChromeClient; 40import android.webkit.WebChromeClient.CustomViewCallback; 41import android.webkit.WebResourceResponse; 42import android.webkit.WebView; 43import android.webkit.WebViewClient; 44 45import org.chromium.android_webview.AwContentsClient; 46import org.chromium.android_webview.AwHttpAuthHandler; 47import org.chromium.android_webview.InterceptedRequestData; 48import org.chromium.android_webview.JsPromptResultReceiver; 49import org.chromium.android_webview.JsResultReceiver; 50import org.chromium.content.browser.ContentView; 51import org.chromium.content.browser.ContentViewClient; 52 53import java.net.URISyntaxException; 54 55/** 56 * An adapter class that forwards the callbacks from {@link ContentViewClient} 57 * to the appropriate {@link WebViewClient} or {@link WebChromeClient}. 58 * 59 * An instance of this class is associated with one {@link WebViewChromium} 60 * instance. A WebViewChromium is a WebView implementation provider (that is 61 * android.webkit.WebView delegates all functionality to it) and has exactly 62 * one corresponding {@link ContentView} instance. 63 * 64 * A {@link ContentViewClient} may be shared between multiple {@link ContentView}s, 65 * and hence multiple WebViews. Many WebViewClient methods pass the source 66 * WebView as an argument. This means that we either need to pass the 67 * corresponding ContentView to the corresponding ContentViewClient methods, 68 * or use an instance of ContentViewClientAdapter per WebViewChromium, to 69 * allow the source WebView to be injected by ContentViewClientAdapter. We 70 * choose the latter, because it makes for a cleaner design. 71 */ 72public class WebViewContentsClientAdapter extends AwContentsClient { 73 private static final String TAG = "ContentViewClientAdapter"; 74 // The WebView instance that this adapter is serving. 75 private final WebView mWebView; 76 // The WebViewClient instance that was passed to WebView.setWebViewClient(). 77 private WebViewClient mWebViewClient; 78 // The WebViewClient instance that was passed to WebView.setContentViewClient(). 79 private WebChromeClient mWebChromeClient; 80 // The listener receiving find-in-page API results. 81 private WebView.FindListener mFindListener; 82 // The listener receiving notifications of screen updates. 83 private WebView.PictureListener mPictureListener; 84 85 private DownloadListener mDownloadListener; 86 87 private Handler mUiThreadHandler; 88 89 private static final int NEW_WEBVIEW_CREATED = 100; 90 91 /** 92 * Adapter constructor. 93 * 94 * @param webView the {@link WebView} instance that this adapter is serving. 95 */ 96 WebViewContentsClientAdapter(WebView webView) { 97 if (webView == null) { 98 throw new IllegalArgumentException("webView can't be null"); 99 } 100 101 mWebView = webView; 102 setWebViewClient(null); 103 setWebChromeClient(null); 104 105 mUiThreadHandler = new Handler() { 106 107 @Override 108 public void handleMessage(Message msg) { 109 switch(msg.what) { 110 case NEW_WEBVIEW_CREATED: 111 WebView.WebViewTransport t = (WebView.WebViewTransport) msg.obj; 112 WebView newWebView = t.getWebView(); 113 if (newWebView == null) { 114 throw new IllegalArgumentException( 115 "Must provide a new WebView for the new window."); 116 } 117 if (newWebView == mWebView) { 118 throw new IllegalArgumentException( 119 "Parent WebView cannot host it's own popup window. Please " + 120 "use WebSettings.setSupportMultipleWindows(false)"); 121 } 122 123 if (newWebView.copyBackForwardList().getSize() != 0) { 124 throw new IllegalArgumentException( 125 "New WebView for popup window must not have been previously " + 126 "navigated."); 127 } 128 129 WebViewChromium.completeWindowCreation(mWebView, newWebView); 130 break; 131 default: 132 throw new IllegalStateException(); 133 } 134 } 135 }; 136 137 } 138 139 // WebViewClassic is coded in such a way that even if a null WebViewClient is set, 140 // certain actions take place. 141 // We choose to replicate this behavior by using a NullWebViewClient implementation (also known 142 // as the Null Object pattern) rather than duplicating the WebViewClassic approach in 143 // ContentView. 144 static class NullWebViewClient extends WebViewClient { 145 @Override 146 public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { 147 // TODO: Investigate more and add a test case. 148 // This is a copy of what Clank does. The WebViewCore key handling code and Clank key 149 // handling code differ enough that it's not trivial to figure out how keycodes are 150 // being filtered. 151 int keyCode = event.getKeyCode(); 152 if (keyCode == KeyEvent.KEYCODE_MENU || 153 keyCode == KeyEvent.KEYCODE_HOME || 154 keyCode == KeyEvent.KEYCODE_BACK || 155 keyCode == KeyEvent.KEYCODE_CALL || 156 keyCode == KeyEvent.KEYCODE_ENDCALL || 157 keyCode == KeyEvent.KEYCODE_POWER || 158 keyCode == KeyEvent.KEYCODE_HEADSETHOOK || 159 keyCode == KeyEvent.KEYCODE_CAMERA || 160 keyCode == KeyEvent.KEYCODE_FOCUS || 161 keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || 162 keyCode == KeyEvent.KEYCODE_VOLUME_MUTE || 163 keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 164 return true; 165 } 166 return false; 167 } 168 169 @Override 170 public boolean shouldOverrideUrlLoading(WebView view, String url) { 171 Intent intent; 172 // Perform generic parsing of the URI to turn it into an Intent. 173 try { 174 intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); 175 } catch (URISyntaxException ex) { 176 Log.w(TAG, "Bad URI " + url + ": " + ex.getMessage()); 177 return false; 178 } 179 // Sanitize the Intent, ensuring web pages can not bypass browser 180 // security (only access to BROWSABLE activities). 181 intent.addCategory(Intent.CATEGORY_BROWSABLE); 182 intent.setComponent(null); 183 // Pass the package name as application ID so that the intent from the 184 // same application can be opened in the same tab. 185 intent.putExtra(Browser.EXTRA_APPLICATION_ID, 186 view.getContext().getPackageName()); 187 try { 188 view.getContext().startActivity(intent); 189 } catch (ActivityNotFoundException ex) { 190 Log.w(TAG, "No application can handle " + url); 191 return false; 192 } 193 return true; 194 } 195 } 196 197 void setWebViewClient(WebViewClient client) { 198 if (client != null) { 199 mWebViewClient = client; 200 } else { 201 mWebViewClient = new NullWebViewClient(); 202 } 203 } 204 205 void setWebChromeClient(WebChromeClient client) { 206 if (client != null) { 207 mWebChromeClient = client; 208 } else { 209 // WebViewClassic doesn't implement any special behavior for a null WebChromeClient. 210 mWebChromeClient = new WebChromeClient(); 211 } 212 } 213 214 void setDownloadListener(DownloadListener listener) { 215 mDownloadListener = listener; 216 } 217 218 void setFindListener(WebView.FindListener listener) { 219 mFindListener = listener; 220 } 221 222 void setPictureListener(WebView.PictureListener listener) { 223 mPictureListener = listener; 224 } 225 226 //-------------------------------------------------------------------------------------------- 227 // Adapter for WebContentsDelegate methods. 228 //-------------------------------------------------------------------------------------------- 229 230 /** 231 * @see AwContentsClient#getVisitedHistory 232 */ 233 @Override 234 public void getVisitedHistory(ValueCallback<String[]> callback) { 235 mWebChromeClient.getVisitedHistory(callback); 236 } 237 238 /** 239 * @see AwContentsClient#doUpdateVisiteHistory(String, boolean) 240 */ 241 @Override 242 public void doUpdateVisitedHistory(String url, boolean isReload) { 243 mWebViewClient.doUpdateVisitedHistory(mWebView, url, isReload); 244 } 245 246 /** 247 * @see AwContentsClient#onProgressChanged(int) 248 */ 249 @Override 250 public void onProgressChanged(int progress) { 251 mWebChromeClient.onProgressChanged(mWebView, progress); 252 } 253 254 /** 255 * @see AwContentsClient#shouldInterceptRequest(java.lang.String) 256 */ 257 @Override 258 public InterceptedRequestData shouldInterceptRequest(String url) { 259 WebResourceResponse response = mWebViewClient.shouldInterceptRequest(mWebView, url); 260 if (response == null) return null; 261 return new InterceptedRequestData( 262 response.getMimeType(), 263 response.getEncoding(), 264 response.getData()); 265 } 266 267 /** 268 * @see AwContentsClient#shouldIgnoreNavigation(java.lang.String) 269 */ 270 @Override 271 public boolean shouldIgnoreNavigation(String url) { 272 return mWebViewClient.shouldOverrideUrlLoading(mWebView, url); 273 } 274 275 /** 276 * @see AwContentsClient#onUnhandledKeyEvent(android.view.KeyEvent) 277 */ 278 @Override 279 public void onUnhandledKeyEvent(KeyEvent event) { 280 mWebViewClient.onUnhandledKeyEvent(mWebView, event); 281 } 282 283 /** 284 * @see AwContentsClient#onConsoleMessage(android.webkit.ConsoleMessage) 285 */ 286 @Override 287 public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 288 return mWebChromeClient.onConsoleMessage(consoleMessage); 289 } 290 291 /** 292 * @see AwContentsClient#onFindResultReceived(int,int,boolean) 293 */ 294 @Override 295 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, 296 boolean isDoneCounting) { 297 if (mFindListener == null) return; 298 mFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); 299 } 300 301 /** 302 * @See AwContentsClient#onNewPicture(Picture) 303 */ 304 @Override 305 public void onNewPicture(Picture picture) { 306 if (mPictureListener == null) return; 307 mPictureListener.onNewPicture(mWebView, picture); 308 } 309 310 @Override 311 public void onLoadResource(String url) { 312 mWebViewClient.onLoadResource(mWebView, url); 313 } 314 315 @Override 316 public boolean onCreateWindow(boolean isDialog, boolean isUserGesture) { 317 Message m = mUiThreadHandler.obtainMessage( 318 NEW_WEBVIEW_CREATED, mWebView.new WebViewTransport()); 319 return mWebChromeClient.onCreateWindow(mWebView, isDialog, isUserGesture, m); 320 } 321 322 /** 323 * @see AwContentsClient#onCloseWindow() 324 */ 325 /* @Override */ 326 public void onCloseWindow() { 327 mWebChromeClient.onCloseWindow(mWebView); 328 } 329 330 /** 331 * @see AwContentsClient#onRequestFocus() 332 */ 333 /* @Override */ 334 public void onRequestFocus() { 335 mWebChromeClient.onRequestFocus(mWebView); 336 } 337 338 /** 339 * @see AwContentsClient#onReceivedTouchIconUrl(String url, boolean precomposed) 340 */ 341 @Override 342 public void onReceivedTouchIconUrl(String url, boolean precomposed) { 343 mWebChromeClient.onReceivedTouchIconUrl(mWebView, url, precomposed); 344 } 345 346 /** 347 * @see AwContentsClient#onReceivedIcon(Bitmap bitmap) 348 */ 349 @Override 350 public void onReceivedIcon(Bitmap bitmap) { 351 mWebChromeClient.onReceivedIcon(mWebView, bitmap); 352 } 353 354 //-------------------------------------------------------------------------------------------- 355 // Trivial Chrome -> WebViewClient mappings. 356 //-------------------------------------------------------------------------------------------- 357 358 /** 359 * @see ContentViewClient#onPageStarted(String) 360 */ 361 @Override 362 public void onPageStarted(String url) { 363 //TODO: Can't get the favicon till b/6094807 is fixed. 364 mWebViewClient.onPageStarted(mWebView, url, null); 365 } 366 367 /** 368 * @see ContentViewClient#onPageFinished(String) 369 */ 370 @Override 371 public void onPageFinished(String url) { 372 mWebViewClient.onPageFinished(mWebView, url); 373 374 // See b/8208948 375 // This fakes an onNewPicture callback after onPageFinished to allow 376 // CTS tests to run in an un-flaky manner. This is required as the 377 // path for sending Picture updates in Chromium are decoupled from the 378 // page loading callbacks, i.e. the Chrome compositor may draw our 379 // content and send the Picture before onPageStarted or onPageFinished 380 // are invoked. The CTS harness discards any pictures it receives before 381 // onPageStarted is invoked, so in the case we get the Picture before that and 382 // no further updates after onPageStarted, we'll fail the test by timing 383 // out waiting for a Picture. 384 // To ensure backwards compatibility, we need to defer sending Picture updates 385 // until onPageFinished has been invoked. This work is being done 386 // upstream, and we can revert this hack when it lands. 387 if (mPictureListener != null) { 388 new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { 389 @Override 390 public void run() { 391 UnimplementedWebViewApi.invoke(); 392 if (mPictureListener != null) { 393 mPictureListener.onNewPicture(mWebView, null); 394 } 395 } 396 }, 100); 397 } 398 } 399 400 /** 401 * @see ContentViewClient#onReceivedError(int,String,String) 402 */ 403 @Override 404 public void onReceivedError(int errorCode, String description, String failingUrl) { 405 mWebViewClient.onReceivedError(mWebView, errorCode, description, failingUrl); 406 } 407 408 /** 409 * @see ContentViewClient#onUpdateTitle(String) 410 */ 411 @Override 412 public void onUpdateTitle(String title) { 413 mWebChromeClient.onReceivedTitle(mWebView, title); 414 } 415 416 417 /** 418 * @see ContentViewClient#shouldOverrideKeyEvent(KeyEvent) 419 */ 420 @Override 421 public boolean shouldOverrideKeyEvent(KeyEvent event) { 422 // TODO(joth): The expression here is a workaround for http://b/7697782 :- 423 // 1. The check for system key should be made in AwContents or ContentViewCore, 424 // before shouldOverrideKeyEvent() is called at all. 425 // 2. shouldOverrideKeyEvent() should be called in onKeyDown/onKeyUp, not from 426 // dispatchKeyEvent(). 427 return event.isSystem() || 428 mWebViewClient.shouldOverrideKeyEvent(mWebView, event); 429 } 430 431 432 //-------------------------------------------------------------------------------------------- 433 // More complicated mappings (including behavior choices) 434 //-------------------------------------------------------------------------------------------- 435 436 /** 437 * @see ContentViewClient#onTabCrash() 438 */ 439 @Override 440 public void onTabCrash() { 441 // The WebViewClassic implementation used a single process, so any crash would 442 // cause the application to terminate. WebViewChromium should have the same 443 // behavior as long as we run the renderer in-process. This needs to be revisited 444 // if we change that decision. 445 Log.e(TAG, "Renderer crash reported."); 446 mWebChromeClient.onCloseWindow(mWebView); 447 } 448 449 //-------------------------------------------------------------------------------------------- 450 // The TODO section 451 //-------------------------------------------------------------------------------------------- 452 453 454 /** 455 * @see ContentViewClient#onImeEvent() 456 */ 457 @Override 458 public void onImeEvent() { 459 } 460 461 /** 462 * @see ContentViewClient#onStartContentIntent(Context, String) 463 * Callback when detecting a click on a content link. 464 */ 465 @Override 466 public void onStartContentIntent(Context context, String contentUrl) { 467 mWebViewClient.shouldOverrideUrlLoading(mWebView, contentUrl); 468 } 469 470 private static class SimpleJsResultReceiver implements JsResult.ResultReceiver { 471 private JsResultReceiver mChromeResultReceiver; 472 473 public SimpleJsResultReceiver(JsResultReceiver receiver) { 474 mChromeResultReceiver = receiver; 475 } 476 477 @Override 478 public void onJsResultComplete(JsResult result) { 479 if (result.getResult()) { 480 mChromeResultReceiver.confirm(); 481 } else { 482 mChromeResultReceiver.cancel(); 483 } 484 } 485 } 486 487 private static class JsPromptResultReceiverAdapter implements JsResult.ResultReceiver { 488 private JsPromptResultReceiver mChromeResultReceiver; 489 private JsPromptResult mPromptResult; 490 491 public JsPromptResultReceiverAdapter(JsPromptResultReceiver receiver) { 492 mChromeResultReceiver = receiver; 493 // We hold onto the JsPromptResult here, just to avoid the need to downcast 494 // in onJsResultComplete. 495 mPromptResult = new JsPromptResult(this); 496 } 497 498 public JsPromptResult getPromptResult() { 499 return mPromptResult; 500 } 501 502 @Override 503 public void onJsResultComplete(JsResult result) { 504 if (result != mPromptResult) throw new RuntimeException("incorrect JsResult instance"); 505 if (mPromptResult.getResult()) { 506 mChromeResultReceiver.confirm(mPromptResult.getStringResult()); 507 } else { 508 mChromeResultReceiver.cancel(); 509 } 510 } 511 } 512 513 @Override 514 public void onGeolocationPermissionsShowPrompt(String origin, 515 GeolocationPermissions.Callback callback) { 516 mWebChromeClient.onGeolocationPermissionsShowPrompt(origin, callback); 517 } 518 519 @Override 520 public void onGeolocationPermissionsHidePrompt() { 521 mWebChromeClient.onGeolocationPermissionsHidePrompt(); 522 } 523 524 @Override 525 public void handleJsAlert(String url, String message, JsResultReceiver receiver) { 526 JsResult res = new JsResult(new SimpleJsResultReceiver(receiver)); 527 mWebChromeClient.onJsAlert(mWebView, url, message, res); 528 // TODO: Handle the case of the client returning false; 529 } 530 531 @Override 532 public void handleJsBeforeUnload(String url, String message, JsResultReceiver receiver) { 533 JsResult res = new JsResult(new SimpleJsResultReceiver(receiver)); 534 mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res); 535 // TODO: Handle the case of the client returning false; 536 } 537 538 @Override 539 public void handleJsConfirm(String url, String message, JsResultReceiver receiver) { 540 JsResult res = new JsResult(new SimpleJsResultReceiver(receiver)); 541 mWebChromeClient.onJsConfirm(mWebView, url, message, res); 542 // TODO: Handle the case of the client returning false; 543 } 544 545 @Override 546 public void handleJsPrompt(String url, String message, String defaultValue, 547 JsPromptResultReceiver receiver) { 548 JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult(); 549 mWebChromeClient.onJsPrompt(mWebView, url, message, defaultValue, res); 550 // TODO: Handle the case of the client returning false; 551 } 552 553 @Override 554 public void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) { 555 mWebViewClient.onReceivedHttpAuthRequest(mWebView, 556 new AwHttpAuthHandlerAdapter(handler), host, realm); 557 } 558 559 @Override 560 public void onReceivedSslError(final ValueCallback<Boolean> callback, SslError error) { 561 SslErrorHandler handler = new SslErrorHandler() { 562 @Override 563 public void proceed() { 564 postProceed(true); 565 } 566 @Override 567 public void cancel() { 568 postProceed(false); 569 } 570 private void postProceed(final boolean proceed) { 571 post(new Runnable() { 572 @Override 573 public void run() { 574 callback.onReceiveValue(proceed); 575 } 576 }); 577 } 578 }; 579 mWebViewClient.onReceivedSslError(mWebView, handler, error); 580 } 581 582 @Override 583 public void onReceivedLoginRequest(String realm, String account, String args) { 584 mWebViewClient.onReceivedLoginRequest(mWebView, realm, account, args); 585 } 586 587 @Override 588 public void onFormResubmission(Message dontResend, Message resend) { 589 mWebViewClient.onFormResubmission(mWebView, dontResend, resend); 590 } 591 592 @Override 593 public void onDownloadStart(String url, 594 String userAgent, 595 String contentDisposition, 596 String mimeType, 597 long contentLength) { 598 if (mDownloadListener != null) { 599 mDownloadListener.onDownloadStart(url, 600 userAgent, 601 contentDisposition, 602 mimeType, 603 contentLength); 604 } 605 } 606 607 @Override 608 public void onScaleChangedScaled(float oldScale, float newScale) { 609 mWebViewClient.onScaleChanged(mWebView, oldScale, newScale); 610 } 611 612 @Override 613 public void onShowCustomView(View view, 614 int requestedOrientation, CustomViewCallback cb) { 615 mWebChromeClient.onShowCustomView(view, requestedOrientation, cb); 616 } 617 618 @Override 619 public void onHideCustomView() { 620 mWebChromeClient.onHideCustomView(); 621 } 622 623 @Override 624 protected View getVideoLoadingProgressView() { 625 return mWebChromeClient.getVideoLoadingProgressView(); 626 } 627 628 private static class AwHttpAuthHandlerAdapter extends android.webkit.HttpAuthHandler { 629 private AwHttpAuthHandler mAwHandler; 630 631 public AwHttpAuthHandlerAdapter(AwHttpAuthHandler awHandler) { 632 mAwHandler = awHandler; 633 } 634 635 @Override 636 public void proceed(String username, String password) { 637 if (username == null) { 638 username = ""; 639 } 640 641 if (password == null) { 642 password = ""; 643 } 644 mAwHandler.proceed(username, password); 645 } 646 647 @Override 648 public void cancel() { 649 mAwHandler.cancel(); 650 } 651 652 @Override 653 public boolean useHttpAuthUsernamePassword() { 654 return mAwHandler.isFirstAttempt(); 655 } 656 } 657} 658