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