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