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