Tab.java revision 9861276c209425ef5e92d01f0bf1cc65cf86e382
1/* 2 * Copyright (C) 2009 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.browser; 18 19import java.io.File; 20import java.util.LinkedList; 21import java.util.Vector; 22 23import android.app.AlertDialog; 24import android.content.ContentResolver; 25import android.content.ContentValues; 26import android.content.DialogInterface; 27import android.content.DialogInterface.OnCancelListener; 28import android.database.Cursor; 29import android.database.sqlite.SQLiteDatabase; 30import android.database.sqlite.SQLiteException; 31import android.graphics.Bitmap; 32import android.net.Uri; 33import android.net.http.SslError; 34import android.os.AsyncTask; 35import android.os.Bundle; 36import android.os.Message; 37import android.provider.Browser; 38import android.util.Log; 39import android.view.KeyEvent; 40import android.view.LayoutInflater; 41import android.view.View; 42import android.view.ViewGroup; 43import android.view.View.OnClickListener; 44import android.webkit.CookieSyncManager; 45import android.webkit.GeolocationPermissions; 46import android.webkit.HttpAuthHandler; 47import android.webkit.SslErrorHandler; 48import android.webkit.URLUtil; 49import android.webkit.ValueCallback; 50import android.webkit.WebBackForwardList; 51import android.webkit.WebChromeClient; 52import android.webkit.WebHistoryItem; 53import android.webkit.WebIconDatabase; 54import android.webkit.WebStorage; 55import android.webkit.WebView; 56import android.webkit.WebViewClient; 57import android.widget.FrameLayout; 58import android.widget.ImageButton; 59import android.widget.LinearLayout; 60import android.widget.TextView; 61 62/** 63 * Class for maintaining Tabs with a main WebView and a subwindow. 64 */ 65class Tab { 66 // Log Tag 67 private static final String LOGTAG = "Tab"; 68 // The Geolocation permissions prompt 69 private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt; 70 // Main WebView wrapper 71 private View mContainer; 72 // Main WebView 73 private WebView mMainView; 74 // Subwindow container 75 private View mSubViewContainer; 76 // Subwindow WebView 77 private WebView mSubView; 78 // Saved bundle for when we are running low on memory. It contains the 79 // information needed to restore the WebView if the user goes back to the 80 // tab. 81 private Bundle mSavedState; 82 // Data used when displaying the tab in the picker. 83 private PickerData mPickerData; 84 // Parent Tab. This is the Tab that created this Tab, or null if the Tab was 85 // created by the UI 86 private Tab mParentTab; 87 // Tab that constructed by this Tab. This is used when this Tab is 88 // destroyed, it clears all mParentTab values in the children. 89 private Vector<Tab> mChildTabs; 90 // If true, the tab will be removed when back out of the first page. 91 private boolean mCloseOnExit; 92 // If true, the tab is in the foreground of the current activity. 93 private boolean mInForeground; 94 // If true, the tab is in loading state. 95 private boolean mInLoad; 96 // Application identifier used to find tabs that another application wants 97 // to reuse. 98 private String mAppId; 99 // Keep the original url around to avoid killing the old WebView if the url 100 // has not changed. 101 private String mOriginalUrl; 102 // Error console for the tab 103 private ErrorConsoleView mErrorConsole; 104 // the lock icon type and previous lock icon type for the tab 105 private int mLockIconType; 106 private int mPrevLockIconType; 107 // Inflation service for making subwindows. 108 private final LayoutInflater mInflateService; 109 // The BrowserActivity which owners the Tab 110 private final BrowserActivity mActivity; 111 112 // AsyncTask for downloading touch icons 113 DownloadTouchIcon mTouchIconLoader; 114 115 // Extra saved information for displaying the tab in the picker. 116 private static class PickerData { 117 String mUrl; 118 String mTitle; 119 Bitmap mFavicon; 120 } 121 122 // Used for saving and restoring each Tab 123 static final String WEBVIEW = "webview"; 124 static final String NUMTABS = "numTabs"; 125 static final String CURRTAB = "currentTab"; 126 static final String CURRURL = "currentUrl"; 127 static final String CURRTITLE = "currentTitle"; 128 static final String CURRPICTURE = "currentPicture"; 129 static final String CLOSEONEXIT = "closeonexit"; 130 static final String PARENTTAB = "parentTab"; 131 static final String APPID = "appid"; 132 static final String ORIGINALURL = "originalUrl"; 133 134 // ------------------------------------------------------------------------- 135 136 // Container class for the next error dialog that needs to be displayed 137 private class ErrorDialog { 138 public final int mTitle; 139 public final String mDescription; 140 public final int mError; 141 ErrorDialog(int title, String desc, int error) { 142 mTitle = title; 143 mDescription = desc; 144 mError = error; 145 } 146 }; 147 148 private void processNextError() { 149 if (mQueuedErrors == null) { 150 return; 151 } 152 // The first one is currently displayed so just remove it. 153 mQueuedErrors.removeFirst(); 154 if (mQueuedErrors.size() == 0) { 155 mQueuedErrors = null; 156 return; 157 } 158 showError(mQueuedErrors.getFirst()); 159 } 160 161 private DialogInterface.OnDismissListener mDialogListener = 162 new DialogInterface.OnDismissListener() { 163 public void onDismiss(DialogInterface d) { 164 processNextError(); 165 } 166 }; 167 private LinkedList<ErrorDialog> mQueuedErrors; 168 169 private void queueError(int err, String desc) { 170 if (mQueuedErrors == null) { 171 mQueuedErrors = new LinkedList<ErrorDialog>(); 172 } 173 for (ErrorDialog d : mQueuedErrors) { 174 if (d.mError == err) { 175 // Already saw a similar error, ignore the new one. 176 return; 177 } 178 } 179 ErrorDialog errDialog = new ErrorDialog( 180 err == WebViewClient.ERROR_FILE_NOT_FOUND ? 181 R.string.browserFrameFileErrorLabel : 182 R.string.browserFrameNetworkErrorLabel, 183 desc, err); 184 mQueuedErrors.addLast(errDialog); 185 186 // Show the dialog now if the queue was empty and it is in foreground 187 if (mQueuedErrors.size() == 1 && mInForeground) { 188 showError(errDialog); 189 } 190 } 191 192 private void showError(ErrorDialog errDialog) { 193 if (mInForeground) { 194 AlertDialog d = new AlertDialog.Builder(mActivity) 195 .setTitle(errDialog.mTitle) 196 .setMessage(errDialog.mDescription) 197 .setPositiveButton(R.string.ok, null) 198 .create(); 199 d.setOnDismissListener(mDialogListener); 200 d.show(); 201 } 202 } 203 204 // ------------------------------------------------------------------------- 205 // WebViewClient implementation for the main WebView 206 // ------------------------------------------------------------------------- 207 208 private final WebViewClient mWebViewClient = new WebViewClient() { 209 @Override 210 public void onPageStarted(WebView view, String url, Bitmap favicon) { 211 mInLoad = true; 212 213 // We've started to load a new page. If there was a pending message 214 // to save a screenshot then we will now take the new page and save 215 // an incorrect screenshot. Therefore, remove any pending thumbnail 216 // messages from the queue. 217 mActivity.removeMessages(BrowserActivity.UPDATE_BOOKMARK_THUMBNAIL, 218 view); 219 220 // If we start a touch icon load and then load a new page, we don't 221 // want to cancel the current touch icon loader. But, we do want to 222 // create a new one when the touch icon url is known. 223 if (mTouchIconLoader != null) { 224 mTouchIconLoader.mTab = null; 225 mTouchIconLoader = null; 226 } 227 228 // reset the error console 229 if (mErrorConsole != null) { 230 mErrorConsole.clearErrorMessages(); 231 if (mActivity.shouldShowErrorConsole()) { 232 mErrorConsole.showConsole(ErrorConsoleView.SHOW_NONE); 233 } 234 } 235 236 // update the bookmark database for favicon 237 if (favicon != null) { 238 BrowserBookmarksAdapter.updateBookmarkFavicon(mActivity 239 .getContentResolver(), view.getOriginalUrl(), view 240 .getUrl(), favicon); 241 } 242 243 // reset sync timer to avoid sync starts during loading a page 244 CookieSyncManager.getInstance().resetSync(); 245 246 if (!mActivity.isNetworkUp()) { 247 view.setNetworkAvailable(false); 248 } 249 250 // finally update the UI in the activity if it is in the foreground 251 if (mInForeground) { 252 mActivity.onPageStarted(view, url, favicon); 253 } 254 } 255 256 @Override 257 public void onPageFinished(WebView view, String url) { 258 mInLoad = false; 259 260 if (mInForeground && !mActivity.didUserStopLoading() 261 || !mInForeground) { 262 // Only update the bookmark screenshot if the user did not 263 // cancel the load early. 264 mActivity.postMessage( 265 BrowserActivity.UPDATE_BOOKMARK_THUMBNAIL, 0, 0, view, 266 500); 267 } 268 269 // finally update the UI in the activity if it is in the foreground 270 if (mInForeground) { 271 mActivity.onPageFinished(view, url); 272 } 273 } 274 275 // return true if want to hijack the url to let another app to handle it 276 @Override 277 public boolean shouldOverrideUrlLoading(WebView view, String url) { 278 if (mInForeground) { 279 return mActivity.shouldOverrideUrlLoading(view, url); 280 } else { 281 return false; 282 } 283 } 284 285 /** 286 * Updates the lock icon. This method is called when we discover another 287 * resource to be loaded for this page (for example, javascript). While 288 * we update the icon type, we do not update the lock icon itself until 289 * we are done loading, it is slightly more secure this way. 290 */ 291 @Override 292 public void onLoadResource(WebView view, String url) { 293 if (url != null && url.length() > 0) { 294 // It is only if the page claims to be secure that we may have 295 // to update the lock: 296 if (mLockIconType == BrowserActivity.LOCK_ICON_SECURE) { 297 // If NOT a 'safe' url, change the lock to mixed content! 298 if (!(URLUtil.isHttpsUrl(url) || URLUtil.isDataUrl(url) 299 || URLUtil.isAboutUrl(url))) { 300 mLockIconType = BrowserActivity.LOCK_ICON_MIXED; 301 } 302 } 303 } 304 } 305 306 /** 307 * Show the dialog if it is in the foreground, asking the user if they 308 * would like to continue after an excessive number of HTTP redirects. 309 * Cancel if it is in the background. 310 */ 311 @Override 312 public void onTooManyRedirects(WebView view, final Message cancelMsg, 313 final Message continueMsg) { 314 if (!mInForeground) { 315 cancelMsg.sendToTarget(); 316 return; 317 } 318 new AlertDialog.Builder(mActivity).setTitle( 319 R.string.browserFrameRedirect).setMessage( 320 R.string.browserFrame307Post).setPositiveButton( 321 R.string.ok, new DialogInterface.OnClickListener() { 322 public void onClick(DialogInterface dialog, int which) { 323 continueMsg.sendToTarget(); 324 } 325 }).setNegativeButton(R.string.cancel, 326 new DialogInterface.OnClickListener() { 327 public void onClick(DialogInterface dialog, int which) { 328 cancelMsg.sendToTarget(); 329 } 330 }).setOnCancelListener(new OnCancelListener() { 331 public void onCancel(DialogInterface dialog) { 332 cancelMsg.sendToTarget(); 333 } 334 }).show(); 335 } 336 337 /** 338 * Show a dialog informing the user of the network error reported by 339 * WebCore if it is in the foreground. 340 */ 341 @Override 342 public void onReceivedError(WebView view, int errorCode, 343 String description, String failingUrl) { 344 if (errorCode != WebViewClient.ERROR_HOST_LOOKUP && 345 errorCode != WebViewClient.ERROR_CONNECT && 346 errorCode != WebViewClient.ERROR_BAD_URL && 347 errorCode != WebViewClient.ERROR_UNSUPPORTED_SCHEME && 348 errorCode != WebViewClient.ERROR_FILE) { 349 queueError(errorCode, description); 350 } 351 Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl 352 + " " + description); 353 354 // We need to reset the title after an error if it is in foreground. 355 if (mInForeground) { 356 mActivity.resetTitleAndRevertLockIcon(); 357 } 358 } 359 360 /** 361 * Check with the user if it is ok to resend POST data as the page they 362 * are trying to navigate to is the result of a POST. 363 */ 364 @Override 365 public void onFormResubmission(WebView view, final Message dontResend, 366 final Message resend) { 367 if (!mInForeground) { 368 dontResend.sendToTarget(); 369 return; 370 } 371 new AlertDialog.Builder(mActivity).setTitle( 372 R.string.browserFrameFormResubmitLabel).setMessage( 373 R.string.browserFrameFormResubmitMessage) 374 .setPositiveButton(R.string.ok, 375 new DialogInterface.OnClickListener() { 376 public void onClick(DialogInterface dialog, 377 int which) { 378 resend.sendToTarget(); 379 } 380 }).setNegativeButton(R.string.cancel, 381 new DialogInterface.OnClickListener() { 382 public void onClick(DialogInterface dialog, 383 int which) { 384 dontResend.sendToTarget(); 385 } 386 }).setOnCancelListener(new OnCancelListener() { 387 public void onCancel(DialogInterface dialog) { 388 dontResend.sendToTarget(); 389 } 390 }).show(); 391 } 392 393 /** 394 * Insert the url into the visited history database. 395 * @param url The url to be inserted. 396 * @param isReload True if this url is being reloaded. 397 * FIXME: Not sure what to do when reloading the page. 398 */ 399 @Override 400 public void doUpdateVisitedHistory(WebView view, String url, 401 boolean isReload) { 402 if (url.regionMatches(true, 0, "about:", 0, 6)) { 403 return; 404 } 405 // remove "client" before updating it to the history so that it wont 406 // show up in the auto-complete list. 407 int index = url.indexOf("client=ms-"); 408 if (index > 0 && url.contains(".google.")) { 409 int end = url.indexOf('&', index); 410 if (end > 0) { 411 url = url.substring(0, index) 412 .concat(url.substring(end + 1)); 413 } else { 414 // the url.charAt(index-1) should be either '?' or '&' 415 url = url.substring(0, index-1); 416 } 417 } 418 Browser.updateVisitedHistory(mActivity.getContentResolver(), url, 419 true); 420 WebIconDatabase.getInstance().retainIconForPageUrl(url); 421 } 422 423 /** 424 * Displays SSL error(s) dialog to the user. 425 */ 426 @Override 427 public void onReceivedSslError(final WebView view, 428 final SslErrorHandler handler, final SslError error) { 429 if (!mInForeground) { 430 handler.cancel(); 431 return; 432 } 433 if (BrowserSettings.getInstance().showSecurityWarnings()) { 434 final LayoutInflater factory = 435 LayoutInflater.from(mActivity); 436 final View warningsView = 437 factory.inflate(R.layout.ssl_warnings, null); 438 final LinearLayout placeholder = 439 (LinearLayout)warningsView.findViewById(R.id.placeholder); 440 441 if (error.hasError(SslError.SSL_UNTRUSTED)) { 442 LinearLayout ll = (LinearLayout)factory 443 .inflate(R.layout.ssl_warning, null); 444 ((TextView)ll.findViewById(R.id.warning)) 445 .setText(R.string.ssl_untrusted); 446 placeholder.addView(ll); 447 } 448 449 if (error.hasError(SslError.SSL_IDMISMATCH)) { 450 LinearLayout ll = (LinearLayout)factory 451 .inflate(R.layout.ssl_warning, null); 452 ((TextView)ll.findViewById(R.id.warning)) 453 .setText(R.string.ssl_mismatch); 454 placeholder.addView(ll); 455 } 456 457 if (error.hasError(SslError.SSL_EXPIRED)) { 458 LinearLayout ll = (LinearLayout)factory 459 .inflate(R.layout.ssl_warning, null); 460 ((TextView)ll.findViewById(R.id.warning)) 461 .setText(R.string.ssl_expired); 462 placeholder.addView(ll); 463 } 464 465 if (error.hasError(SslError.SSL_NOTYETVALID)) { 466 LinearLayout ll = (LinearLayout)factory 467 .inflate(R.layout.ssl_warning, null); 468 ((TextView)ll.findViewById(R.id.warning)) 469 .setText(R.string.ssl_not_yet_valid); 470 placeholder.addView(ll); 471 } 472 473 new AlertDialog.Builder(mActivity).setTitle( 474 R.string.security_warning).setIcon( 475 android.R.drawable.ic_dialog_alert).setView( 476 warningsView).setPositiveButton(R.string.ssl_continue, 477 new DialogInterface.OnClickListener() { 478 public void onClick(DialogInterface dialog, 479 int whichButton) { 480 handler.proceed(); 481 } 482 }).setNeutralButton(R.string.view_certificate, 483 new DialogInterface.OnClickListener() { 484 public void onClick(DialogInterface dialog, 485 int whichButton) { 486 mActivity.showSSLCertificateOnError(view, 487 handler, error); 488 } 489 }).setNegativeButton(R.string.cancel, 490 new DialogInterface.OnClickListener() { 491 public void onClick(DialogInterface dialog, 492 int whichButton) { 493 handler.cancel(); 494 mActivity.resetTitleAndRevertLockIcon(); 495 } 496 }).setOnCancelListener( 497 new DialogInterface.OnCancelListener() { 498 public void onCancel(DialogInterface dialog) { 499 handler.cancel(); 500 mActivity.resetTitleAndRevertLockIcon(); 501 } 502 }).show(); 503 } else { 504 handler.proceed(); 505 } 506 } 507 508 /** 509 * Handles an HTTP authentication request. 510 * 511 * @param handler The authentication handler 512 * @param host The host 513 * @param realm The realm 514 */ 515 @Override 516 public void onReceivedHttpAuthRequest(WebView view, 517 final HttpAuthHandler handler, final String host, 518 final String realm) { 519 String username = null; 520 String password = null; 521 522 boolean reuseHttpAuthUsernamePassword = handler 523 .useHttpAuthUsernamePassword(); 524 525 if (reuseHttpAuthUsernamePassword && mMainView != null) { 526 String[] credentials = mMainView.getHttpAuthUsernamePassword( 527 host, realm); 528 if (credentials != null && credentials.length == 2) { 529 username = credentials[0]; 530 password = credentials[1]; 531 } 532 } 533 534 if (username != null && password != null) { 535 handler.proceed(username, password); 536 } else { 537 if (mInForeground) { 538 mActivity.showHttpAuthentication(handler, host, realm, 539 null, null, null, 0); 540 } else { 541 handler.cancel(); 542 } 543 } 544 } 545 546 @Override 547 public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) { 548 if (!mInForeground) { 549 return false; 550 } 551 if (mActivity.isMenuDown()) { 552 // only check shortcut key when MENU is held 553 return mActivity.getWindow().isShortcutKey(event.getKeyCode(), 554 event); 555 } else { 556 return false; 557 } 558 } 559 560 @Override 561 public void onUnhandledKeyEvent(WebView view, KeyEvent event) { 562 if (!mInForeground) { 563 return; 564 } 565 if (event.isDown()) { 566 mActivity.onKeyDown(event.getKeyCode(), event); 567 } else { 568 mActivity.onKeyUp(event.getKeyCode(), event); 569 } 570 } 571 }; 572 573 // ------------------------------------------------------------------------- 574 // WebChromeClient implementation for the main WebView 575 // ------------------------------------------------------------------------- 576 577 private final WebChromeClient mWebChromeClient = new WebChromeClient() { 578 // Helper method to create a new tab or sub window. 579 private void createWindow(final boolean dialog, final Message msg) { 580 WebView.WebViewTransport transport = 581 (WebView.WebViewTransport) msg.obj; 582 if (dialog) { 583 createSubWindow(); 584 mActivity.attachSubWindow(Tab.this); 585 transport.setWebView(mSubView); 586 } else { 587 final Tab newTab = mActivity.openTabAndShow( 588 BrowserActivity.EMPTY_URL_DATA, false, null); 589 if (newTab != Tab.this) { 590 Tab.this.addChildTab(newTab); 591 } 592 transport.setWebView(newTab.getWebView()); 593 } 594 msg.sendToTarget(); 595 } 596 597 @Override 598 public boolean onCreateWindow(WebView view, final boolean dialog, 599 final boolean userGesture, final Message resultMsg) { 600 // only allow new window or sub window for the foreground case 601 if (!mInForeground) { 602 return false; 603 } 604 // Short-circuit if we can't create any more tabs or sub windows. 605 if (dialog && mSubView != null) { 606 new AlertDialog.Builder(mActivity) 607 .setTitle(R.string.too_many_subwindows_dialog_title) 608 .setIcon(android.R.drawable.ic_dialog_alert) 609 .setMessage(R.string.too_many_subwindows_dialog_message) 610 .setPositiveButton(R.string.ok, null) 611 .show(); 612 return false; 613 } else if (!mActivity.getTabControl().canCreateNewTab()) { 614 new AlertDialog.Builder(mActivity) 615 .setTitle(R.string.too_many_windows_dialog_title) 616 .setIcon(android.R.drawable.ic_dialog_alert) 617 .setMessage(R.string.too_many_windows_dialog_message) 618 .setPositiveButton(R.string.ok, null) 619 .show(); 620 return false; 621 } 622 623 // Short-circuit if this was a user gesture. 624 if (userGesture) { 625 createWindow(dialog, resultMsg); 626 return true; 627 } 628 629 // Allow the popup and create the appropriate window. 630 final AlertDialog.OnClickListener allowListener = 631 new AlertDialog.OnClickListener() { 632 public void onClick(DialogInterface d, 633 int which) { 634 createWindow(dialog, resultMsg); 635 } 636 }; 637 638 // Block the popup by returning a null WebView. 639 final AlertDialog.OnClickListener blockListener = 640 new AlertDialog.OnClickListener() { 641 public void onClick(DialogInterface d, int which) { 642 resultMsg.sendToTarget(); 643 } 644 }; 645 646 // Build a confirmation dialog to display to the user. 647 final AlertDialog d = 648 new AlertDialog.Builder(mActivity) 649 .setTitle(R.string.attention) 650 .setIcon(android.R.drawable.ic_dialog_alert) 651 .setMessage(R.string.popup_window_attempt) 652 .setPositiveButton(R.string.allow, allowListener) 653 .setNegativeButton(R.string.block, blockListener) 654 .setCancelable(false) 655 .create(); 656 657 // Show the confirmation dialog. 658 d.show(); 659 return true; 660 } 661 662 @Override 663 public void onCloseWindow(WebView window) { 664 if (mParentTab != null) { 665 // JavaScript can only close popup window. 666 if (mInForeground) { 667 mActivity.switchToTab(mActivity.getTabControl() 668 .getTabIndex(mParentTab)); 669 } 670 mActivity.closeTab(Tab.this); 671 } 672 } 673 674 @Override 675 public void onProgressChanged(WebView view, int newProgress) { 676 if (newProgress == 100) { 677 // sync cookies and cache promptly here. 678 CookieSyncManager.getInstance().sync(); 679 } 680 if (mInForeground) { 681 mActivity.onProgressChanged(view, newProgress); 682 } 683 } 684 685 @Override 686 public void onReceivedTitle(WebView view, String title) { 687 String url = view.getUrl(); 688 if (mInForeground) { 689 // here, if url is null, we want to reset the title 690 mActivity.setUrlTitle(url, title); 691 } 692 if (url == null || 693 url.length() >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) { 694 return; 695 } 696 // See if we can find the current url in our history database and 697 // add the new title to it. 698 if (url.startsWith("http://www.")) { 699 url = url.substring(11); 700 } else if (url.startsWith("http://")) { 701 url = url.substring(4); 702 } 703 try { 704 final ContentResolver cr = mActivity.getContentResolver(); 705 url = "%" + url; 706 String [] selArgs = new String[] { url }; 707 String where = Browser.BookmarkColumns.URL + " LIKE ? AND " 708 + Browser.BookmarkColumns.BOOKMARK + " = 0"; 709 Cursor c = cr.query(Browser.BOOKMARKS_URI, 710 Browser.HISTORY_PROJECTION, where, selArgs, null); 711 if (c.moveToFirst()) { 712 // Current implementation of database only has one entry per 713 // url. 714 ContentValues map = new ContentValues(); 715 map.put(Browser.BookmarkColumns.TITLE, title); 716 cr.update(Browser.BOOKMARKS_URI, map, "_id = " 717 + c.getInt(0), null); 718 } 719 c.close(); 720 } catch (IllegalStateException e) { 721 Log.e(LOGTAG, "Tab onReceived title", e); 722 } catch (SQLiteException ex) { 723 Log.e(LOGTAG, "onReceivedTitle() caught SQLiteException: ", ex); 724 } 725 } 726 727 @Override 728 public void onReceivedIcon(WebView view, Bitmap icon) { 729 if (icon != null) { 730 BrowserBookmarksAdapter.updateBookmarkFavicon(mActivity 731 .getContentResolver(), view.getOriginalUrl(), view 732 .getUrl(), icon); 733 } 734 if (mInForeground) { 735 mActivity.setFavicon(icon); 736 } 737 } 738 739 @Override 740 public void onReceivedTouchIconUrl(WebView view, String url, 741 boolean precomposed) { 742 final ContentResolver cr = mActivity.getContentResolver(); 743 final Cursor c = BrowserBookmarksAdapter.queryBookmarksForUrl(cr, 744 view.getOriginalUrl(), view.getUrl(), true); 745 if (c != null) { 746 if (c.getCount() > 0) { 747 // Let precomposed icons take precedence over non-composed 748 // icons. 749 if (precomposed && mTouchIconLoader != null) { 750 mTouchIconLoader.cancel(false); 751 mTouchIconLoader = null; 752 } 753 // Have only one async task at a time. 754 if (mTouchIconLoader == null) { 755 mTouchIconLoader = new DownloadTouchIcon(Tab.this, cr, 756 c, view); 757 mTouchIconLoader.execute(url); 758 } 759 } else { 760 c.close(); 761 } 762 } 763 } 764 765 @Override 766 public void onShowCustomView(View view, 767 WebChromeClient.CustomViewCallback callback) { 768 if (mInForeground) mActivity.onShowCustomView(view, callback); 769 } 770 771 @Override 772 public void onHideCustomView() { 773 if (mInForeground) mActivity.onHideCustomView(); 774 } 775 776 /** 777 * The origin has exceeded its database quota. 778 * @param url the URL that exceeded the quota 779 * @param databaseIdentifier the identifier of the database on which the 780 * transaction that caused the quota overflow was run 781 * @param currentQuota the current quota for the origin. 782 * @param estimatedSize the estimated size of the database. 783 * @param totalUsedQuota is the sum of all origins' quota. 784 * @param quotaUpdater The callback to run when a decision to allow or 785 * deny quota has been made. Don't forget to call this! 786 */ 787 @Override 788 public void onExceededDatabaseQuota(String url, 789 String databaseIdentifier, long currentQuota, long estimatedSize, 790 long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { 791 BrowserSettings.getInstance().getWebStorageSizeManager() 792 .onExceededDatabaseQuota(url, databaseIdentifier, 793 currentQuota, estimatedSize, totalUsedQuota, 794 quotaUpdater); 795 } 796 797 /** 798 * The Application Cache has exceeded its max size. 799 * @param spaceNeeded is the amount of disk space that would be needed 800 * in order for the last appcache operation to succeed. 801 * @param totalUsedQuota is the sum of all origins' quota. 802 * @param quotaUpdater A callback to inform the WebCore thread that a 803 * new app cache size is available. This callback must always 804 * be executed at some point to ensure that the sleeping 805 * WebCore thread is woken up. 806 */ 807 @Override 808 public void onReachedMaxAppCacheSize(long spaceNeeded, 809 long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { 810 BrowserSettings.getInstance().getWebStorageSizeManager() 811 .onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota, 812 quotaUpdater); 813 } 814 815 /** 816 * Instructs the browser to show a prompt to ask the user to set the 817 * Geolocation permission state for the specified origin. 818 * @param origin The origin for which Geolocation permissions are 819 * requested. 820 * @param callback The callback to call once the user has set the 821 * Geolocation permission state. 822 */ 823 @Override 824 public void onGeolocationPermissionsShowPrompt(String origin, 825 GeolocationPermissions.Callback callback) { 826 if (mInForeground) { 827 mGeolocationPermissionsPrompt.show(origin, callback); 828 } 829 } 830 831 /** 832 * Instructs the browser to hide the Geolocation permissions prompt. 833 */ 834 @Override 835 public void onGeolocationPermissionsHidePrompt() { 836 if (mInForeground) { 837 mGeolocationPermissionsPrompt.hide(); 838 } 839 } 840 841 /* Adds a JavaScript error message to the system log. 842 * @param message The error message to report. 843 * @param lineNumber The line number of the error. 844 * @param sourceID The name of the source file that caused the error. 845 */ 846 @Override 847 public void addMessageToConsole(String message, int lineNumber, 848 String sourceID) { 849 if (mInForeground) { 850 // call getErrorConsole(true) so it will create one if needed 851 ErrorConsoleView errorConsole = getErrorConsole(true); 852 errorConsole.addErrorMessage(message, sourceID, lineNumber); 853 if (mActivity.shouldShowErrorConsole() 854 && errorConsole.getShowState() != ErrorConsoleView.SHOW_MAXIMIZED) { 855 errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED); 856 } 857 } 858 Log.w(LOGTAG, "Console: " + message + " " + sourceID + ":" 859 + lineNumber); 860 } 861 862 /** 863 * Ask the browser for an icon to represent a <video> element. 864 * This icon will be used if the Web page did not specify a poster attribute. 865 * @return Bitmap The icon or null if no such icon is available. 866 */ 867 @Override 868 public Bitmap getDefaultVideoPoster() { 869 if (mInForeground) { 870 return mActivity.getDefaultVideoPoster(); 871 } 872 return null; 873 } 874 875 /** 876 * Ask the host application for a custom progress view to show while 877 * a <video> is loading. 878 * @return View The progress view. 879 */ 880 @Override 881 public View getVideoLoadingProgressView() { 882 if (mInForeground) { 883 return mActivity.getVideoLoadingProgressView(); 884 } 885 return null; 886 } 887 888 @Override 889 public void openFileChooser(ValueCallback<Uri> uploadMsg) { 890 if (mInForeground) { 891 mActivity.openFileChooser(uploadMsg); 892 } else { 893 uploadMsg.onReceiveValue(null); 894 } 895 } 896 897 /** 898 * Deliver a list of already-visited URLs 899 */ 900 @Override 901 public void getVisitedHistory(final ValueCallback<String[]> callback) { 902 AsyncTask<Void, Void, String[]> task = new AsyncTask<Void, Void, String[]>() { 903 public String[] doInBackground(Void... unused) { 904 return Browser.getVisitedHistory(mActivity 905 .getContentResolver()); 906 } 907 public void onPostExecute(String[] result) { 908 callback.onReceiveValue(result); 909 }; 910 }; 911 task.execute(); 912 }; 913 }; 914 915 // ------------------------------------------------------------------------- 916 // WebViewClient implementation for the sub window 917 // ------------------------------------------------------------------------- 918 919 // Subclass of WebViewClient used in subwindows to notify the main 920 // WebViewClient of certain WebView activities. 921 private static class SubWindowClient extends WebViewClient { 922 // The main WebViewClient. 923 private final WebViewClient mClient; 924 925 SubWindowClient(WebViewClient client) { 926 mClient = client; 927 } 928 @Override 929 public void doUpdateVisitedHistory(WebView view, String url, 930 boolean isReload) { 931 mClient.doUpdateVisitedHistory(view, url, isReload); 932 } 933 @Override 934 public boolean shouldOverrideUrlLoading(WebView view, String url) { 935 return mClient.shouldOverrideUrlLoading(view, url); 936 } 937 @Override 938 public void onReceivedSslError(WebView view, SslErrorHandler handler, 939 SslError error) { 940 mClient.onReceivedSslError(view, handler, error); 941 } 942 @Override 943 public void onReceivedHttpAuthRequest(WebView view, 944 HttpAuthHandler handler, String host, String realm) { 945 mClient.onReceivedHttpAuthRequest(view, handler, host, realm); 946 } 947 @Override 948 public void onFormResubmission(WebView view, Message dontResend, 949 Message resend) { 950 mClient.onFormResubmission(view, dontResend, resend); 951 } 952 @Override 953 public void onReceivedError(WebView view, int errorCode, 954 String description, String failingUrl) { 955 mClient.onReceivedError(view, errorCode, description, failingUrl); 956 } 957 @Override 958 public boolean shouldOverrideKeyEvent(WebView view, 959 android.view.KeyEvent event) { 960 return mClient.shouldOverrideKeyEvent(view, event); 961 } 962 @Override 963 public void onUnhandledKeyEvent(WebView view, 964 android.view.KeyEvent event) { 965 mClient.onUnhandledKeyEvent(view, event); 966 } 967 } 968 969 // ------------------------------------------------------------------------- 970 // WebChromeClient implementation for the sub window 971 // ------------------------------------------------------------------------- 972 973 private class SubWindowChromeClient extends WebChromeClient { 974 // The main WebChromeClient. 975 private final WebChromeClient mClient; 976 977 SubWindowChromeClient(WebChromeClient client) { 978 mClient = client; 979 } 980 @Override 981 public void onProgressChanged(WebView view, int newProgress) { 982 mClient.onProgressChanged(view, newProgress); 983 } 984 @Override 985 public boolean onCreateWindow(WebView view, boolean dialog, 986 boolean userGesture, android.os.Message resultMsg) { 987 return mClient.onCreateWindow(view, dialog, userGesture, resultMsg); 988 } 989 @Override 990 public void onCloseWindow(WebView window) { 991 if (window != mSubView) { 992 Log.e(LOGTAG, "Can't close the window"); 993 } 994 mActivity.dismissSubWindow(Tab.this); 995 } 996 } 997 998 // ------------------------------------------------------------------------- 999 1000 // Construct a new tab 1001 Tab(BrowserActivity activity, WebView w, boolean closeOnExit, String appId, 1002 String url) { 1003 mActivity = activity; 1004 mCloseOnExit = closeOnExit; 1005 mAppId = appId; 1006 mOriginalUrl = url; 1007 mLockIconType = BrowserActivity.LOCK_ICON_UNSECURE; 1008 mPrevLockIconType = BrowserActivity.LOCK_ICON_UNSECURE; 1009 mInLoad = false; 1010 mInForeground = false; 1011 1012 mInflateService = LayoutInflater.from(activity); 1013 1014 // The tab consists of a container view, which contains the main 1015 // WebView, as well as any other UI elements associated with the tab. 1016 mContainer = mInflateService.inflate(R.layout.tab, null); 1017 1018 mGeolocationPermissionsPrompt = 1019 (GeolocationPermissionsPrompt) mContainer.findViewById( 1020 R.id.geolocation_permissions_prompt); 1021 1022 setWebView(w); 1023 } 1024 1025 /** 1026 * Sets the WebView for this tab, correctly removing the old WebView from 1027 * the container view. 1028 */ 1029 void setWebView(WebView w) { 1030 if (mMainView == w) { 1031 return; 1032 } 1033 // If the WebView is changing, the page will be reloaded, so any ongoing 1034 // Geolocation permission requests are void. 1035 mGeolocationPermissionsPrompt.hide(); 1036 1037 // Just remove the old one. 1038 FrameLayout wrapper = 1039 (FrameLayout) mContainer.findViewById(R.id.webview_wrapper); 1040 wrapper.removeView(mMainView); 1041 1042 // set the new one 1043 mMainView = w; 1044 // attached the WebViewClient and WebChromeClient 1045 if (mMainView != null) { 1046 mMainView.setWebViewClient(mWebViewClient); 1047 mMainView.setWebChromeClient(mWebChromeClient); 1048 } 1049 } 1050 1051 /** 1052 * Destroy the tab's main WebView and subWindow if any 1053 */ 1054 void destroy() { 1055 if (mMainView != null) { 1056 dismissSubWindow(); 1057 BrowserSettings.getInstance().deleteObserver(mMainView.getSettings()); 1058 // save the WebView to call destroy() after detach it from the tab 1059 WebView webView = mMainView; 1060 setWebView(null); 1061 webView.destroy(); 1062 } 1063 } 1064 1065 /** 1066 * Remove the tab from the parent 1067 */ 1068 void removeFromTree() { 1069 // detach the children 1070 if (mChildTabs != null) { 1071 for(Tab t : mChildTabs) { 1072 t.setParentTab(null); 1073 } 1074 } 1075 // remove itself from the parent list 1076 if (mParentTab != null) { 1077 mParentTab.mChildTabs.remove(this); 1078 } 1079 } 1080 1081 /** 1082 * Create a new subwindow unless a subwindow already exists. 1083 * @return True if a new subwindow was created. False if one already exists. 1084 */ 1085 boolean createSubWindow() { 1086 if (mSubView == null) { 1087 mSubViewContainer = mInflateService.inflate( 1088 R.layout.browser_subwindow, null); 1089 mSubView = (WebView) mSubViewContainer.findViewById(R.id.webview); 1090 // use trackball directly 1091 mSubView.setMapTrackballToArrowKeys(false); 1092 mSubView.setWebViewClient(new SubWindowClient(mWebViewClient)); 1093 mSubView.setWebChromeClient(new SubWindowChromeClient( 1094 mWebChromeClient)); 1095 mSubView.setDownloadListener(mActivity); 1096 mSubView.setOnCreateContextMenuListener(mActivity); 1097 final BrowserSettings s = BrowserSettings.getInstance(); 1098 s.addObserver(mSubView.getSettings()).update(s, null); 1099 final ImageButton cancel = (ImageButton) mSubViewContainer 1100 .findViewById(R.id.subwindow_close); 1101 cancel.setOnClickListener(new OnClickListener() { 1102 public void onClick(View v) { 1103 mSubView.getWebChromeClient().onCloseWindow(mSubView); 1104 } 1105 }); 1106 return true; 1107 } 1108 return false; 1109 } 1110 1111 /** 1112 * Dismiss the subWindow for the tab. 1113 */ 1114 void dismissSubWindow() { 1115 if (mSubView != null) { 1116 BrowserSettings.getInstance().deleteObserver( 1117 mSubView.getSettings()); 1118 mSubView.destroy(); 1119 mSubView = null; 1120 mSubViewContainer = null; 1121 } 1122 } 1123 1124 /** 1125 * Attach the sub window to the content view. 1126 */ 1127 void attachSubWindow(ViewGroup content) { 1128 if (mSubView != null) { 1129 content.addView(mSubViewContainer, 1130 BrowserActivity.COVER_SCREEN_PARAMS); 1131 } 1132 } 1133 1134 /** 1135 * Remove the sub window from the content view. 1136 */ 1137 void removeSubWindow(ViewGroup content) { 1138 if (mSubView != null) { 1139 content.removeView(mSubViewContainer); 1140 } 1141 } 1142 1143 /** 1144 * This method attaches both the WebView and any sub window to the 1145 * given content view. 1146 */ 1147 void attachTabToContentView(ViewGroup content) { 1148 if (mMainView == null) { 1149 return; 1150 } 1151 1152 // Attach the WebView to the container and then attach the 1153 // container to the content view. 1154 FrameLayout wrapper = 1155 (FrameLayout) mContainer.findViewById(R.id.webview_wrapper); 1156 wrapper.addView(mMainView); 1157 content.addView(mContainer, BrowserActivity.COVER_SCREEN_PARAMS); 1158 attachSubWindow(content); 1159 } 1160 1161 /** 1162 * Remove the WebView and any sub window from the given content view. 1163 */ 1164 void removeTabFromContentView(ViewGroup content) { 1165 if (mMainView == null) { 1166 return; 1167 } 1168 1169 // Remove the container from the content and then remove the 1170 // WebView from the container. This will trigger a focus change 1171 // needed by WebView. 1172 FrameLayout wrapper = 1173 (FrameLayout) mContainer.findViewById(R.id.webview_wrapper); 1174 wrapper.removeView(mMainView); 1175 content.removeView(mContainer); 1176 removeSubWindow(content); 1177 } 1178 1179 /** 1180 * Set the parent tab of this tab. 1181 */ 1182 void setParentTab(Tab parent) { 1183 mParentTab = parent; 1184 // This tab may have been freed due to low memory. If that is the case, 1185 // the parent tab index is already saved. If we are changing that index 1186 // (most likely due to removing the parent tab) we must update the 1187 // parent tab index in the saved Bundle. 1188 if (mSavedState != null) { 1189 if (parent == null) { 1190 mSavedState.remove(PARENTTAB); 1191 } else { 1192 mSavedState.putInt(PARENTTAB, mActivity.getTabControl() 1193 .getTabIndex(parent)); 1194 } 1195 } 1196 } 1197 1198 /** 1199 * When a Tab is created through the content of another Tab, then we 1200 * associate the Tabs. 1201 * @param child the Tab that was created from this Tab 1202 */ 1203 void addChildTab(Tab child) { 1204 if (mChildTabs == null) { 1205 mChildTabs = new Vector<Tab>(); 1206 } 1207 mChildTabs.add(child); 1208 child.setParentTab(this); 1209 } 1210 1211 Vector<Tab> getChildTabs() { 1212 return mChildTabs; 1213 } 1214 1215 void resume() { 1216 if (mMainView != null) { 1217 mMainView.onResume(); 1218 if (mSubView != null) { 1219 mSubView.onResume(); 1220 } 1221 } 1222 } 1223 1224 void pause() { 1225 if (mMainView != null) { 1226 mMainView.onPause(); 1227 if (mSubView != null) { 1228 mSubView.onPause(); 1229 } 1230 } 1231 } 1232 1233 void putInForeground() { 1234 mInForeground = true; 1235 resume(); 1236 mMainView.setOnCreateContextMenuListener(mActivity); 1237 if (mSubView != null) { 1238 mSubView.setOnCreateContextMenuListener(mActivity); 1239 } 1240 // Show the pending error dialog if the queue is not empty 1241 if (mQueuedErrors != null && mQueuedErrors.size() > 0) { 1242 showError(mQueuedErrors.getFirst()); 1243 } 1244 } 1245 1246 void putInBackground() { 1247 mInForeground = false; 1248 pause(); 1249 mMainView.setOnCreateContextMenuListener(null); 1250 if (mSubView != null) { 1251 mSubView.setOnCreateContextMenuListener(null); 1252 } 1253 } 1254 1255 /** 1256 * Return the top window of this tab; either the subwindow if it is not 1257 * null or the main window. 1258 * @return The top window of this tab. 1259 */ 1260 WebView getTopWindow() { 1261 if (mSubView != null) { 1262 return mSubView; 1263 } 1264 return mMainView; 1265 } 1266 1267 /** 1268 * Return the main window of this tab. Note: if a tab is freed in the 1269 * background, this can return null. It is only guaranteed to be 1270 * non-null for the current tab. 1271 * @return The main WebView of this tab. 1272 */ 1273 WebView getWebView() { 1274 return mMainView; 1275 } 1276 1277 /** 1278 * Return the subwindow of this tab or null if there is no subwindow. 1279 * @return The subwindow of this tab or null. 1280 */ 1281 WebView getSubWebView() { 1282 return mSubView; 1283 } 1284 1285 /** 1286 * @return The geolocation permissions prompt for this tab. 1287 */ 1288 GeolocationPermissionsPrompt getGeolocationPermissionsPrompt() { 1289 return mGeolocationPermissionsPrompt; 1290 } 1291 1292 /** 1293 * @return The application id string 1294 */ 1295 String getAppId() { 1296 return mAppId; 1297 } 1298 1299 /** 1300 * Set the application id string 1301 * @param id 1302 */ 1303 void setAppId(String id) { 1304 mAppId = id; 1305 } 1306 1307 /** 1308 * @return The original url associated with this Tab 1309 */ 1310 String getOriginalUrl() { 1311 return mOriginalUrl; 1312 } 1313 1314 /** 1315 * Set the original url associated with this tab 1316 */ 1317 void setOriginalUrl(String url) { 1318 mOriginalUrl = url; 1319 } 1320 1321 /** 1322 * Get the url of this tab. Valid after calling populatePickerData, but 1323 * before calling wipePickerData, or if the webview has been destroyed. 1324 * @return The WebView's url or null. 1325 */ 1326 String getUrl() { 1327 if (mPickerData != null) { 1328 return mPickerData.mUrl; 1329 } 1330 return null; 1331 } 1332 1333 /** 1334 * Get the title of this tab. Valid after calling populatePickerData, but 1335 * before calling wipePickerData, or if the webview has been destroyed. If 1336 * the url has no title, use the url instead. 1337 * @return The WebView's title (or url) or null. 1338 */ 1339 String getTitle() { 1340 if (mPickerData != null) { 1341 return mPickerData.mTitle; 1342 } 1343 return null; 1344 } 1345 1346 /** 1347 * Get the favicon of this tab. Valid after calling populatePickerData, but 1348 * before calling wipePickerData, or if the webview has been destroyed. 1349 * @return The WebView's favicon or null. 1350 */ 1351 Bitmap getFavicon() { 1352 if (mPickerData != null) { 1353 return mPickerData.mFavicon; 1354 } 1355 return null; 1356 } 1357 1358 /** 1359 * Return the tab's error console. Creates the console if createIfNEcessary 1360 * is true and we haven't already created the console. 1361 * @param createIfNecessary Flag to indicate if the console should be 1362 * created if it has not been already. 1363 * @return The tab's error console, or null if one has not been created and 1364 * createIfNecessary is false. 1365 */ 1366 ErrorConsoleView getErrorConsole(boolean createIfNecessary) { 1367 if (createIfNecessary && mErrorConsole == null) { 1368 mErrorConsole = new ErrorConsoleView(mActivity); 1369 mErrorConsole.setWebView(mMainView); 1370 } 1371 return mErrorConsole; 1372 } 1373 1374 /** 1375 * If this Tab was created through another Tab, then this method returns 1376 * that Tab. 1377 * @return the Tab parent or null 1378 */ 1379 public Tab getParentTab() { 1380 return mParentTab; 1381 } 1382 1383 /** 1384 * Return whether this tab should be closed when it is backing out of the 1385 * first page. 1386 * @return TRUE if this tab should be closed when exit. 1387 */ 1388 boolean closeOnExit() { 1389 return mCloseOnExit; 1390 } 1391 1392 /** 1393 * Saves the current lock-icon state before resetting the lock icon. If we 1394 * have an error, we may need to roll back to the previous state. 1395 */ 1396 void resetLockIcon(String url) { 1397 mPrevLockIconType = mLockIconType; 1398 mLockIconType = BrowserActivity.LOCK_ICON_UNSECURE; 1399 if (URLUtil.isHttpsUrl(url)) { 1400 mLockIconType = BrowserActivity.LOCK_ICON_SECURE; 1401 } 1402 } 1403 1404 /** 1405 * Reverts the lock-icon state to the last saved state, for example, if we 1406 * had an error, and need to cancel the load. 1407 */ 1408 void revertLockIcon() { 1409 mLockIconType = mPrevLockIconType; 1410 } 1411 1412 /** 1413 * @return The tab's lock icon type. 1414 */ 1415 int getLockIconType() { 1416 return mLockIconType; 1417 } 1418 1419 /** 1420 * @return TRUE if onPageStarted is called while onPageFinished is not 1421 * called yet. 1422 */ 1423 boolean inLoad() { 1424 return mInLoad; 1425 } 1426 1427 // force mInLoad to be false. This should only be called before closing the 1428 // tab to ensure BrowserActivity's pauseWebViewTimers() is called correctly. 1429 void clearInLoad() { 1430 mInLoad = false; 1431 } 1432 1433 void populatePickerData() { 1434 if (mMainView == null) { 1435 populatePickerDataFromSavedState(); 1436 return; 1437 } 1438 1439 // FIXME: The only place we cared about subwindow was for 1440 // bookmarking (i.e. not when saving state). Was this deliberate? 1441 final WebBackForwardList list = mMainView.copyBackForwardList(); 1442 final WebHistoryItem item = list != null ? list.getCurrentItem() : null; 1443 populatePickerData(item); 1444 } 1445 1446 // Populate the picker data using the given history item and the current top 1447 // WebView. 1448 private void populatePickerData(WebHistoryItem item) { 1449 mPickerData = new PickerData(); 1450 if (item != null) { 1451 mPickerData.mUrl = item.getUrl(); 1452 mPickerData.mTitle = item.getTitle(); 1453 mPickerData.mFavicon = item.getFavicon(); 1454 if (mPickerData.mTitle == null) { 1455 mPickerData.mTitle = mPickerData.mUrl; 1456 } 1457 } 1458 } 1459 1460 // Create the PickerData and populate it using the saved state of the tab. 1461 void populatePickerDataFromSavedState() { 1462 if (mSavedState == null) { 1463 return; 1464 } 1465 mPickerData = new PickerData(); 1466 mPickerData.mUrl = mSavedState.getString(CURRURL); 1467 mPickerData.mTitle = mSavedState.getString(CURRTITLE); 1468 } 1469 1470 void clearPickerData() { 1471 mPickerData = null; 1472 } 1473 1474 /** 1475 * Get the saved state bundle. 1476 * @return 1477 */ 1478 Bundle getSavedState() { 1479 return mSavedState; 1480 } 1481 1482 /** 1483 * Set the saved state. 1484 */ 1485 void setSavedState(Bundle state) { 1486 mSavedState = state; 1487 } 1488 1489 /** 1490 * @return TRUE if succeed in saving the state. 1491 */ 1492 boolean saveState() { 1493 // If the WebView is null it means we ran low on memory and we already 1494 // stored the saved state in mSavedState. 1495 if (mMainView == null) { 1496 return mSavedState != null; 1497 } 1498 1499 mSavedState = new Bundle(); 1500 final WebBackForwardList list = mMainView.saveState(mSavedState); 1501 if (list != null) { 1502 final File f = new File(mActivity.getTabControl().getThumbnailDir(), 1503 mMainView.hashCode() + "_pic.save"); 1504 if (mMainView.savePicture(mSavedState, f)) { 1505 mSavedState.putString(CURRPICTURE, f.getPath()); 1506 } 1507 } 1508 1509 // Store some extra info for displaying the tab in the picker. 1510 final WebHistoryItem item = list != null ? list.getCurrentItem() : null; 1511 populatePickerData(item); 1512 1513 if (mPickerData.mUrl != null) { 1514 mSavedState.putString(CURRURL, mPickerData.mUrl); 1515 } 1516 if (mPickerData.mTitle != null) { 1517 mSavedState.putString(CURRTITLE, mPickerData.mTitle); 1518 } 1519 mSavedState.putBoolean(CLOSEONEXIT, mCloseOnExit); 1520 if (mAppId != null) { 1521 mSavedState.putString(APPID, mAppId); 1522 } 1523 if (mOriginalUrl != null) { 1524 mSavedState.putString(ORIGINALURL, mOriginalUrl); 1525 } 1526 // Remember the parent tab so the relationship can be restored. 1527 if (mParentTab != null) { 1528 mSavedState.putInt(PARENTTAB, mActivity.getTabControl().getTabIndex( 1529 mParentTab)); 1530 } 1531 return true; 1532 } 1533 1534 /* 1535 * Restore the state of the tab. 1536 */ 1537 boolean restoreState(Bundle b) { 1538 if (b == null) { 1539 return false; 1540 } 1541 // Restore the internal state even if the WebView fails to restore. 1542 // This will maintain the app id, original url and close-on-exit values. 1543 mSavedState = null; 1544 mPickerData = null; 1545 mCloseOnExit = b.getBoolean(CLOSEONEXIT); 1546 mAppId = b.getString(APPID); 1547 mOriginalUrl = b.getString(ORIGINALURL); 1548 1549 final WebBackForwardList list = mMainView.restoreState(b); 1550 if (list == null) { 1551 return false; 1552 } 1553 if (b.containsKey(CURRPICTURE)) { 1554 final File f = new File(b.getString(CURRPICTURE)); 1555 mMainView.restorePicture(b, f); 1556 f.delete(); 1557 } 1558 return true; 1559 } 1560} 1561