WebViewContentsClientAdapter.java revision 251a1c8d0a987b0df24e19904d0c0ef492304286
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 /** 78 * Adapter constructor. 79 * 80 * @param webView the {@link WebView} instance that this adapter is serving. 81 */ 82 WebViewContentsClientAdapter(WebView webView) { 83 if (webView == null) { 84 throw new IllegalArgumentException("webView can't be null"); 85 } 86 87 mWebView = webView; 88 setWebViewClient(null); 89 setWebChromeClient(null); 90 } 91 92 // WebViewClassic is coded in such a way that even if a null WebViewClient is set, 93 // certain actions take place. 94 // We choose to replicate this behavior by using a NullWebViewClient implementation (also known 95 // as the Null Object pattern) rather than duplicating the WebViewClassic approach in 96 // ContentView. 97 static class NullWebViewClient extends WebViewClient { 98 // The Context that was passed to the WebView by the external client app. 99 private final Context mContext; 100 101 NullWebViewClient(Context context) { 102 mContext = context; 103 } 104 105 @Override 106 public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { 107 // TODO: Investigate more and add a test case. 108 // This is a copy of what Clank does. The WebViewCore key handling code and Clank key 109 // handling code differ enough that it's not trivial to figure out how keycodes are 110 // being filtered. 111 int keyCode = event.getKeyCode(); 112 if (keyCode == KeyEvent.KEYCODE_MENU || 113 keyCode == KeyEvent.KEYCODE_HOME || 114 keyCode == KeyEvent.KEYCODE_BACK || 115 keyCode == KeyEvent.KEYCODE_CALL || 116 keyCode == KeyEvent.KEYCODE_ENDCALL || 117 keyCode == KeyEvent.KEYCODE_POWER || 118 keyCode == KeyEvent.KEYCODE_HEADSETHOOK || 119 keyCode == KeyEvent.KEYCODE_CAMERA || 120 keyCode == KeyEvent.KEYCODE_FOCUS || 121 keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || 122 keyCode == KeyEvent.KEYCODE_VOLUME_MUTE || 123 keyCode == KeyEvent.KEYCODE_VOLUME_UP) { 124 return true; 125 } 126 return false; 127 } 128 129 @Override 130 public boolean shouldOverrideUrlLoading(WebView view, String url) { 131 Intent intent; 132 // Perform generic parsing of the URI to turn it into an Intent. 133 try { 134 intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME); 135 } catch (URISyntaxException ex) { 136 Log.w(TAG, "Bad URI " + url + ": " + ex.getMessage()); 137 return false; 138 } 139 // Sanitize the Intent, ensuring web pages can not bypass browser 140 // security (only access to BROWSABLE activities). 141 intent.addCategory(Intent.CATEGORY_BROWSABLE); 142 intent.setComponent(null); 143 // Pass the package name as application ID so that the intent from the 144 // same application can be opened in the same tab. 145 intent.putExtra(Browser.EXTRA_APPLICATION_ID, mContext.getPackageName()); 146 try { 147 mContext.startActivity(intent); 148 } catch (ActivityNotFoundException ex) { 149 Log.w(TAG, "No application can handle " + url); 150 return false; 151 } 152 return true; 153 } 154 } 155 156 void setWebViewClient(WebViewClient client) { 157 if (client != null) { 158 mWebViewClient = client; 159 } else { 160 mWebViewClient = new NullWebViewClient(mWebView.getContext()); 161 } 162 } 163 164 void setWebChromeClient(WebChromeClient client) { 165 if (client != null) { 166 mWebChromeClient = client; 167 } else { 168 // WebViewClassic doesn't implement any special behavior for a null WebChromeClient. 169 mWebChromeClient = new WebChromeClient(); 170 } 171 } 172 173 void setFindListener(WebView.FindListener listener) { 174 mFindListener = listener; 175 } 176 177 void setPictureListener(WebView.PictureListener listener) { 178 mPictureListener = listener; 179 } 180 181 //-------------------------------------------------------------------------------------------- 182 // Adapter for WebContentsDelegate methods. 183 //-------------------------------------------------------------------------------------------- 184 185 /** 186 * @see AwContentsClient#onProgressChanged(int) 187 */ 188 @Override 189 public void onProgressChanged(int progress) { 190 mWebChromeClient.onProgressChanged(mWebView, progress); 191 } 192 193 /** 194 * @see AwContentsClient#shouldInterceptRequest(java.lang.String) 195 */ 196 @Override 197 public InterceptedRequestData shouldInterceptRequest(String url) { 198 WebResourceResponse response = mWebViewClient.shouldInterceptRequest(mWebView, url); 199 if (response == null) return null; 200 return new InterceptedRequestData( 201 response.getMimeType(), 202 response.getEncoding(), 203 response.getData()); 204 } 205 206 /** 207 * @see AwContentsClient#shouldIgnoreNavigation(java.lang.String) 208 */ 209 @Override 210 public boolean shouldIgnoreNavigation(String url) { 211 return mWebViewClient.shouldOverrideUrlLoading(mWebView, url); 212 } 213 214 /** 215 * @see AwContentsClient#onUnhandledKeyEvent(android.view.KeyEvent) 216 */ 217 @Override 218 public void onUnhandledKeyEvent(KeyEvent event) { 219 mWebViewClient.onUnhandledKeyEvent(mWebView, event); 220 } 221 222 /** 223 * @see AwContentsClient#onConsoleMessage(android.webkit.ConsoleMessage) 224 */ 225 @Override 226 public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 227 return mWebChromeClient.onConsoleMessage(consoleMessage); 228 } 229 230 /** 231 * @see AwContentsClient#onFindResultReceived(int,int,boolean) 232 */ 233 @Override 234 public void onFindResultReceived(int activeMatchOrdinal, int numberOfMatches, 235 boolean isDoneCounting) { 236 if (mFindListener == null) return; 237 mFindListener.onFindResultReceived(activeMatchOrdinal, numberOfMatches, isDoneCounting); 238 } 239 240 @Override 241 public void onLoadResource(String url) { 242 mWebViewClient.onLoadResource(mWebView, url); 243 } 244 245 //-------------------------------------------------------------------------------------------- 246 // Trivial Chrome -> WebViewClient mappings. 247 //-------------------------------------------------------------------------------------------- 248 249 /** 250 * @see ContentViewClient#onPageStarted(String) 251 */ 252 @Override 253 public void onPageStarted(String url) { 254 //TODO: Can't get the favicon till b/6094807 is fixed. 255 mWebViewClient.onPageStarted(mWebView, url, null); 256 } 257 258 /** 259 * @see ContentViewClient#onPageFinished(String) 260 */ 261 @Override 262 public void onPageFinished(String url) { 263 mWebViewClient.onPageFinished(mWebView, url); 264 265 // HACK: Fake a picture listener update, to allow CTS tests to progress. 266 // TODO: Remove when we have real picture listener updates implemented. 267 if (mPictureListener != null) { 268 new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { 269 @Override 270 public void run() { 271 UnimplementedWebViewApi.invoke(); 272 if (mPictureListener != null) { 273 mPictureListener.onNewPicture(mWebView, new Picture()); 274 } 275 } 276 }, 100); 277 } 278 } 279 280 /** 281 * @see ContentViewClient#onReceivedError(int,String,String) 282 */ 283 @Override 284 public void onReceivedError(int errorCode, String description, String failingUrl) { 285 mWebViewClient.onReceivedError(mWebView, errorCode, description, failingUrl); 286 } 287 288 /** 289 * @see ContentViewClient#onUpdateTitle(String) 290 */ 291 @Override 292 public void onUpdateTitle(String title) { 293 mWebChromeClient.onReceivedTitle(mWebView, title); 294 } 295 296 297 /** 298 * @see ContentViewClient#shouldOverrideKeyEvent(KeyEvent) 299 */ 300 @Override 301 public boolean shouldOverrideKeyEvent(KeyEvent event) { 302 return mWebViewClient.shouldOverrideKeyEvent(mWebView, event); 303 } 304 305 306 //-------------------------------------------------------------------------------------------- 307 // More complicated mappings (including behavior choices) 308 //-------------------------------------------------------------------------------------------- 309 310 /** 311 * @see ContentViewClient#onTabCrash() 312 */ 313 @Override 314 public void onTabCrash() { 315 // The WebViewClassic implementation used a single process, so any crash would 316 // cause the application to terminate. WebViewChromium should have the same 317 // behavior as long as we run the renderer in-process. This needs to be revisited 318 // if we change that decision. 319 Log.e(TAG, "Renderer crash reported."); 320 mWebChromeClient.onCloseWindow(mWebView); 321 } 322 323 //-------------------------------------------------------------------------------------------- 324 // The TODO section 325 //-------------------------------------------------------------------------------------------- 326 327 328 /** 329 * @see ContentViewClient#onImeEvent() 330 */ 331 @Override 332 public void onImeEvent() { 333 } 334 335 /** 336 * @see ContentViewClient#onEvaluateJavaScriptResult(int,String) 337 */ 338 @Override 339 public void onEvaluateJavaScriptResult(int id, String jsonResult) { 340 } 341 342 /** 343 * @see ContentViewClient#onStartContentIntent(Context, String) 344 */ 345 @Override 346 public void onStartContentIntent(Context context, String contentUrl) { 347 } 348 349 private static class SimpleJsResultReceiver implements JsResult.ResultReceiver { 350 private JsResultReceiver mChromeResultReceiver; 351 352 public SimpleJsResultReceiver(JsResultReceiver receiver) { 353 mChromeResultReceiver = receiver; 354 } 355 356 @Override 357 public void onJsResultComplete(JsResult result) { 358 if (result.getResult()) { 359 mChromeResultReceiver.confirm(); 360 } else { 361 mChromeResultReceiver.cancel(); 362 } 363 } 364 } 365 366 private static class JsPromptResultReceiverAdapter implements JsResult.ResultReceiver { 367 private JsPromptResultReceiver mChromeResultReceiver; 368 private JsPromptResult mPromptResult; 369 370 public JsPromptResultReceiverAdapter(JsPromptResultReceiver receiver) { 371 mChromeResultReceiver = receiver; 372 // We hold onto the JsPromptResult here, just to avoid the need to downcast 373 // in onJsResultComplete. 374 mPromptResult = new JsPromptResult(this); 375 } 376 377 public JsPromptResult getPromptResult() { 378 return mPromptResult; 379 } 380 381 @Override 382 public void onJsResultComplete(JsResult result) { 383 if (result != mPromptResult) throw new RuntimeException("incorrect JsResult instance"); 384 if (mPromptResult.getResult()) { 385 mChromeResultReceiver.confirm(mPromptResult.getStringResult()); 386 } else { 387 mChromeResultReceiver.cancel(); 388 } 389 } 390 } 391 392 @Override 393 public void handleJsAlert(String url, String message, JsResultReceiver receiver) { 394 JsResult res = new JsResult(new SimpleJsResultReceiver(receiver)); 395 mWebChromeClient.onJsAlert(mWebView, url, message, res); 396 // TODO: Handle the case of the client returning false; 397 } 398 399 @Override 400 public void handleJsBeforeUnload(String url, String message, JsResultReceiver receiver) { 401 JsResult res = new JsResult(new SimpleJsResultReceiver(receiver)); 402 mWebChromeClient.onJsBeforeUnload(mWebView, url, message, res); 403 // TODO: Handle the case of the client returning false; 404 } 405 406 @Override 407 public void handleJsConfirm(String url, String message, JsResultReceiver receiver) { 408 JsResult res = new JsResult(new SimpleJsResultReceiver(receiver)); 409 mWebChromeClient.onJsConfirm(mWebView, url, message, res); 410 // TODO: Handle the case of the client returning false; 411 } 412 413 @Override 414 public void handleJsPrompt(String url, String message, String defaultValue, 415 JsPromptResultReceiver receiver) { 416 JsPromptResult res = new JsPromptResultReceiverAdapter(receiver).getPromptResult(); 417 mWebChromeClient.onJsPrompt(mWebView, url, message, defaultValue, res); 418 // TODO: Handle the case of the client returning false; 419 } 420 421 @Override 422 public void onReceivedHttpAuthRequest(AwHttpAuthHandler handler, String host, String realm) { 423 mWebViewClient.onReceivedHttpAuthRequest(mWebView, 424 new AwHttpAuthHandlerAdapter(handler), host, realm); 425 } 426 427 @Override 428 public void onFormResubmission(Message dontResend, Message resend) { 429 mWebViewClient.onFormResubmission(mWebView, dontResend, resend); 430 } 431 432 private static class AwHttpAuthHandlerAdapter extends android.webkit.HttpAuthHandler { 433 private AwHttpAuthHandler mAwHandler; 434 435 public AwHttpAuthHandlerAdapter(AwHttpAuthHandler awHandler) { 436 mAwHandler = awHandler; 437 } 438 439 @Override 440 public void proceed(String username, String password) { 441 if (username == null) { 442 username = ""; 443 } 444 445 if (password == null) { 446 password = ""; 447 } 448 mAwHandler.proceed(username, password); 449 } 450 451 @Override 452 public void cancel() { 453 mAwHandler.cancel(); 454 } 455 456 @Override 457 public boolean useHttpAuthUsernamePassword() { 458 // The documentation for this method says: 459 // Gets whether the credentials stored for the current host (i.e. the host 460 // for which {@link WebViewClient#onReceivedHttpAuthRequest} was called) 461 // are suitable for use. Credentials are not suitable if they have 462 // previously been rejected by the server for the current request. 463 // @return whether the credentials are suitable for use 464 // 465 // The CTS tests point out that it always returns true (at odds with 466 // the documentation). 467 // TODO: Decide whether to follow the docs or follow the classic 468 // implementation and update the docs. For now the latter, as it's 469 // easiest. (though not updating docs until this is resolved). 470 // See b/6204427. 471 return true; 472 } 473 } 474} 475