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 if (BrowserWebView.isClassic()) { 1297 getWebViewClassic().setWebBackForwardListClient(mWebBackForwardListClient); 1298 } 1299 TabControl tc = mWebViewController.getTabControl(); 1300 if (tc != null && tc.getOnThumbnailUpdatedListener() != null) { 1301 mMainView.setPictureListener(this); 1302 } 1303 if (restore && (mSavedState != null)) { 1304 restoreUserAgent(); 1305 WebBackForwardList restoredState 1306 = mMainView.restoreState(mSavedState); 1307 if (restoredState == null || restoredState.getSize() == 0) { 1308 Log.w(LOGTAG, "Failed to restore WebView state!"); 1309 loadUrl(mCurrentState.mOriginalUrl, null); 1310 } 1311 mSavedState = null; 1312 } 1313 } 1314 } 1315 1316 /** 1317 * Destroy the tab's main WebView and subWindow if any 1318 */ 1319 void destroy() { 1320 if (mMainView != null) { 1321 dismissSubWindow(); 1322 // save the WebView to call destroy() after detach it from the tab 1323 WebView webView = mMainView; 1324 setWebView(null); 1325 webView.destroy(); 1326 } 1327 } 1328 1329 /** 1330 * Remove the tab from the parent 1331 */ 1332 void removeFromTree() { 1333 // detach the children 1334 if (mChildren != null) { 1335 for(Tab t : mChildren) { 1336 t.setParent(null); 1337 } 1338 } 1339 // remove itself from the parent list 1340 if (mParent != null) { 1341 mParent.mChildren.remove(this); 1342 } 1343 deleteThumbnail(); 1344 } 1345 1346 /** 1347 * Create a new subwindow unless a subwindow already exists. 1348 * @return True if a new subwindow was created. False if one already exists. 1349 */ 1350 boolean createSubWindow() { 1351 if (mSubView == null) { 1352 mWebViewController.createSubWindow(this); 1353 mSubView.setWebViewClient(new SubWindowClient(mWebViewClient, 1354 mWebViewController)); 1355 mSubView.setWebChromeClient(new SubWindowChromeClient( 1356 mWebChromeClient)); 1357 // Set a different DownloadListener for the mSubView, since it will 1358 // just need to dismiss the mSubView, rather than close the Tab 1359 mSubView.setDownloadListener(new BrowserDownloadListener() { 1360 public void onDownloadStart(String url, String userAgent, 1361 String contentDisposition, String mimetype, String referer, 1362 long contentLength) { 1363 mWebViewController.onDownloadStart(Tab.this, url, userAgent, 1364 contentDisposition, mimetype, referer, contentLength); 1365 if (mSubView.copyBackForwardList().getSize() == 0) { 1366 // This subwindow was opened for the sole purpose of 1367 // downloading a file. Remove it. 1368 mWebViewController.dismissSubWindow(Tab.this); 1369 } 1370 } 1371 }); 1372 mSubView.setOnCreateContextMenuListener(mWebViewController.getActivity()); 1373 return true; 1374 } 1375 return false; 1376 } 1377 1378 /** 1379 * Dismiss the subWindow for the tab. 1380 */ 1381 void dismissSubWindow() { 1382 if (mSubView != null) { 1383 mWebViewController.endActionMode(); 1384 mSubView.destroy(); 1385 mSubView = null; 1386 mSubViewContainer = null; 1387 } 1388 } 1389 1390 1391 /** 1392 * Set the parent tab of this tab. 1393 */ 1394 void setParent(Tab parent) { 1395 if (parent == this) { 1396 throw new IllegalStateException("Cannot set parent to self!"); 1397 } 1398 mParent = parent; 1399 // This tab may have been freed due to low memory. If that is the case, 1400 // the parent tab id is already saved. If we are changing that id 1401 // (most likely due to removing the parent tab) we must update the 1402 // parent tab id in the saved Bundle. 1403 if (mSavedState != null) { 1404 if (parent == null) { 1405 mSavedState.remove(PARENTTAB); 1406 } else { 1407 mSavedState.putLong(PARENTTAB, parent.getId()); 1408 } 1409 } 1410 1411 // Sync the WebView useragent with the parent 1412 if (parent != null && mSettings.hasDesktopUseragent(parent.getWebView()) 1413 != mSettings.hasDesktopUseragent(getWebView())) { 1414 mSettings.toggleDesktopUseragent(getWebView()); 1415 } 1416 1417 if (parent != null && parent.getId() == getId()) { 1418 throw new IllegalStateException("Parent has same ID as child!"); 1419 } 1420 } 1421 1422 /** 1423 * If this Tab was created through another Tab, then this method returns 1424 * that Tab. 1425 * @return the Tab parent or null 1426 */ 1427 public Tab getParent() { 1428 return mParent; 1429 } 1430 1431 /** 1432 * When a Tab is created through the content of another Tab, then we 1433 * associate the Tabs. 1434 * @param child the Tab that was created from this Tab 1435 */ 1436 void addChildTab(Tab child) { 1437 if (mChildren == null) { 1438 mChildren = new Vector<Tab>(); 1439 } 1440 mChildren.add(child); 1441 child.setParent(this); 1442 } 1443 1444 Vector<Tab> getChildren() { 1445 return mChildren; 1446 } 1447 1448 void resume() { 1449 if (mMainView != null) { 1450 setupHwAcceleration(mMainView); 1451 mMainView.onResume(); 1452 if (mSubView != null) { 1453 mSubView.onResume(); 1454 } 1455 } 1456 } 1457 1458 private void setupHwAcceleration(View web) { 1459 if (web == null) return; 1460 BrowserSettings settings = BrowserSettings.getInstance(); 1461 if (settings.isHardwareAccelerated()) { 1462 web.setLayerType(View.LAYER_TYPE_NONE, null); 1463 } else { 1464 web.setLayerType(View.LAYER_TYPE_SOFTWARE, null); 1465 } 1466 } 1467 1468 void pause() { 1469 if (mMainView != null) { 1470 mMainView.onPause(); 1471 if (mSubView != null) { 1472 mSubView.onPause(); 1473 } 1474 } 1475 } 1476 1477 void putInForeground() { 1478 if (mInForeground) { 1479 return; 1480 } 1481 mInForeground = true; 1482 resume(); 1483 Activity activity = mWebViewController.getActivity(); 1484 mMainView.setOnCreateContextMenuListener(activity); 1485 if (mSubView != null) { 1486 mSubView.setOnCreateContextMenuListener(activity); 1487 } 1488 // Show the pending error dialog if the queue is not empty 1489 if (mQueuedErrors != null && mQueuedErrors.size() > 0) { 1490 showError(mQueuedErrors.getFirst()); 1491 } 1492 mWebViewController.bookmarkedStatusHasChanged(this); 1493 } 1494 1495 void putInBackground() { 1496 if (!mInForeground) { 1497 return; 1498 } 1499 capture(); 1500 mInForeground = false; 1501 pause(); 1502 mMainView.setOnCreateContextMenuListener(null); 1503 if (mSubView != null) { 1504 mSubView.setOnCreateContextMenuListener(null); 1505 } 1506 } 1507 1508 boolean inForeground() { 1509 return mInForeground; 1510 } 1511 1512 /** 1513 * Return the top window of this tab; either the subwindow if it is not 1514 * null or the main window. 1515 * @return The top window of this tab. 1516 */ 1517 WebView getTopWindow() { 1518 if (mSubView != null) { 1519 return mSubView; 1520 } 1521 return mMainView; 1522 } 1523 1524 /** 1525 * Return the main window of this tab. Note: if a tab is freed in the 1526 * background, this can return null. It is only guaranteed to be 1527 * non-null for the current tab. 1528 * @return The main WebView of this tab. 1529 */ 1530 WebView getWebView() { 1531 return mMainView; 1532 } 1533 1534 /** 1535 * Return the underlying WebViewClassic implementation. As with getWebView, 1536 * this maybe null for background tabs. 1537 * @return The main WebView of this tab. 1538 */ 1539 WebViewClassic getWebViewClassic() { 1540 if (!BrowserWebView.isClassic()) { 1541 return null; 1542 } 1543 return WebViewClassic.fromWebView(mMainView); 1544 } 1545 1546 void setViewContainer(View container) { 1547 mContainer = container; 1548 } 1549 1550 View getViewContainer() { 1551 return mContainer; 1552 } 1553 1554 /** 1555 * Return whether private browsing is enabled for the main window of 1556 * this tab. 1557 * @return True if private browsing is enabled. 1558 */ 1559 boolean isPrivateBrowsingEnabled() { 1560 return mCurrentState.mIncognito; 1561 } 1562 1563 /** 1564 * Return the subwindow of this tab or null if there is no subwindow. 1565 * @return The subwindow of this tab or null. 1566 */ 1567 WebView getSubWebView() { 1568 return mSubView; 1569 } 1570 1571 void setSubWebView(WebView subView) { 1572 mSubView = subView; 1573 } 1574 1575 View getSubViewContainer() { 1576 return mSubViewContainer; 1577 } 1578 1579 void setSubViewContainer(View subViewContainer) { 1580 mSubViewContainer = subViewContainer; 1581 } 1582 1583 /** 1584 * @return The geolocation permissions prompt for this tab. 1585 */ 1586 GeolocationPermissionsPrompt getGeolocationPermissionsPrompt() { 1587 if (mGeolocationPermissionsPrompt == null) { 1588 ViewStub stub = (ViewStub) mContainer 1589 .findViewById(R.id.geolocation_permissions_prompt); 1590 mGeolocationPermissionsPrompt = (GeolocationPermissionsPrompt) stub 1591 .inflate(); 1592 } 1593 return mGeolocationPermissionsPrompt; 1594 } 1595 1596 /** 1597 * @return The application id string 1598 */ 1599 String getAppId() { 1600 return mAppId; 1601 } 1602 1603 /** 1604 * Set the application id string 1605 * @param id 1606 */ 1607 void setAppId(String id) { 1608 mAppId = id; 1609 } 1610 1611 boolean closeOnBack() { 1612 return mCloseOnBack; 1613 } 1614 1615 void setCloseOnBack(boolean close) { 1616 mCloseOnBack = close; 1617 } 1618 1619 String getUrl() { 1620 return UrlUtils.filteredUrl(mCurrentState.mUrl); 1621 } 1622 1623 String getOriginalUrl() { 1624 if (mCurrentState.mOriginalUrl == null) { 1625 return getUrl(); 1626 } 1627 return UrlUtils.filteredUrl(mCurrentState.mOriginalUrl); 1628 } 1629 1630 /** 1631 * Get the title of this tab. 1632 */ 1633 String getTitle() { 1634 if (mCurrentState.mTitle == null && mInPageLoad) { 1635 return mContext.getString(R.string.title_bar_loading); 1636 } 1637 return mCurrentState.mTitle; 1638 } 1639 1640 /** 1641 * Get the favicon of this tab. 1642 */ 1643 Bitmap getFavicon() { 1644 if (mCurrentState.mFavicon != null) { 1645 return mCurrentState.mFavicon; 1646 } 1647 return getDefaultFavicon(mContext); 1648 } 1649 1650 public boolean isBookmarkedSite() { 1651 return mCurrentState.mIsBookmarkedSite; 1652 } 1653 1654 /** 1655 * Return the tab's error console. Creates the console if createIfNEcessary 1656 * is true and we haven't already created the console. 1657 * @param createIfNecessary Flag to indicate if the console should be 1658 * created if it has not been already. 1659 * @return The tab's error console, or null if one has not been created and 1660 * createIfNecessary is false. 1661 */ 1662 ErrorConsoleView getErrorConsole(boolean createIfNecessary) { 1663 if (createIfNecessary && mErrorConsole == null) { 1664 mErrorConsole = new ErrorConsoleView(mContext); 1665 mErrorConsole.setWebView(mMainView); 1666 } 1667 return mErrorConsole; 1668 } 1669 1670 /** 1671 * Sets the security state, clears the SSL certificate error and informs 1672 * the controller. 1673 */ 1674 private void setSecurityState(SecurityState securityState) { 1675 mCurrentState.mSecurityState = securityState; 1676 mCurrentState.mSslCertificateError = null; 1677 mWebViewController.onUpdatedSecurityState(this); 1678 } 1679 1680 /** 1681 * @return The tab's security state. 1682 */ 1683 SecurityState getSecurityState() { 1684 return mCurrentState.mSecurityState; 1685 } 1686 1687 /** 1688 * Gets the SSL certificate error, if any, for the page's main resource. 1689 * This is only non-null when the security state is 1690 * SECURITY_STATE_BAD_CERTIFICATE. 1691 */ 1692 SslError getSslCertificateError() { 1693 return mCurrentState.mSslCertificateError; 1694 } 1695 1696 int getLoadProgress() { 1697 if (mInPageLoad) { 1698 return mPageLoadProgress; 1699 } 1700 return 100; 1701 } 1702 1703 /** 1704 * @return TRUE if onPageStarted is called while onPageFinished is not 1705 * called yet. 1706 */ 1707 boolean inPageLoad() { 1708 return mInPageLoad; 1709 } 1710 1711 /** 1712 * @return The Bundle with the tab's state if it can be saved, otherwise null 1713 */ 1714 public Bundle saveState() { 1715 // If the WebView is null it means we ran low on memory and we already 1716 // stored the saved state in mSavedState. 1717 if (mMainView == null) { 1718 return mSavedState; 1719 } 1720 1721 if (TextUtils.isEmpty(mCurrentState.mUrl)) { 1722 return null; 1723 } 1724 1725 mSavedState = new Bundle(); 1726 WebBackForwardList savedList = mMainView.saveState(mSavedState); 1727 if (savedList == null || savedList.getSize() == 0) { 1728 Log.w(LOGTAG, "Failed to save back/forward list for " 1729 + mCurrentState.mUrl); 1730 } 1731 1732 mSavedState.putLong(ID, mId); 1733 mSavedState.putString(CURRURL, mCurrentState.mUrl); 1734 mSavedState.putString(CURRTITLE, mCurrentState.mTitle); 1735 mSavedState.putBoolean(INCOGNITO, mMainView.isPrivateBrowsingEnabled()); 1736 if (mAppId != null) { 1737 mSavedState.putString(APPID, mAppId); 1738 } 1739 mSavedState.putBoolean(CLOSEFLAG, mCloseOnBack); 1740 // Remember the parent tab so the relationship can be restored. 1741 if (mParent != null) { 1742 mSavedState.putLong(PARENTTAB, mParent.mId); 1743 } 1744 mSavedState.putBoolean(USERAGENT, 1745 mSettings.hasDesktopUseragent(getWebView())); 1746 return mSavedState; 1747 } 1748 1749 /* 1750 * Restore the state of the tab. 1751 */ 1752 private void restoreState(Bundle b) { 1753 mSavedState = b; 1754 if (mSavedState == null) { 1755 return; 1756 } 1757 // Restore the internal state even if the WebView fails to restore. 1758 // This will maintain the app id, original url and close-on-exit values. 1759 mId = b.getLong(ID); 1760 mAppId = b.getString(APPID); 1761 mCloseOnBack = b.getBoolean(CLOSEFLAG); 1762 restoreUserAgent(); 1763 String url = b.getString(CURRURL); 1764 String title = b.getString(CURRTITLE); 1765 boolean incognito = b.getBoolean(INCOGNITO); 1766 mCurrentState = new PageState(mContext, incognito, url, null); 1767 mCurrentState.mTitle = title; 1768 synchronized (Tab.this) { 1769 if (mCapture != null) { 1770 DataController.getInstance(mContext).loadThumbnail(this); 1771 } 1772 } 1773 } 1774 1775 private void restoreUserAgent() { 1776 if (mMainView == null || mSavedState == null) { 1777 return; 1778 } 1779 if (mSavedState.getBoolean(USERAGENT) 1780 != mSettings.hasDesktopUseragent(mMainView)) { 1781 mSettings.toggleDesktopUseragent(mMainView); 1782 } 1783 } 1784 1785 public void updateBookmarkedStatus() { 1786 mDataController.queryBookmarkStatus(getUrl(), mIsBookmarkCallback); 1787 } 1788 1789 private DataController.OnQueryUrlIsBookmark mIsBookmarkCallback 1790 = new DataController.OnQueryUrlIsBookmark() { 1791 @Override 1792 public void onQueryUrlIsBookmark(String url, boolean isBookmark) { 1793 if (mCurrentState.mUrl.equals(url)) { 1794 mCurrentState.mIsBookmarkedSite = isBookmark; 1795 mWebViewController.bookmarkedStatusHasChanged(Tab.this); 1796 } 1797 } 1798 }; 1799 1800 public Bitmap getScreenshot() { 1801 synchronized (Tab.this) { 1802 return mCapture; 1803 } 1804 } 1805 1806 public boolean isSnapshot() { 1807 return false; 1808 } 1809 1810 private static class SaveCallback implements ValueCallback<Boolean> { 1811 boolean mResult; 1812 1813 @Override 1814 public void onReceiveValue(Boolean value) { 1815 mResult = value; 1816 synchronized (this) { 1817 notifyAll(); 1818 } 1819 } 1820 1821 } 1822 1823 /** 1824 * Must be called on the UI thread 1825 */ 1826 public ContentValues createSnapshotValues() { 1827 WebViewClassic web = getWebViewClassic(); 1828 if (web == null) return null; 1829 ContentValues values = new ContentValues(); 1830 values.put(Snapshots.TITLE, mCurrentState.mTitle); 1831 values.put(Snapshots.URL, mCurrentState.mUrl); 1832 values.put(Snapshots.BACKGROUND, web.getPageBackgroundColor()); 1833 values.put(Snapshots.DATE_CREATED, System.currentTimeMillis()); 1834 values.put(Snapshots.FAVICON, compressBitmap(getFavicon())); 1835 Bitmap screenshot = Controller.createScreenshot(mMainView, 1836 Controller.getDesiredThumbnailWidth(mContext), 1837 Controller.getDesiredThumbnailHeight(mContext)); 1838 values.put(Snapshots.THUMBNAIL, compressBitmap(screenshot)); 1839 return values; 1840 } 1841 1842 /** 1843 * Probably want to call this on a background thread 1844 */ 1845 public boolean saveViewState(ContentValues values) { 1846 WebViewClassic web = getWebViewClassic(); 1847 if (web == null) return false; 1848 String path = UUID.randomUUID().toString(); 1849 SaveCallback callback = new SaveCallback(); 1850 OutputStream outs = null; 1851 try { 1852 outs = mContext.openFileOutput(path, Context.MODE_PRIVATE); 1853 GZIPOutputStream stream = new GZIPOutputStream(outs); 1854 synchronized (callback) { 1855 web.saveViewState(stream, callback); 1856 callback.wait(); 1857 } 1858 stream.flush(); 1859 stream.close(); 1860 } catch (Exception e) { 1861 Log.w(LOGTAG, "Failed to save view state", e); 1862 if (outs != null) { 1863 try { 1864 outs.close(); 1865 } catch (IOException ignore) {} 1866 } 1867 File file = mContext.getFileStreamPath(path); 1868 if (file.exists() && !file.delete()) { 1869 file.deleteOnExit(); 1870 } 1871 return false; 1872 } 1873 File savedFile = mContext.getFileStreamPath(path); 1874 if (!callback.mResult) { 1875 if (!savedFile.delete()) { 1876 savedFile.deleteOnExit(); 1877 } 1878 return false; 1879 } 1880 long size = savedFile.length(); 1881 values.put(Snapshots.VIEWSTATE_PATH, path); 1882 values.put(Snapshots.VIEWSTATE_SIZE, size); 1883 return true; 1884 } 1885 1886 public byte[] compressBitmap(Bitmap bitmap) { 1887 if (bitmap == null) { 1888 return null; 1889 } 1890 ByteArrayOutputStream stream = new ByteArrayOutputStream(); 1891 bitmap.compress(CompressFormat.PNG, 100, stream); 1892 return stream.toByteArray(); 1893 } 1894 1895 public void loadUrl(String url, Map<String, String> headers) { 1896 if (mMainView != null) { 1897 mPageLoadProgress = INITIAL_PROGRESS; 1898 mInPageLoad = true; 1899 mCurrentState = new PageState(mContext, false, url, null); 1900 mWebViewController.onPageStarted(this, mMainView, null); 1901 mMainView.loadUrl(url, headers); 1902 } 1903 } 1904 1905 public void disableUrlOverridingForLoad() { 1906 mDisableOverrideUrlLoading = true; 1907 } 1908 1909 protected void capture() { 1910 if (mMainView == null || mCapture == null) return; 1911 if (mMainView.getContentWidth() <= 0 || mMainView.getContentHeight() <= 0) { 1912 return; 1913 } 1914 Canvas c = new Canvas(mCapture); 1915 final int left = mMainView.getScrollX(); 1916 final int top = mMainView.getScrollY() + mMainView.getVisibleTitleHeight(); 1917 int state = c.save(); 1918 c.translate(-left, -top); 1919 float scale = mCaptureWidth / (float) mMainView.getWidth(); 1920 c.scale(scale, scale, left, top); 1921 if (mMainView instanceof BrowserWebView) { 1922 ((BrowserWebView)mMainView).drawContent(c); 1923 } else { 1924 mMainView.draw(c); 1925 } 1926 c.restoreToCount(state); 1927 // manually anti-alias the edges for the tilt 1928 c.drawRect(0, 0, 1, mCapture.getHeight(), sAlphaPaint); 1929 c.drawRect(mCapture.getWidth() - 1, 0, mCapture.getWidth(), 1930 mCapture.getHeight(), sAlphaPaint); 1931 c.drawRect(0, 0, mCapture.getWidth(), 1, sAlphaPaint); 1932 c.drawRect(0, mCapture.getHeight() - 1, mCapture.getWidth(), 1933 mCapture.getHeight(), sAlphaPaint); 1934 c.setBitmap(null); 1935 mHandler.removeMessages(MSG_CAPTURE); 1936 persistThumbnail(); 1937 TabControl tc = mWebViewController.getTabControl(); 1938 if (tc != null) { 1939 OnThumbnailUpdatedListener updateListener 1940 = tc.getOnThumbnailUpdatedListener(); 1941 if (updateListener != null) { 1942 updateListener.onThumbnailUpdated(this); 1943 } 1944 } 1945 } 1946 1947 @Override 1948 public void onNewPicture(WebView view, Picture picture) { 1949 postCapture(); 1950 } 1951 1952 private void postCapture() { 1953 if (!mHandler.hasMessages(MSG_CAPTURE)) { 1954 mHandler.sendEmptyMessageDelayed(MSG_CAPTURE, CAPTURE_DELAY); 1955 } 1956 } 1957 1958 public boolean canGoBack() { 1959 return mMainView != null ? mMainView.canGoBack() : false; 1960 } 1961 1962 public boolean canGoForward() { 1963 return mMainView != null ? mMainView.canGoForward() : false; 1964 } 1965 1966 public void goBack() { 1967 if (mMainView != null) { 1968 mMainView.goBack(); 1969 } 1970 } 1971 1972 public void goForward() { 1973 if (mMainView != null) { 1974 mMainView.goForward(); 1975 } 1976 } 1977 1978 /** 1979 * Causes the tab back/forward stack to be cleared once, if the given URL is the next URL 1980 * to be added to the stack. 1981 * 1982 * This is used to ensure that preloaded URLs that are not subsequently seen by the user do 1983 * not appear in the back stack. 1984 */ 1985 public void clearBackStackWhenItemAdded(Pattern urlPattern) { 1986 mClearHistoryUrlPattern = urlPattern; 1987 } 1988 1989 protected void persistThumbnail() { 1990 DataController.getInstance(mContext).saveThumbnail(this); 1991 } 1992 1993 protected void deleteThumbnail() { 1994 DataController.getInstance(mContext).deleteThumbnail(this); 1995 } 1996 1997 void updateCaptureFromBlob(byte[] blob) { 1998 synchronized (Tab.this) { 1999 if (mCapture == null) { 2000 return; 2001 } 2002 ByteBuffer buffer = ByteBuffer.wrap(blob); 2003 try { 2004 mCapture.copyPixelsFromBuffer(buffer); 2005 } catch (RuntimeException rex) { 2006 Log.e(LOGTAG, "Load capture has mismatched sizes; buffer: " 2007 + buffer.capacity() + " blob: " + blob.length 2008 + "capture: " + mCapture.getByteCount()); 2009 throw rex; 2010 } 2011 } 2012 } 2013 2014 @Override 2015 public String toString() { 2016 StringBuilder builder = new StringBuilder(100); 2017 builder.append(mId); 2018 builder.append(") has parent: "); 2019 if (getParent() != null) { 2020 builder.append("true["); 2021 builder.append(getParent().getId()); 2022 builder.append("]"); 2023 } else { 2024 builder.append("false"); 2025 } 2026 builder.append(", incog: "); 2027 builder.append(isPrivateBrowsingEnabled()); 2028 if (!isPrivateBrowsingEnabled()) { 2029 builder.append(", title: "); 2030 builder.append(getTitle()); 2031 builder.append(", url: "); 2032 builder.append(getUrl()); 2033 } 2034 return builder.toString(); 2035 } 2036 2037 private void handleProceededAfterSslError(SslError error) { 2038 if (error.getUrl().equals(mCurrentState.mUrl)) { 2039 // The security state should currently be SECURITY_STATE_SECURE. 2040 setSecurityState(SecurityState.SECURITY_STATE_BAD_CERTIFICATE); 2041 mCurrentState.mSslCertificateError = error; 2042 } else if (getSecurityState() == SecurityState.SECURITY_STATE_SECURE) { 2043 // The page's main resource is secure and this error is for a 2044 // sub-resource. 2045 setSecurityState(SecurityState.SECURITY_STATE_MIXED); 2046 } 2047 } 2048 } 2049