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