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