WebViewContentsClientAdapter.java revision b5d9eb0196b542499722ffcc8ede41c0eae53516
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 // The Context that was passed to the WebView by the external client app. 139 private final Context mContext; 140 141 NullWebViewClient(Context context) { 142 mContext = context; 143 } 144 145 @Override 146 public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { 147 // TODO: Investigate more and add a test case. 148 // This is a copy of what Clank does. The WebViewCore key handling code and Clank key 149 // handling code differ enough that it's not trivial to figure out how keycodes are 150 // being filtered. 151 int keyCode = event.getKeyCode(); 152 if (keyCode == KeyEvent.KEYCODE_MENU || 153 keyCode == KeyEvent.KEYCODE_HOME || 154 keyCode == KeyEvent.KEYCODE_BACK || 155 keyCode == KeyEvent.KEYCODE_CALL || 156 keyCode == KeyEvent.KEYCODE_ENDCALL || 157 keyCode == KeyEvent.KEYCODE_POWER || 158 keyCode == KeyEvent.KEYCODE_HEADSETHOOK || 159 keyCode == KeyEvent.KEYCODE_CAMERA || 160 keyCode == KeyEvent.KEYCODE_FOCUS || 161 keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || 162 keyCode == KeyEvent.KEYCODE_VOLUME_MUTE || 163 keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 164 return true; 165 } 166 return false; 167 } 168 169 @Override 170 public boolean shouldOverrideUrlLoading(WebView view, String url) { 171 Intent intent; 172 // Perform generic parsing of the URI to turn it into an Intent. 173 try { 174 intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); 175 } catch (URISyntaxException ex) { 176 Log.w(TAG, "Bad URI " + url + ": " + ex.getMessage()); 177 return false; 178 } 179 // Sanitize the Intent, ensuring web pages can not bypass browser 180 // security (only access to BROWSABLE activities). 181 intent.addCategory(Intent.CATEGORY_BROWSABLE); 182 intent.setComponent(null); 183 // Pass the package name as application ID so that the intent from the 184 // same application can be opened in the same tab. 185 intent.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName()); 186 try { 187 mContext.startActivity(intent); 188 } catch (ActivityNotFoundException ex) { 189 Log.w(TAG, "No application can handle " + url); 190 return false; 191 } 192 return true; 193 } 194 } 195 196 void setWebViewClient(WebViewClient client) { 197 if (client != null) { 198 mWebViewClient = client; 199 } else { 200 mWebViewClient = new NullWebViewClient(mWebView.getContext()); 201 } 202 } 203 204 void setWebChromeClient(WebChromeClient client) { 205 if (client != null) { 206 mWebChromeClient = client; 207 } else { 208 // WebViewClassic doesn't implement any special behavior for a null WebChromeClient. 209 mWebChromeClient = new WebChromeClient(); 210 } 211 } 212 213 void setDownloadListener(DownloadListener listener) { 214 mDownloadListener = listener; 215 } 216 217 void setFindListener(WebView.FindListener listener) { 218 mFindListener = listener; 219 } 220 221 void setPictureListener(WebView.PictureListener listener) { 222 mPictureListener = listener; 223 } 224 225 //-------------------------------------------------------------------------------------------- 226 // Adapter for WebContentsDelegate methods. 227 //-------------------------------------------------------------------------------------------- 228 229 /** 230 * @see AwContentsClient#onProgressChanged(int) 231 */ 232 @Override 233 public void onProgressChanged(int progress) { 234 mWebChromeClient.onProgressChanged(mWebView, progress); 235 } 236 237 /** 238 * @see AwContentsClient#shouldInterceptRequest(java.lang.String) 239 */ 240 @Override 241 public InterceptedRequestData shouldInterceptRequest(String url) { 242 WebResourceResponse response = mWebViewClient.shouldInterceptRequest(mWebView, url); 243 if (response == null) return null; 244 return new InterceptedRequestData( 245 response.getMimeType(), 246 response.getEncoding(), 247 response.getData()); 248 } 249 250 /** 251 * @see AwContentsClient#shouldIgnoreNavigation(java.lang.String) 252 */ 253 @Override 254 public boolean shouldIgnoreNavigation(String url) { 255 return mWebViewClient.shouldOverrideUrlLoading(mWebView, url); 256 } 257 258 /** 259 * @see AwContentsClient#onUnhandledKeyEvent(android.view.KeyEvent) 260 */ 261 @Override 262 public void onUnhandledKeyEvent(KeyEvent event) { 263 mWebViewClient.onUnhandledKeyEvent(mWebView, event); 264 } 265 266 /** 267 * @see AwContentsClient#onConsoleMessage(android.webkit.ConsoleMessage) 268 */ 269 @Override 270 public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 271 return mWebChromeClient.onConsoleMessage(consoleMessage); 272 } 273 274 /** 275 * @see AwContentsClient#onFindResultReceived(int,int,boolean) 276 */ 277 @Override 278 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, 279 boolean isDoneCounting) { 280 if (mFindListener == null) return; 281 mFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); 282 } 283 284 @Override 285 public void onLoadResource(String url) { 286 mWebViewClient.onLoadResource(mWebView, url); 287 } 288 289 @Override 290 public boolean onCreateWindow(boolean isDialog, boolean isUserGesture) { 291 Message m = mUiThreadHandler.obtainMessage( 292 NEW_WEBVIEW_CREATED, mWebView.new WebViewTransport()); 293 return mWebChromeClient.onCreateWindow(mWebView, isDialog, isUserGesture, m); 294 } 295 296 /** 297 * @see AwContentsClient#onCloseWindow() 298 */ 299 /* @Override */ 300 public void onCloseWindow() { 301 mWebChromeClient.onCloseWindow(mWebView); 302 } 303 304 //-------------------------------------------------------------------------------------------- 305 // Trivial Chrome -> WebViewClient mappings. 306 //-------------------------------------------------------------------------------------------- 307 308 /** 309 * @see ContentViewClient#onPageStarted(String) 310 */ 311 @Override 312 public void onPageStarted(String url) { 313 //TODO: Can't get the favicon till b/6094807 is fixed. 314 mWebViewClient.onPageStarted(mWebView, url, null); 315 } 316 317 /** 318 * @see ContentViewClient#onPageFinished(String) 319 */ 320 @Override 321 public void onPageFinished(String url) { 322 mWebViewClient.onPageFinished(mWebView, url); 323 324 // HACK: Fake a picture listener update, to allow CTS tests to progress. 325 // TODO: Remove when we have real picture listener updates implemented. 326 if (mPictureListener != null) { 327 new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { 328 @Override 329 public void run() { 330 UnimplementedWebViewApi.invoke(); 331 if (mPictureListener != null) { 332 mPictureListener.onNewPicture(mWebView, new Picture()); 333 } 334 } 335 }, 100); 336 } 337 } 338 339 /** 340 * @see ContentViewClient#onReceivedError(int,String,String) 341 */ 342 @Override 343 public void onReceivedError(int errorCode, String description, String failingUrl) { 344 mWebViewClient.onReceivedError(mWebView, errorCode, description, failingUrl); 345 } 346 347 /** 348 * @see ContentViewClient#onUpdateTitle(String) 349 */ 350 @Override 351 public void onUpdateTitle(String title) { 352 mWebChromeClient.onReceivedTitle(mWebView, title); 353 } 354 355 356 /** 357 * @see ContentViewClient#shouldOverrideKeyEvent(KeyEvent) 358 */ 359 @Override 360 public boolean shouldOverrideKeyEvent(KeyEvent event) { 361 // TODO(joth): The expression here is a workaround for http://b/7697782 :- 362 // 1. The check for system key should be made in AwContents or ContentViewCore, 363 // before shouldOverrideKeyEvent() is called at all. 364 // 2. shouldOverrideKeyEvent() should be called in onKeyDown/onKeyUp, not from 365 // dispatchKeyEvent(). 366 return event.isSystem() || 367 mWebViewClient.shouldOverrideKeyEvent(mWebView, event); 368 } 369 370 371 //-------------------------------------------------------------------------------------------- 372 // More complicated mappings (including behavior choices) 373 //-------------------------------------------------------------------------------------------- 374 375 /** 376 * @see ContentViewClient#onTabCrash() 377 */ 378 @Override 379 public void onTabCrash() { 380 // The WebViewClassic implementation used a single process, so any crash would 381 // cause the application to terminate. WebViewChromium should have the same 382 // behavior as long as we run the renderer in-process. This needs to be revisited 383 // if we change that decision. 384 Log.e(TAG, "Renderer crash reported."); 385 mWebChromeClient.onCloseWindow(mWebView); 386 } 387 388 //-------------------------------------------------------------------------------------------- 389 // The TODO section 390 //-------------------------------------------------------------------------------------------- 391 392 393 /** 394 * @see ContentViewClient#onImeEvent() 395 */ 396 @Override 397 public void onImeEvent() { 398 } 399 400 /** 401 * @see ContentViewClient#onEvaluateJavaScriptResult(int,String) 402 */ 403 @Override 404 public void onEvaluateJavaScriptResult(int id, String jsonResult) { 405 } 406 407 /** 408 * @see ContentViewClient#onStartContentIntent(Context, String) 409 */ 410 @Override 411 public void onStartContentIntent(Context context, String contentUrl) { 412 } 413 414 private static class SimpleJsResultReceiver implements JsResult.ResultReceiver { 415 private JsResultReceiver mChromeResultReceiver; 416 417 public SimpleJsResultReceiver(JsResultReceiver receiver) { 418 mChromeResultReceiver = receiver; 419 } 420 421 @Override 422 public void onJsResultComplete(JsResult result) { 423 if (result.getResult()) { 424 mChromeResultReceiver.confirm(); 425 } else { 426 mChromeResultReceiver.cancel(); 427 } 428 } 429 } 430 431 private static class JsPromptResultReceiverAdapter implements JsResult.ResultReceiver { 432 private JsPromptResultReceiver mChromeResultReceiver; 433 private JsPromptResult mPromptResult; 434 435 public JsPromptResultReceiverAdapter(JsPromptResultReceiver receiver) { 436 mChromeResultReceiver = receiver; 437 // We hold onto the JsPromptResult here, just to avoid the need to downcast 438 // in onJsResultComplete. 439 mPromptResult = new JsPromptResult(this); 440 } 441 442 public JsPromptResult getPromptResult() { 443 return mPromptResult; 444 } 445 446 @Override 447 public void onJsResultComplete(JsResult result) { 448 if (result != mPromptResult) throw new RuntimeException("incorrect JsResult instance"); 449 if (mPromptResult.getResult()) { 450 mChromeResultReceiver.confirm(mPromptResult.getStringResult()); 451 } else { 452 mChromeResultReceiver.cancel(); 453 } 454 } 455 } 456 457 @Override 458 public void handleJsAlert(String url, String message, JsResultReceiver receiver) { 459 JsResult res = new JsResult(new SimpleJsResultReceiver(receiver)); 460 mWebChromeClient.onJsAlert(mWebView, url, message, res); 461 // TODO: Handle the case of the client returning false; 462 } 463 464 @Override 465 public void handleJsBeforeUnload(String url, String message, JsResultReceiver receiver) { 466 JsResult res = new JsResult(new SimpleJsResultReceiver(receiver)); 467 mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res); 468 // TODO: Handle the case of the client returning false; 469 } 470 471 @Override 472 public void handleJsConfirm(String url, String message, JsResultReceiver receiver) { 473 JsResult res = new JsResult(new SimpleJsResultReceiver(receiver)); 474 mWebChromeClient.onJsConfirm(mWebView, url, message, res); 475 // TODO: Handle the case of the client returning false; 476 } 477 478 @Override 479 public void handleJsPrompt(String url, String message, String defaultValue, 480 JsPromptResultReceiver receiver) { 481 JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult(); 482 mWebChromeClient.onJsPrompt(mWebView, url, message, defaultValue, res); 483 // TODO: Handle the case of the client returning false; 484 } 485 486 @Override 487 public void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) { 488 mWebViewClient.onReceivedHttpAuthRequest(mWebView, 489 new AwHttpAuthHandlerAdapter(handler), host, realm); 490 } 491 492 @Override 493 public void onFormResubmission(Message dontResend, Message resend) { 494 mWebViewClient.onFormResubmission(mWebView, dontResend, resend); 495 } 496 497 @Override 498 public void onDownloadStart(String url, 499 String userAgent, 500 String contentDisposition, 501 String mimeType, 502 long contentLength) { 503 if (mDownloadListener != null) { 504 mDownloadListener.onDownloadStart(url, 505 userAgent, 506 contentDisposition, 507 mimeType, 508 contentLength); 509 } 510 } 511 512 513 private static class AwHttpAuthHandlerAdapter extends android.webkit.HttpAuthHandler { 514 private AwHttpAuthHandler mAwHandler; 515 516 public AwHttpAuthHandlerAdapter(AwHttpAuthHandler awHandler) { 517 mAwHandler = awHandler; 518 } 519 520 @Override 521 public void proceed(String username, String password) { 522 if (username == null) { 523 username = ""; 524 } 525 526 if (password == null) { 527 password = ""; 528 } 529 mAwHandler.proceed(username, password); 530 } 531 532 @Override 533 public void cancel() { 534 mAwHandler.cancel(); 535 } 536 537 @Override 538 public boolean useHttpAuthUsernamePassword() { 539 // The documentation for this method says: 540 // Gets whether the credentials stored for the current host (i.e. the host 541 // for which {@link WebViewClient#onReceivedHttpAuthRequest} was called) 542 // are suitable for use. Credentials are not suitable if they have 543 // previously been rejected by the server for the current request. 544 // @return whether the credentials are suitable for use 545 // 546 // The CTS tests point out that it always returns true (at odds with 547 // the documentation). 548 // TODO: Decide whether to follow the docs or follow the classic 549 // implementation and update the docs. For now the latter, as it's 550 // easiest. (though not updating docs until this is resolved). 551 // See b/6204427. 552 return true; 553 } 554 } 555} 556