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