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