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