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