1 /* 2 * Copyright (C) 2009 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 17 package com.android.browser; 18 19 import android.app.Activity; 20 import android.app.AlertDialog; 21 import android.content.ContentResolver; 22 import android.content.ContentValues; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.content.DialogInterface.OnCancelListener; 26 import android.graphics.Bitmap; 27 import android.graphics.Bitmap.CompressFormat; 28 import android.graphics.BitmapFactory; 29 import android.graphics.Canvas; 30 import android.graphics.Color; 31 import android.graphics.Paint; 32 import android.graphics.Picture; 33 import android.graphics.PorterDuff; 34 import android.graphics.PorterDuffXfermode; 35 import android.net.Uri; 36 import android.net.http.SslError; 37 import android.os.Bundle; 38 import android.os.Handler; 39 import android.os.Message; 40 import android.os.SystemClock; 41 import android.security.KeyChain; 42 import android.security.KeyChainAliasCallback; 43 import android.text.TextUtils; 44 import android.util.Log; 45 import android.view.KeyEvent; 46 import android.view.LayoutInflater; 47 import android.view.View; 48 import android.view.ViewStub; 49 import android.webkit.ClientCertRequestHandler; 50 import android.webkit.ConsoleMessage; 51 import android.webkit.DownloadListener; 52 import android.webkit.GeolocationPermissions; 53 import android.webkit.HttpAuthHandler; 54 import android.webkit.SslErrorHandler; 55 import android.webkit.URLUtil; 56 import android.webkit.ValueCallback; 57 import android.webkit.WebBackForwardList; 58 import android.webkit.WebBackForwardListClient; 59 import android.webkit.WebChromeClient; 60 import android.webkit.WebHistoryItem; 61 import android.webkit.WebResourceResponse; 62 import android.webkit.WebStorage; 63 import android.webkit.WebView; 64 import android.webkit.WebView.PictureListener; 65 import android.webkit.WebViewClassic; 66 import android.webkit.WebViewClient; 67 import android.widget.CheckBox; 68 import android.widget.Toast; 69 70 import com.android.browser.TabControl.OnThumbnailUpdatedListener; 71 import com.android.browser.homepages.HomeProvider; 72 import com.android.browser.provider.SnapshotProvider.Snapshots; 73 74 import java.io.ByteArrayOutputStream; 75 import java.io.File; 76 import java.io.IOException; 77 import java.io.OutputStream; 78 import java.nio.ByteBuffer; 79 import java.util.LinkedList; 80 import java.util.Map; 81 import java.util.UUID; 82 import java.util.Vector; 83 import java.util.regex.Pattern; 84 import java.util.zip.GZIPOutputStream; 85 86 /** 87 * Class for maintaining Tabs with a main WebView and a subwindow. 88 */ 89 class Tab implements PictureListener { 90 91 // Log Tag 92 private static final String LOGTAG = "Tab"; 93 private static final boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED; 94 // Special case the logtag for messages for the Console to make it easier to 95 // filter them and match the logtag used for these messages in older versions 96 // of the browser. 97 private static final String CONSOLE_LOGTAG = "browser"; 98 99 private static final int MSG_CAPTURE = 42; 100 private static final int CAPTURE_DELAY = 100; 101 private static final int INITIAL_PROGRESS = 5; 102 103 private static Bitmap sDefaultFavicon; 104 105 private static Paint sAlphaPaint = new Paint(); 106 static { sAlphaPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR))107 sAlphaPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 108 sAlphaPaint.setColor(Color.TRANSPARENT); 109 } 110 111 public enum SecurityState { 112 // The page's main resource does not use SSL. Note that we use this 113 // state irrespective of the SSL authentication state of sub-resources. 114 SECURITY_STATE_NOT_SECURE, 115 // The page's main resource uses SSL and the certificate is good. The 116 // same is true of all sub-resources. 117 SECURITY_STATE_SECURE, 118 // The page's main resource uses SSL and the certificate is good, but 119 // some sub-resources either do not use SSL or have problems with their 120 // certificates. 121 SECURITY_STATE_MIXED, 122 // The page's main resource uses SSL but there is a problem with its 123 // certificate. 124 SECURITY_STATE_BAD_CERTIFICATE, 125 } 126 127 Context mContext; 128 protected WebViewController mWebViewController; 129 130 // The tab ID 131 private long mId = -1; 132 133 // The Geolocation permissions prompt 134 private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt; 135 // Main WebView wrapper 136 private View mContainer; 137 // Main WebView 138 private WebView mMainView; 139 // Subwindow container 140 private View mSubViewContainer; 141 // Subwindow WebView 142 private WebView mSubView; 143 // Saved bundle for when we are running low on memory. It contains the 144 // information needed to restore the WebView if the user goes back to the 145 // tab. 146 private Bundle mSavedState; 147 // Parent Tab. This is the Tab that created this Tab, or null if the Tab was 148 // created by the UI 149 private Tab mParent; 150 // Tab that constructed by this Tab. This is used when this Tab is 151 // destroyed, it clears all mParentTab values in the children. 152 private Vector<Tab> mChildren; 153 // If true, the tab is in the foreground of the current activity. 154 private boolean mInForeground; 155 // If true, the tab is in page loading state (after onPageStarted, 156 // before onPageFinsihed) 157 private boolean mInPageLoad; 158 // The last reported progress of the current page 159 private int mPageLoadProgress; 160 // The time the load started, used to find load page time 161 private long mLoadStartTime; 162 // Application identifier used to find tabs that another application wants 163 // to reuse. 164 private String mAppId; 165 // flag to indicate if tab should be closed on back 166 private boolean mCloseOnBack; 167 // Keep the original url around to avoid killing the old WebView if the url 168 // has not changed. 169 // Error console for the tab 170 private ErrorConsoleView mErrorConsole; 171 // The listener that gets invoked when a download is started from the 172 // mMainView 173 private final DownloadListener mDownloadListener; 174 // Listener used to know when we move forward or back in the history list. 175 private final WebBackForwardListClient mWebBackForwardListClient; 176 private DataController mDataController; 177 // State of the auto-login request. 178 private DeviceAccountLogin mDeviceAccountLogin; 179 180 // AsyncTask for downloading touch icons 181 DownloadTouchIcon mTouchIconLoader; 182 183 private BrowserSettings mSettings; 184 private int mCaptureWidth; 185 private int mCaptureHeight; 186 private Bitmap mCapture; 187 private Handler mHandler; 188 private boolean mUpdateThumbnail; 189 190 /** 191 * See {@link #clearBackStackWhenItemAdded(String)}. 192 */ 193 private Pattern mClearHistoryUrlPattern; 194 getDefaultFavicon(Context context)195 private static synchronized Bitmap getDefaultFavicon(Context context) { 196 if (sDefaultFavicon == null) { 197 sDefaultFavicon = BitmapFactory.decodeResource( 198 context.getResources(), R.drawable.app_web_browser_sm); 199 } 200 return sDefaultFavicon; 201 } 202 203 // All the state needed for a page 204 protected static class PageState { 205 String mUrl; 206 String mOriginalUrl; 207 String mTitle; 208 SecurityState mSecurityState; 209 // This is non-null only when mSecurityState is SECURITY_STATE_BAD_CERTIFICATE. 210 SslError mSslCertificateError; 211 Bitmap mFavicon; 212 boolean mIsBookmarkedSite; 213 boolean mIncognito; 214 PageState(Context c, boolean incognito)215 PageState(Context c, boolean incognito) { 216 mIncognito = incognito; 217 if (mIncognito) { 218 mOriginalUrl = mUrl = "browser:incognito"; 219 mTitle = c.getString(R.string.new_incognito_tab); 220 } else { 221 mOriginalUrl = mUrl = ""; 222 mTitle = c.getString(R.string.new_tab); 223 } 224 mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE; 225 } 226 PageState(Context c, boolean incognito, String url, Bitmap favicon)227 PageState(Context c, boolean incognito, String url, Bitmap favicon) { 228 mIncognito = incognito; 229 mOriginalUrl = mUrl = url; 230 if (URLUtil.isHttpsUrl(url)) { 231 mSecurityState = SecurityState.SECURITY_STATE_SECURE; 232 } else { 233 mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE; 234 } 235 mFavicon = favicon; 236 } 237 238 } 239 240 // The current/loading page's state 241 protected PageState mCurrentState; 242 243 // Used for saving and restoring each Tab 244 static final String ID = "ID"; 245 static final String CURRURL = "currentUrl"; 246 static final String CURRTITLE = "currentTitle"; 247 static final String PARENTTAB = "parentTab"; 248 static final String APPID = "appid"; 249 static final String INCOGNITO = "privateBrowsingEnabled"; 250 static final String USERAGENT = "useragent"; 251 static final String CLOSEFLAG = "closeOnBack"; 252 253 // Container class for the next error dialog that needs to be displayed 254 private class ErrorDialog { 255 public final int mTitle; 256 public final String mDescription; 257 public final int mError; ErrorDialog(int title, String desc, int error)258 ErrorDialog(int title, String desc, int error) { 259 mTitle = title; 260 mDescription = desc; 261 mError = error; 262 } 263 } 264 processNextError()265 private void processNextError() { 266 if (mQueuedErrors == null) { 267 return; 268 } 269 // The first one is currently displayed so just remove it. 270 mQueuedErrors.removeFirst(); 271 if (mQueuedErrors.size() == 0) { 272 mQueuedErrors = null; 273 return; 274 } 275 showError(mQueuedErrors.getFirst()); 276 } 277 278 private DialogInterface.OnDismissListener mDialogListener = 279 new DialogInterface.OnDismissListener() { 280 public void onDismiss(DialogInterface d) { 281 processNextError(); 282 } 283 }; 284 private LinkedList<ErrorDialog> mQueuedErrors; 285 queueError(int err, String desc)286 private void queueError(int err, String desc) { 287 if (mQueuedErrors == null) { 288 mQueuedErrors = new LinkedList<ErrorDialog>(); 289 } 290 for (ErrorDialog d : mQueuedErrors) { 291 if (d.mError == err) { 292 // Already saw a similar error, ignore the new one. 293 return; 294 } 295 } 296 ErrorDialog errDialog = new ErrorDialog( 297 err == WebViewClient.ERROR_FILE_NOT_FOUND ? 298 R.string.browserFrameFileErrorLabel : 299 R.string.browserFrameNetworkErrorLabel, 300 desc, err); 301 mQueuedErrors.addLast(errDialog); 302 303 // Show the dialog now if the queue was empty and it is in foreground 304 if (mQueuedErrors.size() == 1 && mInForeground) { 305 showError(errDialog); 306 } 307 } 308 showError(ErrorDialog errDialog)309 private void showError(ErrorDialog errDialog) { 310 if (mInForeground) { 311 AlertDialog d = new AlertDialog.Builder(mContext) 312 .setTitle(errDialog.mTitle) 313 .setMessage(errDialog.mDescription) 314 .setPositiveButton(R.string.ok, null) 315 .create(); 316 d.setOnDismissListener(mDialogListener); 317 d.show(); 318 } 319 } 320 321 // ------------------------------------------------------------------------- 322 // WebViewClient implementation for the main WebView 323 // ------------------------------------------------------------------------- 324 325 private final WebViewClient mWebViewClient = new WebViewClient() { 326 private Message mDontResend; 327 private Message mResend; 328 329 private boolean providersDiffer(String url, String otherUrl) { 330 Uri uri1 = Uri.parse(url); 331 Uri uri2 = Uri.parse(otherUrl); 332 return !uri1.getEncodedAuthority().equals(uri2.getEncodedAuthority()); 333 } 334 335 @Override 336 public void onPageStarted(WebView view, String url, Bitmap favicon) { 337 mInPageLoad = true; 338 mUpdateThumbnail = true; 339 mPageLoadProgress = INITIAL_PROGRESS; 340 mCurrentState = new PageState(mContext, 341 view.isPrivateBrowsingEnabled(), url, favicon); 342 mLoadStartTime = SystemClock.uptimeMillis(); 343 344 // If we start a touch icon load and then load a new page, we don't 345 // want to cancel the current touch icon loader. But, we do want to 346 // create a new one when the touch icon url is known. 347 if (mTouchIconLoader != null) { 348 mTouchIconLoader.mTab = null; 349 mTouchIconLoader = null; 350 } 351 352 // reset the error console 353 if (mErrorConsole != null) { 354 mErrorConsole.clearErrorMessages(); 355 if (mWebViewController.shouldShowErrorConsole()) { 356 mErrorConsole.showConsole(ErrorConsoleView.SHOW_NONE); 357 } 358 } 359 360 // Cancel the auto-login process. 361 if (mDeviceAccountLogin != null) { 362 mDeviceAccountLogin.cancel(); 363 mDeviceAccountLogin = null; 364 mWebViewController.hideAutoLogin(Tab.this); 365 } 366 367 // finally update the UI in the activity if it is in the foreground 368 mWebViewController.onPageStarted(Tab.this, view, favicon); 369 370 updateBookmarkedStatus(); 371 } 372 373 @Override 374 public void onPageFinished(WebView view, String url) { 375 if (!isPrivateBrowsingEnabled()) { 376 LogTag.logPageFinishedLoading( 377 url, SystemClock.uptimeMillis() - mLoadStartTime); 378 } 379 syncCurrentState(view, url); 380 mWebViewController.onPageFinished(Tab.this); 381 } 382 383 // return true if want to hijack the url to let another app to handle it 384 @Override 385 public boolean shouldOverrideUrlLoading(WebView view, String url) { 386 if (mInForeground) { 387 return mWebViewController.shouldOverrideUrlLoading(Tab.this, 388 view, url); 389 } else { 390 return false; 391 } 392 } 393 394 /** 395 * Updates the security state. This method is called when we discover 396 * another resource to be loaded for this page (for example, 397 * javascript). While we update the security state, we do not update 398 * the lock icon until we are done loading, as it is slightly more 399 * secure this way. 400 */ 401 @Override 402 public void onLoadResource(WebView view, String url) { 403 if (url != null && url.length() > 0) { 404 // It is only if the page claims to be secure that we may have 405 // to update the security state: 406 if (mCurrentState.mSecurityState == SecurityState.SECURITY_STATE_SECURE) { 407 // If NOT a 'safe' url, change the state to mixed content! 408 if (!(URLUtil.isHttpsUrl(url) || URLUtil.isDataUrl(url) 409 || URLUtil.isAboutUrl(url))) { 410 mCurrentState.mSecurityState = SecurityState.SECURITY_STATE_MIXED; 411 } 412 } 413 } 414 } 415 416 /** 417 * Show a dialog informing the user of the network error reported by 418 * WebCore if it is in the foreground. 419 */ 420 @Override 421 public void onReceivedError(WebView view, int errorCode, 422 String description, String failingUrl) { 423 if (errorCode != WebViewClient.ERROR_HOST_LOOKUP && 424 errorCode != WebViewClient.ERROR_CONNECT && 425 errorCode != WebViewClient.ERROR_BAD_URL && 426 errorCode != WebViewClient.ERROR_UNSUPPORTED_SCHEME && 427 errorCode != WebViewClient.ERROR_FILE) { 428 queueError(errorCode, description); 429 430 // Don't log URLs when in private browsing mode 431 if (!isPrivateBrowsingEnabled()) { 432 Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl 433 + " " + description); 434 } 435 } 436 } 437 438 /** 439 * Check with the user if it is ok to resend POST data as the page they 440 * are trying to navigate to is the result of a POST. 441 */ 442 @Override 443 public void onFormResubmission(WebView view, final Message dontResend, 444 final Message resend) { 445 if (!mInForeground) { 446 dontResend.sendToTarget(); 447 return; 448 } 449 if (mDontResend != null) { 450 Log.w(LOGTAG, "onFormResubmission should not be called again " 451 + "while dialog is still up"); 452 dontResend.sendToTarget(); 453 return; 454 } 455 mDontResend = dontResend; 456 mResend = resend; 457 new AlertDialog.Builder(mContext).setTitle( 458 R.string.browserFrameFormResubmitLabel).setMessage( 459 R.string.browserFrameFormResubmitMessage) 460 .setPositiveButton(R.string.ok, 461 new DialogInterface.OnClickListener() { 462 public void onClick(DialogInterface dialog, 463 int which) { 464 if (mResend != null) { 465 mResend.sendToTarget(); 466 mResend = null; 467 mDontResend = null; 468 } 469 } 470 }).setNegativeButton(R.string.cancel, 471 new DialogInterface.OnClickListener() { 472 public void onClick(DialogInterface dialog, 473 int which) { 474 if (mDontResend != null) { 475 mDontResend.sendToTarget(); 476 mResend = null; 477 mDontResend = null; 478 } 479 } 480 }).setOnCancelListener(new OnCancelListener() { 481 public void onCancel(DialogInterface dialog) { 482 if (mDontResend != null) { 483 mDontResend.sendToTarget(); 484 mResend = null; 485 mDontResend = null; 486 } 487 } 488 }).show(); 489 } 490 491 /** 492 * Insert the url into the visited history database. 493 * @param url The url to be inserted. 494 * @param isReload True if this url is being reloaded. 495 * FIXME: Not sure what to do when reloading the page. 496 */ 497 @Override 498 public void doUpdateVisitedHistory(WebView view, String url, 499 boolean isReload) { 500 mWebViewController.doUpdateVisitedHistory(Tab.this, isReload); 501 } 502 503 /** 504 * Displays SSL error(s) dialog to the user. 505 */ 506 @Override 507 public void onReceivedSslError(final WebView view, 508 final SslErrorHandler handler, final SslError error) { 509 if (!mInForeground) { 510 handler.cancel(); 511 setSecurityState(SecurityState.SECURITY_STATE_NOT_SECURE); 512 return; 513 } 514 if (mSettings.showSecurityWarnings()) { 515 new AlertDialog.Builder(mContext) 516 .setTitle(R.string.security_warning) 517 .setMessage(R.string.ssl_warnings_header) 518 .setIcon(android.R.drawable.ic_dialog_alert) 519 .setPositiveButton(R.string.ssl_continue, 520 new DialogInterface.OnClickListener() { 521 @Override 522 public void onClick(DialogInterface dialog, 523 int whichButton) { 524 handler.proceed(); 525 handleProceededAfterSslError(error); 526 } 527 }) 528 .setNeutralButton(R.string.view_certificate, 529 new DialogInterface.OnClickListener() { 530 @Override 531 public void onClick(DialogInterface dialog, 532 int whichButton) { 533 mWebViewController.showSslCertificateOnError( 534 view, handler, error); 535 } 536 }) 537 .setNegativeButton(R.string.ssl_go_back, 538 new DialogInterface.OnClickListener() { 539 @Override 540 public void onClick(DialogInterface dialog, 541 int whichButton) { 542 dialog.cancel(); 543 } 544 }) 545 .setOnCancelListener( 546 new DialogInterface.OnCancelListener() { 547 @Override 548 public void onCancel(DialogInterface dialog) { 549 handler.cancel(); 550 setSecurityState(SecurityState.SECURITY_STATE_NOT_SECURE); 551 mWebViewController.onUserCanceledSsl(Tab.this); 552 } 553 }) 554 .show(); 555 } else { 556 handler.proceed(); 557 } 558 } 559 560 /** 561 * Called when an SSL error occurred while loading a resource, but the 562 * WebView but chose to proceed anyway based on a decision retained 563 * from a previous response to onReceivedSslError(). We update our 564 * security state to reflect this. 565 */ 566 @Override 567 public void onProceededAfterSslError(WebView view, SslError error) { 568 handleProceededAfterSslError(error); 569 } 570 571 /** 572 * Displays client certificate request to the user. 573 */ 574 @Override 575 public void onReceivedClientCertRequest(final WebView view, 576 final ClientCertRequestHandler handler, final String host_and_port) { 577 if (!mInForeground) { 578 handler.ignore(); 579 return; 580 } 581 int colon = host_and_port.lastIndexOf(':'); 582 String host; 583 int port; 584 if (colon == -1) { 585 host = host_and_port; 586 port = -1; 587 } else { 588 String portString = host_and_port.substring(colon + 1); 589 try { 590 port = Integer.parseInt(portString); 591 host = host_and_port.substring(0, colon); 592 } catch (NumberFormatException e) { 593 host = host_and_port; 594 port = -1; 595 } 596 } 597 KeyChain.choosePrivateKeyAlias( 598 mWebViewController.getActivity(), new KeyChainAliasCallback() { 599 @Override public void alias(String alias) { 600 if (alias == null) { 601 handler.cancel(); 602 return; 603 } 604 new KeyChainLookup(mContext, handler, alias).execute(); 605 } 606 }, null, null, host, port, null); 607 } 608 609 /** 610 * Handles an HTTP authentication request. 611 * 612 * @param handler The authentication handler 613 * @param host The host 614 * @param realm The realm 615 */ 616 @Override 617 public void onReceivedHttpAuthRequest(WebView view, 618 final HttpAuthHandler handler, final String host, 619 final String realm) { 620 mWebViewController.onReceivedHttpAuthRequest(Tab.this, view, handler, host, realm); 621 } 622 623 @Override 624 public WebResourceResponse shouldInterceptRequest(WebView view, 625 String url) { 626 WebResourceResponse res = HomeProvider.shouldInterceptRequest( 627 mContext, url); 628 return res; 629 } 630 631 @Override 632 public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { 633 if (!mInForeground) { 634 return false; 635 } 636 return mWebViewController.shouldOverrideKeyEvent(event); 637 } 638 639 @Override 640 public void onUnhandledKeyEvent(WebView view, KeyEvent event) { 641 if (!mInForeground) { 642 return; 643 } 644 if (!mWebViewController.onUnhandledKeyEvent(event)) { 645 super.onUnhandledKeyEvent(view, event); 646 } 647 } 648 649 @Override 650 public void onReceivedLoginRequest(WebView view, String realm, 651 String account, String args) { 652 new DeviceAccountLogin(mWebViewController.getActivity(), view, Tab.this, mWebViewController) 653 .handleLogin(realm, account, args); 654 } 655 656 }; 657 syncCurrentState(WebView view, String url)658 private void syncCurrentState(WebView view, String url) { 659 // Sync state (in case of stop/timeout) 660 mCurrentState.mUrl = view.getUrl(); 661 if (mCurrentState.mUrl == null) { 662 mCurrentState.mUrl = ""; 663 } 664 mCurrentState.mOriginalUrl = view.getOriginalUrl(); 665 mCurrentState.mTitle = view.getTitle(); 666 mCurrentState.mFavicon = view.getFavicon(); 667 if (!URLUtil.isHttpsUrl(mCurrentState.mUrl)) { 668 // In case we stop when loading an HTTPS page from an HTTP page 669 // but before a provisional load occurred 670 mCurrentState.mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE; 671 mCurrentState.mSslCertificateError = null; 672 } 673 mCurrentState.mIncognito = view.isPrivateBrowsingEnabled(); 674 } 675 676 // Called by DeviceAccountLogin when the Tab needs to have the auto-login UI 677 // displayed. setDeviceAccountLogin(DeviceAccountLogin login)678 void setDeviceAccountLogin(DeviceAccountLogin login) { 679 mDeviceAccountLogin = login; 680 } 681 682 // Returns non-null if the title bar should display the auto-login UI. getDeviceAccountLogin()683 DeviceAccountLogin getDeviceAccountLogin() { 684 return mDeviceAccountLogin; 685 } 686 687 // ------------------------------------------------------------------------- 688 // WebChromeClient implementation for the main WebView 689 // ------------------------------------------------------------------------- 690 691 private final WebChromeClient mWebChromeClient = new WebChromeClient() { 692 // Helper method to create a new tab or sub window. 693 private void createWindow(final boolean dialog, final Message msg) { 694 WebView.WebViewTransport transport = 695 (WebView.WebViewTransport) msg.obj; 696 if (dialog) { 697 createSubWindow(); 698 mWebViewController.attachSubWindow(Tab.this); 699 transport.setWebView(mSubView); 700 } else { 701 final Tab newTab = mWebViewController.openTab(null, 702 Tab.this, true, true); 703 transport.setWebView(newTab.getWebView()); 704 } 705 msg.sendToTarget(); 706 } 707 708 @Override 709 public boolean onCreateWindow(WebView view, final boolean dialog, 710 final boolean userGesture, final Message resultMsg) { 711 // only allow new window or sub window for the foreground case 712 if (!mInForeground) { 713 return false; 714 } 715 // Short-circuit if we can't create any more tabs or sub windows. 716 if (dialog && mSubView != null) { 717 new AlertDialog.Builder(mContext) 718 .setTitle(R.string.too_many_subwindows_dialog_title) 719 .setIcon(android.R.drawable.ic_dialog_alert) 720 .setMessage(R.string.too_many_subwindows_dialog_message) 721 .setPositiveButton(R.string.ok, null) 722 .show(); 723 return false; 724 } else if (!mWebViewController.getTabControl().canCreateNewTab()) { 725 new AlertDialog.Builder(mContext) 726 .setTitle(R.string.too_many_windows_dialog_title) 727 .setIcon(android.R.drawable.ic_dialog_alert) 728 .setMessage(R.string.too_many_windows_dialog_message) 729 .setPositiveButton(R.string.ok, null) 730 .show(); 731 return false; 732 } 733 734 // Short-circuit if this was a user gesture. 735 if (userGesture) { 736 createWindow(dialog, resultMsg); 737 return true; 738 } 739 740 // Allow the popup and create the appropriate window. 741 final AlertDialog.OnClickListener allowListener = 742 new AlertDialog.OnClickListener() { 743 public void onClick(DialogInterface d, 744 int which) { 745 createWindow(dialog, resultMsg); 746 } 747 }; 748 749 // Block the popup by returning a null WebView. 750 final AlertDialog.OnClickListener blockListener = 751 new AlertDialog.OnClickListener() { 752 public void onClick(DialogInterface d, int which) { 753 resultMsg.sendToTarget(); 754 } 755 }; 756 757 // Build a confirmation dialog to display to the user. 758 final AlertDialog d = 759 new AlertDialog.Builder(mContext) 760 .setIcon(android.R.drawable.ic_dialog_alert) 761 .setMessage(R.string.popup_window_attempt) 762 .setPositiveButton(R.string.allow, allowListener) 763 .setNegativeButton(R.string.block, blockListener) 764 .setCancelable(false) 765 .create(); 766 767 // Show the confirmation dialog. 768 d.show(); 769 return true; 770 } 771 772 @Override 773 public void onRequestFocus(WebView view) { 774 if (!mInForeground) { 775 mWebViewController.switchToTab(Tab.this); 776 } 777 } 778 779 @Override 780 public void onCloseWindow(WebView window) { 781 if (mParent != null) { 782 // JavaScript can only close popup window. 783 if (mInForeground) { 784 mWebViewController.switchToTab(mParent); 785 } 786 mWebViewController.closeTab(Tab.this); 787 } 788 } 789 790 @Override 791 public void onProgressChanged(WebView view, int newProgress) { 792 mPageLoadProgress = newProgress; 793 if (newProgress == 100) { 794 mInPageLoad = false; 795 } 796 mWebViewController.onProgressChanged(Tab.this); 797 if (mUpdateThumbnail && newProgress == 100) { 798 mUpdateThumbnail = false; 799 } 800 } 801 802 @Override 803 public void onReceivedTitle(WebView view, final String title) { 804 mCurrentState.mTitle = title; 805 mWebViewController.onReceivedTitle(Tab.this, title); 806 } 807 808 @Override 809 public void onReceivedIcon(WebView view, Bitmap icon) { 810 mCurrentState.mFavicon = icon; 811 mWebViewController.onFavicon(Tab.this, view, icon); 812 } 813 814 @Override 815 public void onReceivedTouchIconUrl(WebView view, String url, 816 boolean precomposed) { 817 final ContentResolver cr = mContext.getContentResolver(); 818 // Let precomposed icons take precedence over non-composed 819 // icons. 820 if (precomposed && mTouchIconLoader != null) { 821 mTouchIconLoader.cancel(false); 822 mTouchIconLoader = null; 823 } 824 // Have only one async task at a time. 825 if (mTouchIconLoader == null) { 826 mTouchIconLoader = new DownloadTouchIcon(Tab.this, 827 mContext, cr, view); 828 mTouchIconLoader.execute(url); 829 } 830 } 831 832 @Override 833 public void onShowCustomView(View view, 834 WebChromeClient.CustomViewCallback callback) { 835 Activity activity = mWebViewController.getActivity(); 836 if (activity != null) { 837 onShowCustomView(view, activity.getRequestedOrientation(), callback); 838 } 839 } 840 841 @Override 842 public void onShowCustomView(View view, int requestedOrientation, 843 WebChromeClient.CustomViewCallback callback) { 844 if (mInForeground) mWebViewController.showCustomView(Tab.this, view, 845 requestedOrientation, callback); 846 } 847 848 @Override 849 public void onHideCustomView() { 850 if (mInForeground) mWebViewController.hideCustomView(); 851 } 852 853 /** 854 * The origin has exceeded its database quota. 855 * @param url the URL that exceeded the quota 856 * @param databaseIdentifier the identifier of the database on which the 857 * transaction that caused the quota overflow was run 858 * @param currentQuota the current quota for the origin. 859 * @param estimatedSize the estimated size of the database. 860 * @param totalUsedQuota is the sum of all origins' quota. 861 * @param quotaUpdater The callback to run when a decision to allow or 862 * deny quota has been made. Don't forget to call this! 863 */ 864 @Override 865 public void onExceededDatabaseQuota(String url, 866 String databaseIdentifier, long currentQuota, long estimatedSize, 867 long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { 868 mSettings.getWebStorageSizeManager() 869 .onExceededDatabaseQuota(url, databaseIdentifier, 870 currentQuota, estimatedSize, totalUsedQuota, 871 quotaUpdater); 872 } 873 874 /** 875 * The Application Cache has exceeded its max size. 876 * @param spaceNeeded is the amount of disk space that would be needed 877 * in order for the last appcache operation to succeed. 878 * @param totalUsedQuota is the sum of all origins' quota. 879 * @param quotaUpdater A callback to inform the WebCore thread that a 880 * new app cache size is available. This callback must always 881 * be executed at some point to ensure that the sleeping 882 * WebCore thread is woken up. 883 */ 884 @Override 885 public void onReachedMaxAppCacheSize(long spaceNeeded, 886 long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { 887 mSettings.getWebStorageSizeManager() 888 .onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota, 889 quotaUpdater); 890 } 891 892 /** 893 * Instructs the browser to show a prompt to ask the user to set the 894 * Geolocation permission state for the specified origin. 895 * @param origin The origin for which Geolocation permissions are 896 * requested. 897 * @param callback The callback to call once the user has set the 898 * Geolocation permission state. 899 */ 900 @Override 901 public void onGeolocationPermissionsShowPrompt(String origin, 902 GeolocationPermissions.Callback callback) { 903 if (mInForeground) { 904 getGeolocationPermissionsPrompt().show(origin, callback); 905 } 906 } 907 908 /** 909 * Instructs the browser to hide the Geolocation permissions prompt. 910 */ 911 @Override 912 public void onGeolocationPermissionsHidePrompt() { 913 if (mInForeground && mGeolocationPermissionsPrompt != null) { 914 mGeolocationPermissionsPrompt.hide(); 915 } 916 } 917 918 /* Adds a JavaScript error message to the system log and if the JS 919 * console is enabled in the about:debug options, to that console 920 * also. 921 * @param consoleMessage the message object. 922 */ 923 @Override 924 public boolean onConsoleMessage(ConsoleMessage consoleMessage) { 925 if (mInForeground) { 926 // call getErrorConsole(true) so it will create one if needed 927 ErrorConsoleView errorConsole = getErrorConsole(true); 928 errorConsole.addErrorMessage(consoleMessage); 929 if (mWebViewController.shouldShowErrorConsole() 930 && errorConsole.getShowState() != 931 ErrorConsoleView.SHOW_MAXIMIZED) { 932 errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); 933 } 934 } 935 936 // Don't log console messages in private browsing mode 937 if (isPrivateBrowsingEnabled()) return true; 938 939 String message = "Console: " + consoleMessage.message() + " " 940 + consoleMessage.sourceId() + ":" 941 + consoleMessage.lineNumber(); 942 943 switch (consoleMessage.messageLevel()) { 944 case TIP: 945 Log.v(CONSOLE_LOGTAG, message); 946 break; 947 case LOG: 948 Log.i(CONSOLE_LOGTAG, message); 949 break; 950 case WARNING: 951 Log.w(CONSOLE_LOGTAG, message); 952 break; 953 case ERROR: 954 Log.e(CONSOLE_LOGTAG, message); 955 break; 956 case DEBUG: 957 Log.d(CONSOLE_LOGTAG, message); 958 break; 959 } 960 961 return true; 962 } 963 964 /** 965 * Ask the browser for an icon to represent a <video> element. 966 * This icon will be used if the Web page did not specify a poster attribute. 967 * @return Bitmap The icon or null if no such icon is available. 968 */ 969 @Override 970 public Bitmap getDefaultVideoPoster() { 971 if (mInForeground) { 972 return mWebViewController.getDefaultVideoPoster(); 973 } 974 return null; 975 } 976 977 /** 978 * Ask the host application for a custom progress view to show while 979 * a <video> is loading. 980 * @return View The progress view. 981 */ 982 @Override 983 public View getVideoLoadingProgressView() { 984 if (mInForeground) { 985 return mWebViewController.getVideoLoadingProgressView(); 986 } 987 return null; 988 } 989 990 @Override 991 public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) { 992 if (mInForeground) { 993 mWebViewController.openFileChooser(uploadMsg, acceptType, capture); 994 } else { 995 uploadMsg.onReceiveValue(null); 996 } 997 } 998 999 /** 1000 * Deliver a list of already-visited URLs 1001 */ 1002 @Override 1003 public void getVisitedHistory(final ValueCallback<String[]> callback) { 1004 mWebViewController.getVisitedHistory(callback); 1005 } 1006 1007 @Override 1008 public void setupAutoFill(Message message) { 1009 // Prompt the user to set up their profile. 1010 final Message msg = message; 1011 AlertDialog.Builder builder = new AlertDialog.Builder(mContext); 1012 LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 1013 Context.LAYOUT_INFLATER_SERVICE); 1014 final View layout = inflater.inflate(R.layout.setup_autofill_dialog, null); 1015 1016 builder.setView(layout) 1017 .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { 1018 @Override 1019 public void onClick(DialogInterface dialog, int id) { 1020 CheckBox disableAutoFill = (CheckBox) layout.findViewById( 1021 R.id.setup_autofill_dialog_disable_autofill); 1022 1023 if (disableAutoFill.isChecked()) { 1024 // Disable autofill and show a toast with how to turn it on again. 1025 mSettings.setAutofillEnabled(false); 1026 Toast.makeText(mContext, 1027 R.string.autofill_setup_dialog_negative_toast, 1028 Toast.LENGTH_LONG).show(); 1029 } else { 1030 // Take user to the AutoFill profile editor. When they return, 1031 // we will send the message that we pass here which will trigger 1032 // the form to get filled out with their new profile. 1033 mWebViewController.setupAutoFill(msg); 1034 } 1035 } 1036 }) 1037 .setNegativeButton(R.string.cancel, null) 1038 .show(); 1039 } 1040 }; 1041 1042 // ------------------------------------------------------------------------- 1043 // WebViewClient implementation for the sub window 1044 // ------------------------------------------------------------------------- 1045 1046 // Subclass of WebViewClient used in subwindows to notify the main 1047 // WebViewClient of certain WebView activities. 1048 private static class SubWindowClient extends WebViewClient { 1049 // The main WebViewClient. 1050 private final WebViewClient mClient; 1051 private final WebViewController mController; 1052 SubWindowClient(WebViewClient client, WebViewController controller)1053 SubWindowClient(WebViewClient client, WebViewController controller) { 1054 mClient = client; 1055 mController = controller; 1056 } 1057 @Override onPageStarted(WebView view, String url, Bitmap favicon)1058 public void onPageStarted(WebView view, String url, Bitmap favicon) { 1059 // Unlike the others, do not call mClient's version, which would 1060 // change the progress bar. However, we do want to remove the 1061 // find or select dialog. 1062 mController.endActionMode(); 1063 } 1064 @Override doUpdateVisitedHistory(WebView view, String url, boolean isReload)1065 public void doUpdateVisitedHistory(WebView view, String url, 1066 boolean isReload) { 1067 mClient.doUpdateVisitedHistory(view, url, isReload); 1068 } 1069 @Override shouldOverrideUrlLoading(WebView view, String url)1070 public boolean shouldOverrideUrlLoading(WebView view, String url) { 1071 return mClient.shouldOverrideUrlLoading(view, url); 1072 } 1073 @Override onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)1074 public void onReceivedSslError(WebView view, SslErrorHandler handler, 1075 SslError error) { 1076 mClient.onReceivedSslError(view, handler, error); 1077 } 1078 @Override onReceivedClientCertRequest(WebView view, ClientCertRequestHandler handler, String host_and_port)1079 public void onReceivedClientCertRequest(WebView view, 1080 ClientCertRequestHandler handler, String host_and_port) { 1081 mClient.onReceivedClientCertRequest(view, handler, host_and_port); 1082 } 1083 @Override onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm)1084 public void onReceivedHttpAuthRequest(WebView view, 1085 HttpAuthHandler handler, String host, String realm) { 1086 mClient.onReceivedHttpAuthRequest(view, handler, host, realm); 1087 } 1088 @Override onFormResubmission(WebView view, Message dontResend, Message resend)1089 public void onFormResubmission(WebView view, Message dontResend, 1090 Message resend) { 1091 mClient.onFormResubmission(view, dontResend, resend); 1092 } 1093 @Override onReceivedError(WebView view, int errorCode, String description, String failingUrl)1094 public void onReceivedError(WebView view, int errorCode, 1095 String description, String failingUrl) { 1096 mClient.onReceivedError(view, errorCode, description, failingUrl); 1097 } 1098 @Override shouldOverrideKeyEvent(WebView view, android.view.KeyEvent event)1099 public boolean shouldOverrideKeyEvent(WebView view, 1100 android.view.KeyEvent event) { 1101 return mClient.shouldOverrideKeyEvent(view, event); 1102 } 1103 @Override onUnhandledKeyEvent(WebView view, android.view.KeyEvent event)1104 public void onUnhandledKeyEvent(WebView view, 1105 android.view.KeyEvent event) { 1106 mClient.onUnhandledKeyEvent(view, event); 1107 } 1108 } 1109 1110 // ------------------------------------------------------------------------- 1111 // WebChromeClient implementation for the sub window 1112 // ------------------------------------------------------------------------- 1113 1114 private class SubWindowChromeClient extends WebChromeClient { 1115 // The main WebChromeClient. 1116 private final WebChromeClient mClient; 1117 SubWindowChromeClient(WebChromeClient client)1118 SubWindowChromeClient(WebChromeClient client) { 1119 mClient = client; 1120 } 1121 @Override onProgressChanged(WebView view, int newProgress)1122 public void onProgressChanged(WebView view, int newProgress) { 1123 mClient.onProgressChanged(view, newProgress); 1124 } 1125 @Override onCreateWindow(WebView view, boolean dialog, boolean userGesture, android.os.Message resultMsg)1126 public boolean onCreateWindow(WebView view, boolean dialog, 1127 boolean userGesture, android.os.Message resultMsg) { 1128 return mClient.onCreateWindow(view, dialog, userGesture, resultMsg); 1129 } 1130 @Override onCloseWindow(WebView window)1131 public void onCloseWindow(WebView window) { 1132 if (window != mSubView) { 1133 Log.e(LOGTAG, "Can't close the window"); 1134 } 1135 mWebViewController.dismissSubWindow(Tab.this); 1136 } 1137 } 1138 1139 // ------------------------------------------------------------------------- 1140 1141 // Construct a new tab Tab(WebViewController wvcontroller, WebView w)1142 Tab(WebViewController wvcontroller, WebView w) { 1143 this(wvcontroller, w, null); 1144 } 1145 Tab(WebViewController wvcontroller, Bundle state)1146 Tab(WebViewController wvcontroller, Bundle state) { 1147 this(wvcontroller, null, state); 1148 } 1149 Tab(WebViewController wvcontroller, WebView w, Bundle state)1150 Tab(WebViewController wvcontroller, WebView w, Bundle state) { 1151 mWebViewController = wvcontroller; 1152 mContext = mWebViewController.getContext(); 1153 mSettings = BrowserSettings.getInstance(); 1154 mDataController = DataController.getInstance(mContext); 1155 mCurrentState = new PageState(mContext, w != null 1156 ? w.isPrivateBrowsingEnabled() : false); 1157 mInPageLoad = false; 1158 mInForeground = false; 1159 1160 mDownloadListener = new DownloadListener() { 1161 public void onDownloadStart(String url, String userAgent, 1162 String contentDisposition, String mimetype, 1163 long contentLength) { 1164 mWebViewController.onDownloadStart(Tab.this, url, userAgent, contentDisposition, 1165 mimetype, contentLength); 1166 } 1167 }; 1168 mWebBackForwardListClient = new WebBackForwardListClient() { 1169 @Override 1170 public void onNewHistoryItem(WebHistoryItem item) { 1171 if (mClearHistoryUrlPattern != null) { 1172 boolean match = 1173 mClearHistoryUrlPattern.matcher(item.getOriginalUrl()).matches(); 1174 if (LOGD_ENABLED) { 1175 Log.d(LOGTAG, "onNewHistoryItem: match=" + match + "\n\t" 1176 + item.getUrl() + "\n\t" 1177 + mClearHistoryUrlPattern); 1178 } 1179 if (match) { 1180 if (mMainView != null) { 1181 mMainView.clearHistory(); 1182 } 1183 } 1184 mClearHistoryUrlPattern = null; 1185 } 1186 } 1187 }; 1188 1189 mCaptureWidth = mContext.getResources().getDimensionPixelSize( 1190 R.dimen.tab_thumbnail_width); 1191 mCaptureHeight = mContext.getResources().getDimensionPixelSize( 1192 R.dimen.tab_thumbnail_height); 1193 updateShouldCaptureThumbnails(); 1194 restoreState(state); 1195 if (getId() == -1) { 1196 mId = TabControl.getNextId(); 1197 } 1198 setWebView(w); 1199 mHandler = new Handler() { 1200 @Override 1201 public void handleMessage(Message m) { 1202 switch (m.what) { 1203 case MSG_CAPTURE: 1204 capture(); 1205 break; 1206 } 1207 } 1208 }; 1209 } 1210 shouldUpdateThumbnail()1211 public boolean shouldUpdateThumbnail() { 1212 return mUpdateThumbnail; 1213 } 1214 1215 /** 1216 * This is used to get a new ID when the tab has been preloaded, before it is displayed and 1217 * added to TabControl. Preloaded tabs can be created before restoreInstanceState, leading 1218 * to overlapping IDs between the preloaded and restored tabs. 1219 */ refreshIdAfterPreload()1220 public void refreshIdAfterPreload() { 1221 mId = TabControl.getNextId(); 1222 } 1223 updateShouldCaptureThumbnails()1224 public void updateShouldCaptureThumbnails() { 1225 if (mWebViewController.shouldCaptureThumbnails()) { 1226 synchronized (Tab.this) { 1227 if (mCapture == null) { 1228 mCapture = Bitmap.createBitmap(mCaptureWidth, mCaptureHeight, 1229 Bitmap.Config.RGB_565); 1230 mCapture.eraseColor(Color.WHITE); 1231 if (mInForeground) { 1232 postCapture(); 1233 } 1234 } 1235 } 1236 } else { 1237 synchronized (Tab.this) { 1238 mCapture = null; 1239 deleteThumbnail(); 1240 } 1241 } 1242 } 1243 setController(WebViewController ctl)1244 public void setController(WebViewController ctl) { 1245 mWebViewController = ctl; 1246 updateShouldCaptureThumbnails(); 1247 } 1248 getId()1249 public long getId() { 1250 return mId; 1251 } 1252 setWebView(WebView w)1253 void setWebView(WebView w) { 1254 setWebView(w, true); 1255 } 1256 1257 /** 1258 * Sets the WebView for this tab, correctly removing the old WebView from 1259 * the container view. 1260 */ setWebView(WebView w, boolean restore)1261 void setWebView(WebView w, boolean restore) { 1262 if (mMainView == w) { 1263 return; 1264 } 1265 1266 // If the WebView is changing, the page will be reloaded, so any ongoing 1267 // Geolocation permission requests are void. 1268 if (mGeolocationPermissionsPrompt != null) { 1269 mGeolocationPermissionsPrompt.hide(); 1270 } 1271 1272 mWebViewController.onSetWebView(this, w); 1273 1274 if (mMainView != null) { 1275 mMainView.setPictureListener(null); 1276 if (w != null) { 1277 syncCurrentState(w, null); 1278 } else { 1279 mCurrentState = new PageState(mContext, false); 1280 } 1281 } 1282 // set the new one 1283 mMainView = w; 1284 // attach the WebViewClient, WebChromeClient and DownloadListener 1285 if (mMainView != null) { 1286 mMainView.setWebViewClient(mWebViewClient); 1287 mMainView.setWebChromeClient(mWebChromeClient); 1288 // Attach DownloadManager so that downloads can start in an active 1289 // or a non-active window. This can happen when going to a site that 1290 // does a redirect after a period of time. The user could have 1291 // switched to another tab while waiting for the download to start. 1292 mMainView.setDownloadListener(mDownloadListener); 1293 getWebViewClassic().setWebBackForwardListClient(mWebBackForwardListClient); 1294 TabControl tc = mWebViewController.getTabControl(); 1295 if (tc != null && tc.getOnThumbnailUpdatedListener() != null) { 1296 mMainView.setPictureListener(this); 1297 } 1298 if (restore && (mSavedState != null)) { 1299 restoreUserAgent(); 1300 WebBackForwardList restoredState 1301 = mMainView.restoreState(mSavedState); 1302 if (restoredState == null || restoredState.getSize() == 0) { 1303 Log.w(LOGTAG, "Failed to restore WebView state!"); 1304 loadUrl(mCurrentState.mOriginalUrl, null); 1305 } 1306 mSavedState = null; 1307 } 1308 } 1309 } 1310 1311 /** 1312 * Destroy the tab's main WebView and subWindow if any 1313 */ destroy()1314 void destroy() { 1315 if (mMainView != null) { 1316 dismissSubWindow(); 1317 // save the WebView to call destroy() after detach it from the tab 1318 WebView webView = mMainView; 1319 setWebView(null); 1320 webView.destroy(); 1321 } 1322 } 1323 1324 /** 1325 * Remove the tab from the parent 1326 */ removeFromTree()1327 void removeFromTree() { 1328 // detach the children 1329 if (mChildren != null) { 1330 for(Tab t : mChildren) { 1331 t.setParent(null); 1332 } 1333 } 1334 // remove itself from the parent list 1335 if (mParent != null) { 1336 mParent.mChildren.remove(this); 1337 } 1338 deleteThumbnail(); 1339 } 1340 1341 /** 1342 * Create a new subwindow unless a subwindow already exists. 1343 * @return True if a new subwindow was created. False if one already exists. 1344 */ createSubWindow()1345 boolean createSubWindow() { 1346 if (mSubView == null) { 1347 mWebViewController.createSubWindow(this); 1348 mSubView.setWebViewClient(new SubWindowClient(mWebViewClient, 1349 mWebViewController)); 1350 mSubView.setWebChromeClient(new SubWindowChromeClient( 1351 mWebChromeClient)); 1352 // Set a different DownloadListener for the mSubView, since it will 1353 // just need to dismiss the mSubView, rather than close the Tab 1354 mSubView.setDownloadListener(new DownloadListener() { 1355 public void onDownloadStart(String url, String userAgent, 1356 String contentDisposition, String mimetype, 1357 long contentLength) { 1358 mWebViewController.onDownloadStart(Tab.this, url, userAgent, 1359 contentDisposition, mimetype, contentLength); 1360 if (mSubView.copyBackForwardList().getSize() == 0) { 1361 // This subwindow was opened for the sole purpose of 1362 // downloading a file. Remove it. 1363 mWebViewController.dismissSubWindow(Tab.this); 1364 } 1365 } 1366 }); 1367 mSubView.setOnCreateContextMenuListener(mWebViewController.getActivity()); 1368 return true; 1369 } 1370 return false; 1371 } 1372 1373 /** 1374 * Dismiss the subWindow for the tab. 1375 */ dismissSubWindow()1376 void dismissSubWindow() { 1377 if (mSubView != null) { 1378 mWebViewController.endActionMode(); 1379 mSubView.destroy(); 1380 mSubView = null; 1381 mSubViewContainer = null; 1382 } 1383 } 1384 1385 1386 /** 1387 * Set the parent tab of this tab. 1388 */ setParent(Tab parent)1389 void setParent(Tab parent) { 1390 if (parent == this) { 1391 throw new IllegalStateException("Cannot set parent to self!"); 1392 } 1393 mParent = parent; 1394 // This tab may have been freed due to low memory. If that is the case, 1395 // the parent tab id is already saved. If we are changing that id 1396 // (most likely due to removing the parent tab) we must update the 1397 // parent tab id in the saved Bundle. 1398 if (mSavedState != null) { 1399 if (parent == null) { 1400 mSavedState.remove(PARENTTAB); 1401 } else { 1402 mSavedState.putLong(PARENTTAB, parent.getId()); 1403 } 1404 } 1405 1406 // Sync the WebView useragent with the parent 1407 if (parent != null && mSettings.hasDesktopUseragent(parent.getWebView()) 1408 != mSettings.hasDesktopUseragent(getWebView())) { 1409 mSettings.toggleDesktopUseragent(getWebView()); 1410 } 1411 1412 if (parent != null && parent.getId() == getId()) { 1413 throw new IllegalStateException("Parent has same ID as child!"); 1414 } 1415 } 1416 1417 /** 1418 * If this Tab was created through another Tab, then this method returns 1419 * that Tab. 1420 * @return the Tab parent or null 1421 */ getParent()1422 public Tab getParent() { 1423 return mParent; 1424 } 1425 1426 /** 1427 * When a Tab is created through the content of another Tab, then we 1428 * associate the Tabs. 1429 * @param child the Tab that was created from this Tab 1430 */ addChildTab(Tab child)1431 void addChildTab(Tab child) { 1432 if (mChildren == null) { 1433 mChildren = new Vector<Tab>(); 1434 } 1435 mChildren.add(child); 1436 child.setParent(this); 1437 } 1438 getChildren()1439 Vector<Tab> getChildren() { 1440 return mChildren; 1441 } 1442 resume()1443 void resume() { 1444 if (mMainView != null) { 1445 setupHwAcceleration(mMainView); 1446 mMainView.onResume(); 1447 if (mSubView != null) { 1448 mSubView.onResume(); 1449 } 1450 } 1451 } 1452 setupHwAcceleration(View web)1453 private void setupHwAcceleration(View web) { 1454 if (web == null) return; 1455 BrowserSettings settings = BrowserSettings.getInstance(); 1456 if (settings.isHardwareAccelerated()) { 1457 web.setLayerType(View.LAYER_TYPE_NONE, null); 1458 } else { 1459 web.setLayerType(View.LAYER_TYPE_SOFTWARE, null); 1460 } 1461 } 1462 pause()1463 void pause() { 1464 if (mMainView != null) { 1465 mMainView.onPause(); 1466 if (mSubView != null) { 1467 mSubView.onPause(); 1468 } 1469 } 1470 } 1471 putInForeground()1472 void putInForeground() { 1473 if (mInForeground) { 1474 return; 1475 } 1476 mInForeground = true; 1477 resume(); 1478 Activity activity = mWebViewController.getActivity(); 1479 mMainView.setOnCreateContextMenuListener(activity); 1480 if (mSubView != null) { 1481 mSubView.setOnCreateContextMenuListener(activity); 1482 } 1483 // Show the pending error dialog if the queue is not empty 1484 if (mQueuedErrors != null && mQueuedErrors.size() > 0) { 1485 showError(mQueuedErrors.getFirst()); 1486 } 1487 mWebViewController.bookmarkedStatusHasChanged(this); 1488 } 1489 putInBackground()1490 void putInBackground() { 1491 if (!mInForeground) { 1492 return; 1493 } 1494 capture(); 1495 mInForeground = false; 1496 pause(); 1497 mMainView.setOnCreateContextMenuListener(null); 1498 if (mSubView != null) { 1499 mSubView.setOnCreateContextMenuListener(null); 1500 } 1501 } 1502 inForeground()1503 boolean inForeground() { 1504 return mInForeground; 1505 } 1506 1507 /** 1508 * Return the top window of this tab; either the subwindow if it is not 1509 * null or the main window. 1510 * @return The top window of this tab. 1511 */ getTopWindow()1512 WebView getTopWindow() { 1513 if (mSubView != null) { 1514 return mSubView; 1515 } 1516 return mMainView; 1517 } 1518 1519 /** 1520 * Return the main window of this tab. Note: if a tab is freed in the 1521 * background, this can return null. It is only guaranteed to be 1522 * non-null for the current tab. 1523 * @return The main WebView of this tab. 1524 */ getWebView()1525 WebView getWebView() { 1526 return mMainView; 1527 } 1528 1529 /** 1530 * Return the underlying WebViewClassic implementation. As with getWebView, 1531 * this maybe null for background tabs. 1532 * @return The main WebView of this tab. 1533 */ getWebViewClassic()1534 WebViewClassic getWebViewClassic() { 1535 return WebViewClassic.fromWebView(mMainView); 1536 } 1537 setViewContainer(View container)1538 void setViewContainer(View container) { 1539 mContainer = container; 1540 } 1541 getViewContainer()1542 View getViewContainer() { 1543 return mContainer; 1544 } 1545 1546 /** 1547 * Return whether private browsing is enabled for the main window of 1548 * this tab. 1549 * @return True if private browsing is enabled. 1550 */ isPrivateBrowsingEnabled()1551 boolean isPrivateBrowsingEnabled() { 1552 return mCurrentState.mIncognito; 1553 } 1554 1555 /** 1556 * Return the subwindow of this tab or null if there is no subwindow. 1557 * @return The subwindow of this tab or null. 1558 */ getSubWebView()1559 WebView getSubWebView() { 1560 return mSubView; 1561 } 1562 setSubWebView(WebView subView)1563 void setSubWebView(WebView subView) { 1564 mSubView = subView; 1565 } 1566 getSubViewContainer()1567 View getSubViewContainer() { 1568 return mSubViewContainer; 1569 } 1570 setSubViewContainer(View subViewContainer)1571 void setSubViewContainer(View subViewContainer) { 1572 mSubViewContainer = subViewContainer; 1573 } 1574 1575 /** 1576 * @return The geolocation permissions prompt for this tab. 1577 */ getGeolocationPermissionsPrompt()1578 GeolocationPermissionsPrompt getGeolocationPermissionsPrompt() { 1579 if (mGeolocationPermissionsPrompt == null) { 1580 ViewStub stub = (ViewStub) mContainer 1581 .findViewById(R.id.geolocation_permissions_prompt); 1582 mGeolocationPermissionsPrompt = (GeolocationPermissionsPrompt) stub 1583 .inflate(); 1584 } 1585 return mGeolocationPermissionsPrompt; 1586 } 1587 1588 /** 1589 * @return The application id string 1590 */ getAppId()1591 String getAppId() { 1592 return mAppId; 1593 } 1594 1595 /** 1596 * Set the application id string 1597 * @param id 1598 */ setAppId(String id)1599 void setAppId(String id) { 1600 mAppId = id; 1601 } 1602 closeOnBack()1603 boolean closeOnBack() { 1604 return mCloseOnBack; 1605 } 1606 setCloseOnBack(boolean close)1607 void setCloseOnBack(boolean close) { 1608 mCloseOnBack = close; 1609 } 1610 getUrl()1611 String getUrl() { 1612 return UrlUtils.filteredUrl(mCurrentState.mUrl); 1613 } 1614 getOriginalUrl()1615 String getOriginalUrl() { 1616 if (mCurrentState.mOriginalUrl == null) { 1617 return getUrl(); 1618 } 1619 return UrlUtils.filteredUrl(mCurrentState.mOriginalUrl); 1620 } 1621 1622 /** 1623 * Get the title of this tab. 1624 */ getTitle()1625 String getTitle() { 1626 if (mCurrentState.mTitle == null && mInPageLoad) { 1627 return mContext.getString(R.string.title_bar_loading); 1628 } 1629 return mCurrentState.mTitle; 1630 } 1631 1632 /** 1633 * Get the favicon of this tab. 1634 */ getFavicon()1635 Bitmap getFavicon() { 1636 if (mCurrentState.mFavicon != null) { 1637 return mCurrentState.mFavicon; 1638 } 1639 return getDefaultFavicon(mContext); 1640 } 1641 isBookmarkedSite()1642 public boolean isBookmarkedSite() { 1643 return mCurrentState.mIsBookmarkedSite; 1644 } 1645 1646 /** 1647 * Return the tab's error console. Creates the console if createIfNEcessary 1648 * is true and we haven't already created the console. 1649 * @param createIfNecessary Flag to indicate if the console should be 1650 * created if it has not been already. 1651 * @return The tab's error console, or null if one has not been created and 1652 * createIfNecessary is false. 1653 */ getErrorConsole(boolean createIfNecessary)1654 ErrorConsoleView getErrorConsole(boolean createIfNecessary) { 1655 if (createIfNecessary && mErrorConsole == null) { 1656 mErrorConsole = new ErrorConsoleView(mContext); 1657 mErrorConsole.setWebView(mMainView); 1658 } 1659 return mErrorConsole; 1660 } 1661 1662 /** 1663 * Sets the security state, clears the SSL certificate error and informs 1664 * the controller. 1665 */ setSecurityState(SecurityState securityState)1666 private void setSecurityState(SecurityState securityState) { 1667 mCurrentState.mSecurityState = securityState; 1668 mCurrentState.mSslCertificateError = null; 1669 mWebViewController.onUpdatedSecurityState(this); 1670 } 1671 1672 /** 1673 * @return The tab's security state. 1674 */ getSecurityState()1675 SecurityState getSecurityState() { 1676 return mCurrentState.mSecurityState; 1677 } 1678 1679 /** 1680 * Gets the SSL certificate error, if any, for the page's main resource. 1681 * This is only non-null when the security state is 1682 * SECURITY_STATE_BAD_CERTIFICATE. 1683 */ getSslCertificateError()1684 SslError getSslCertificateError() { 1685 return mCurrentState.mSslCertificateError; 1686 } 1687 getLoadProgress()1688 int getLoadProgress() { 1689 if (mInPageLoad) { 1690 return mPageLoadProgress; 1691 } 1692 return 100; 1693 } 1694 1695 /** 1696 * @return TRUE if onPageStarted is called while onPageFinished is not 1697 * called yet. 1698 */ inPageLoad()1699 boolean inPageLoad() { 1700 return mInPageLoad; 1701 } 1702 1703 /** 1704 * @return The Bundle with the tab's state if it can be saved, otherwise null 1705 */ saveState()1706 public Bundle saveState() { 1707 // If the WebView is null it means we ran low on memory and we already 1708 // stored the saved state in mSavedState. 1709 if (mMainView == null) { 1710 return mSavedState; 1711 } 1712 1713 if (TextUtils.isEmpty(mCurrentState.mUrl)) { 1714 return null; 1715 } 1716 1717 mSavedState = new Bundle(); 1718 WebBackForwardList savedList = mMainView.saveState(mSavedState); 1719 if (savedList == null || savedList.getSize() == 0) { 1720 Log.w(LOGTAG, "Failed to save back/forward list for " 1721 + mCurrentState.mUrl); 1722 } 1723 1724 mSavedState.putLong(ID, mId); 1725 mSavedState.putString(CURRURL, mCurrentState.mUrl); 1726 mSavedState.putString(CURRTITLE, mCurrentState.mTitle); 1727 mSavedState.putBoolean(INCOGNITO, mMainView.isPrivateBrowsingEnabled()); 1728 if (mAppId != null) { 1729 mSavedState.putString(APPID, mAppId); 1730 } 1731 mSavedState.putBoolean(CLOSEFLAG, mCloseOnBack); 1732 // Remember the parent tab so the relationship can be restored. 1733 if (mParent != null) { 1734 mSavedState.putLong(PARENTTAB, mParent.mId); 1735 } 1736 mSavedState.putBoolean(USERAGENT, 1737 mSettings.hasDesktopUseragent(getWebView())); 1738 return mSavedState; 1739 } 1740 1741 /* 1742 * Restore the state of the tab. 1743 */ restoreState(Bundle b)1744 private void restoreState(Bundle b) { 1745 mSavedState = b; 1746 if (mSavedState == null) { 1747 return; 1748 } 1749 // Restore the internal state even if the WebView fails to restore. 1750 // This will maintain the app id, original url and close-on-exit values. 1751 mId = b.getLong(ID); 1752 mAppId = b.getString(APPID); 1753 mCloseOnBack = b.getBoolean(CLOSEFLAG); 1754 restoreUserAgent(); 1755 String url = b.getString(CURRURL); 1756 String title = b.getString(CURRTITLE); 1757 boolean incognito = b.getBoolean(INCOGNITO); 1758 mCurrentState = new PageState(mContext, incognito, url, null); 1759 mCurrentState.mTitle = title; 1760 synchronized (Tab.this) { 1761 if (mCapture != null) { 1762 DataController.getInstance(mContext).loadThumbnail(this); 1763 } 1764 } 1765 } 1766 restoreUserAgent()1767 private void restoreUserAgent() { 1768 if (mMainView == null || mSavedState == null) { 1769 return; 1770 } 1771 if (mSavedState.getBoolean(USERAGENT) 1772 != mSettings.hasDesktopUseragent(mMainView)) { 1773 mSettings.toggleDesktopUseragent(mMainView); 1774 } 1775 } 1776 updateBookmarkedStatus()1777 public void updateBookmarkedStatus() { 1778 mDataController.queryBookmarkStatus(getUrl(), mIsBookmarkCallback); 1779 } 1780 1781 private DataController.OnQueryUrlIsBookmark mIsBookmarkCallback 1782 = new DataController.OnQueryUrlIsBookmark() { 1783 @Override 1784 public void onQueryUrlIsBookmark(String url, boolean isBookmark) { 1785 if (mCurrentState.mUrl.equals(url)) { 1786 mCurrentState.mIsBookmarkedSite = isBookmark; 1787 mWebViewController.bookmarkedStatusHasChanged(Tab.this); 1788 } 1789 } 1790 }; 1791 getScreenshot()1792 public Bitmap getScreenshot() { 1793 synchronized (Tab.this) { 1794 return mCapture; 1795 } 1796 } 1797 isSnapshot()1798 public boolean isSnapshot() { 1799 return false; 1800 } 1801 1802 private static class SaveCallback implements ValueCallback<Boolean> { 1803 boolean mResult; 1804 1805 @Override onReceiveValue(Boolean value)1806 public void onReceiveValue(Boolean value) { 1807 mResult = value; 1808 synchronized (this) { 1809 notifyAll(); 1810 } 1811 } 1812 1813 } 1814 1815 /** 1816 * Must be called on the UI thread 1817 */ createSnapshotValues()1818 public ContentValues createSnapshotValues() { 1819 WebViewClassic web = getWebViewClassic(); 1820 if (web == null) return null; 1821 ContentValues values = new ContentValues(); 1822 values.put(Snapshots.TITLE, mCurrentState.mTitle); 1823 values.put(Snapshots.URL, mCurrentState.mUrl); 1824 values.put(Snapshots.BACKGROUND, web.getPageBackgroundColor()); 1825 values.put(Snapshots.DATE_CREATED, System.currentTimeMillis()); 1826 values.put(Snapshots.FAVICON, compressBitmap(getFavicon())); 1827 Bitmap screenshot = Controller.createScreenshot(mMainView, 1828 Controller.getDesiredThumbnailWidth(mContext), 1829 Controller.getDesiredThumbnailHeight(mContext)); 1830 values.put(Snapshots.THUMBNAIL, compressBitmap(screenshot)); 1831 return values; 1832 } 1833 1834 /** 1835 * Probably want to call this on a background thread 1836 */ saveViewState(ContentValues values)1837 public boolean saveViewState(ContentValues values) { 1838 WebViewClassic web = getWebViewClassic(); 1839 if (web == null) return false; 1840 String path = UUID.randomUUID().toString(); 1841 SaveCallback callback = new SaveCallback(); 1842 OutputStream outs = null; 1843 try { 1844 outs = mContext.openFileOutput(path, Context.MODE_PRIVATE); 1845 GZIPOutputStream stream = new GZIPOutputStream(outs); 1846 synchronized (callback) { 1847 web.saveViewState(stream, callback); 1848 callback.wait(); 1849 } 1850 stream.flush(); 1851 stream.close(); 1852 } catch (Exception e) { 1853 Log.w(LOGTAG, "Failed to save view state", e); 1854 if (outs != null) { 1855 try { 1856 outs.close(); 1857 } catch (IOException ignore) {} 1858 } 1859 File file = mContext.getFileStreamPath(path); 1860 if (file.exists() && !file.delete()) { 1861 file.deleteOnExit(); 1862 } 1863 return false; 1864 } 1865 File savedFile = mContext.getFileStreamPath(path); 1866 if (!callback.mResult) { 1867 if (!savedFile.delete()) { 1868 savedFile.deleteOnExit(); 1869 } 1870 return false; 1871 } 1872 long size = savedFile.length(); 1873 values.put(Snapshots.VIEWSTATE_PATH, path); 1874 values.put(Snapshots.VIEWSTATE_SIZE, size); 1875 return true; 1876 } 1877 compressBitmap(Bitmap bitmap)1878 public byte[] compressBitmap(Bitmap bitmap) { 1879 if (bitmap == null) { 1880 return null; 1881 } 1882 ByteArrayOutputStream stream = new ByteArrayOutputStream(); 1883 bitmap.compress(CompressFormat.PNG, 100, stream); 1884 return stream.toByteArray(); 1885 } 1886 loadUrl(String url, Map<String, String> headers)1887 public void loadUrl(String url, Map<String, String> headers) { 1888 if (mMainView != null) { 1889 mPageLoadProgress = INITIAL_PROGRESS; 1890 mInPageLoad = true; 1891 mCurrentState = new PageState(mContext, false, url, null); 1892 mWebViewController.onPageStarted(this, mMainView, null); 1893 mMainView.loadUrl(url, headers); 1894 } 1895 } 1896 capture()1897 protected void capture() { 1898 if (mMainView == null || mCapture == null) return; 1899 if (mMainView.getContentWidth() <= 0 || mMainView.getContentHeight() <= 0) { 1900 return; 1901 } 1902 Canvas c = new Canvas(mCapture); 1903 final int left = mMainView.getScrollX(); 1904 final int top = mMainView.getScrollY() + mMainView.getVisibleTitleHeight(); 1905 int state = c.save(); 1906 c.translate(-left, -top); 1907 float scale = mCaptureWidth / (float) mMainView.getWidth(); 1908 c.scale(scale, scale, left, top); 1909 if (mMainView instanceof BrowserWebView) { 1910 ((BrowserWebView)mMainView).drawContent(c); 1911 } else { 1912 mMainView.draw(c); 1913 } 1914 c.restoreToCount(state); 1915 // manually anti-alias the edges for the tilt 1916 c.drawRect(0, 0, 1, mCapture.getHeight(), sAlphaPaint); 1917 c.drawRect(mCapture.getWidth() - 1, 0, mCapture.getWidth(), 1918 mCapture.getHeight(), sAlphaPaint); 1919 c.drawRect(0, 0, mCapture.getWidth(), 1, sAlphaPaint); 1920 c.drawRect(0, mCapture.getHeight() - 1, mCapture.getWidth(), 1921 mCapture.getHeight(), sAlphaPaint); 1922 c.setBitmap(null); 1923 mHandler.removeMessages(MSG_CAPTURE); 1924 persistThumbnail(); 1925 TabControl tc = mWebViewController.getTabControl(); 1926 if (tc != null) { 1927 OnThumbnailUpdatedListener updateListener 1928 = tc.getOnThumbnailUpdatedListener(); 1929 if (updateListener != null) { 1930 updateListener.onThumbnailUpdated(this); 1931 } 1932 } 1933 } 1934 1935 @Override onNewPicture(WebView view, Picture picture)1936 public void onNewPicture(WebView view, Picture picture) { 1937 postCapture(); 1938 } 1939 postCapture()1940 private void postCapture() { 1941 if (!mHandler.hasMessages(MSG_CAPTURE)) { 1942 mHandler.sendEmptyMessageDelayed(MSG_CAPTURE, CAPTURE_DELAY); 1943 } 1944 } 1945 canGoBack()1946 public boolean canGoBack() { 1947 return mMainView != null ? mMainView.canGoBack() : false; 1948 } 1949 canGoForward()1950 public boolean canGoForward() { 1951 return mMainView != null ? mMainView.canGoForward() : false; 1952 } 1953 goBack()1954 public void goBack() { 1955 if (mMainView != null) { 1956 mMainView.goBack(); 1957 } 1958 } 1959 goForward()1960 public void goForward() { 1961 if (mMainView != null) { 1962 mMainView.goForward(); 1963 } 1964 } 1965 1966 /** 1967 * Causes the tab back/forward stack to be cleared once, if the given URL is the next URL 1968 * to be added to the stack. 1969 * 1970 * This is used to ensure that preloaded URLs that are not subsequently seen by the user do 1971 * not appear in the back stack. 1972 */ clearBackStackWhenItemAdded(Pattern urlPattern)1973 public void clearBackStackWhenItemAdded(Pattern urlPattern) { 1974 mClearHistoryUrlPattern = urlPattern; 1975 } 1976 persistThumbnail()1977 protected void persistThumbnail() { 1978 DataController.getInstance(mContext).saveThumbnail(this); 1979 } 1980 deleteThumbnail()1981 protected void deleteThumbnail() { 1982 DataController.getInstance(mContext).deleteThumbnail(this); 1983 } 1984 updateCaptureFromBlob(byte[] blob)1985 void updateCaptureFromBlob(byte[] blob) { 1986 synchronized (Tab.this) { 1987 if (mCapture == null) { 1988 return; 1989 } 1990 ByteBuffer buffer = ByteBuffer.wrap(blob); 1991 try { 1992 mCapture.copyPixelsFromBuffer(buffer); 1993 } catch (RuntimeException rex) { 1994 Log.e(LOGTAG, "Load capture has mismatched sizes; buffer: " 1995 + buffer.capacity() + " blob: " + blob.length 1996 + "capture: " + mCapture.getByteCount()); 1997 throw rex; 1998 } 1999 } 2000 } 2001 2002 @Override toString()2003 public String toString() { 2004 StringBuilder builder = new StringBuilder(100); 2005 builder.append(mId); 2006 builder.append(") has parent: "); 2007 if (getParent() != null) { 2008 builder.append("true["); 2009 builder.append(getParent().getId()); 2010 builder.append("]"); 2011 } else { 2012 builder.append("false"); 2013 } 2014 builder.append(", incog: "); 2015 builder.append(isPrivateBrowsingEnabled()); 2016 if (!isPrivateBrowsingEnabled()) { 2017 builder.append(", title: "); 2018 builder.append(getTitle()); 2019 builder.append(", url: "); 2020 builder.append(getUrl()); 2021 } 2022 return builder.toString(); 2023 } 2024 handleProceededAfterSslError(SslError error)2025 private void handleProceededAfterSslError(SslError error) { 2026 if (error.getUrl().equals(mCurrentState.mUrl)) { 2027 // The security state should currently be SECURITY_STATE_SECURE. 2028 setSecurityState(SecurityState.SECURITY_STATE_BAD_CERTIFICATE); 2029 mCurrentState.mSslCertificateError = error; 2030 } else if (getSecurityState() == SecurityState.SECURITY_STATE_SECURE) { 2031 // The page's main resource is secure and this error is for a 2032 // sub-resource. 2033 setSecurityState(SecurityState.SECURITY_STATE_MIXED); 2034 } 2035 } 2036 } 2037