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