/WebVox/src/com/marvin/webvox/TabControl.java
Java | 1324 lines | 828 code | 115 blank | 381 comment | 224 complexity | 27e94971d75802ce457ab33c7bf2d2df MD5 | raw file
1/* 2 * Copyright (C) 2007 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.marvin.webvox; 18 19import com.marvin.webvox.R; 20 21import android.content.Context; 22import android.graphics.Bitmap; 23import android.graphics.Picture; 24//import android.net.http.SslError; 25import android.os.Bundle; 26import android.os.Message; 27import android.util.Log; 28import android.view.Gravity; 29import android.view.LayoutInflater; 30import android.view.View; 31import android.view.ViewGroup; 32import android.view.View.OnClickListener; 33import android.webkit.HttpAuthHandler; 34import android.webkit.JsPromptResult; 35import android.webkit.JsResult; 36import android.webkit.SslErrorHandler; 37import android.webkit.WebBackForwardList; 38import android.webkit.WebChromeClient; 39import android.webkit.WebHistoryItem; 40import android.webkit.WebView; 41import android.webkit.WebViewClient; 42import android.widget.FrameLayout; 43import android.widget.ImageButton; 44import android.widget.LinearLayout; 45 46import java.io.File; 47import java.io.FileInputStream; 48import java.util.ArrayList; 49import java.util.Vector; 50 51class TabControl { 52 // Log Tag 53 private static final String LOGTAG = "TabControl"; 54 // Maximum number of tabs. 55 static final int MAX_TABS = 8; 56 // Static instance of an empty callback. 57 private static final WebViewClient mEmptyClient = 58 new WebViewClient(); 59 // Instance of BackgroundChromeClient for background tabs. 60 private final BackgroundChromeClient mBackgroundChromeClient = 61 new BackgroundChromeClient(); 62 // Private array of WebViews that are used as tabs. 63 private ArrayList<Tab> mTabs = new ArrayList<Tab>(MAX_TABS); 64 // Queue of most recently viewed tabs. 65 private ArrayList<Tab> mTabQueue = new ArrayList<Tab>(MAX_TABS); 66 // Current position in mTabs. 67 private int mCurrentTab = -1; 68 // A private instance of BrowserActivity to interface with when adding and 69 // switching between tabs. 70 private final BrowserActivity mActivity; 71 // Inflation service for making subwindows. 72 private final LayoutInflater mInflateService; 73 // Subclass of WebViewClient used in subwindows to notify the main 74 // WebViewClient of certain WebView activities. 75 private static class SubWindowClient extends WebViewClient { 76 // The main WebViewClient. 77 private final WebViewClient mClient; 78 79 SubWindowClient(WebViewClient client) { 80 mClient = client; 81 } 82 @Override 83 public void doUpdateVisitedHistory(WebView view, String url, 84 boolean isReload) { 85 mClient.doUpdateVisitedHistory(view, url, isReload); 86 } 87 @Override 88 public boolean shouldOverrideUrlLoading(WebView view, String url) { 89 return mClient.shouldOverrideUrlLoading(view, url); 90 } 91// @Override 92// public void onReceivedSslError(WebView view, SslErrorHandler handler, 93// SslError error) { 94// mClient.onReceivedSslError(view, handler, error); 95// } 96 @Override 97 public void onReceivedHttpAuthRequest(WebView view, 98 HttpAuthHandler handler, String host, String realm) { 99 mClient.onReceivedHttpAuthRequest(view, handler, host, realm); 100 } 101 @Override 102 public void onFormResubmission(WebView view, Message dontResend, 103 Message resend) { 104 mClient.onFormResubmission(view, dontResend, resend); 105 } 106 @Override 107 public void onReceivedError(WebView view, int errorCode, 108 String description, String failingUrl) { 109 mClient.onReceivedError(view, errorCode, description, failingUrl); 110 } 111 @Override 112 public boolean shouldOverrideKeyEvent(WebView view, 113 android.view.KeyEvent event) { 114 return mClient.shouldOverrideKeyEvent(view, event); 115 } 116 @Override 117 public void onUnhandledKeyEvent(WebView view, 118 android.view.KeyEvent event) { 119 mClient.onUnhandledKeyEvent(view, event); 120 } 121 } 122 // Subclass of WebChromeClient to display javascript dialogs. 123 private class SubWindowChromeClient extends WebChromeClient { 124 // This subwindow's tab. 125 private final Tab mTab; 126 // The main WebChromeClient. 127 private final WebChromeClient mClient; 128 129 SubWindowChromeClient(Tab t, WebChromeClient client) { 130 mTab = t; 131 mClient = client; 132 } 133 @Override 134 public void onProgressChanged(WebView view, int newProgress) { 135 mClient.onProgressChanged(view, newProgress); 136 } 137 @Override 138 public boolean onCreateWindow(WebView view, boolean dialog, 139 boolean userGesture, android.os.Message resultMsg) { 140 return mClient.onCreateWindow(view, dialog, userGesture, resultMsg); 141 } 142 @Override 143 public void onCloseWindow(WebView window) { 144 if (Browser.DEBUG && window != mTab.mSubView) { 145 throw new AssertionError("Can't close the window"); 146 } 147 mActivity.dismissSubWindow(mTab); 148 } 149 } 150 // Background WebChromeClient for focusing tabs 151 private class BackgroundChromeClient extends WebChromeClient { 152 @Override 153 public void onRequestFocus(WebView view) { 154 Tab t = getTabFromView(view); 155 if (t != getCurrentTab()) { 156 mActivity.switchToTab(getTabIndex(t)); 157 } 158 } 159 } 160 161 // Extra saved information for displaying the tab in the picker. 162 public static class PickerData { 163 String mUrl; 164 String mTitle; 165 Bitmap mFavicon; 166 float mScale; 167 int mScrollX; 168 int mScrollY; 169 } 170 171 /** 172 * Private class for maintaining Tabs with a main WebView and a subwindow. 173 */ 174 public class Tab { 175 // The Geolocation permissions prompt 176 private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt; 177 private View mContainer; 178 // Main WebView 179 private WebView mMainView; 180 // Subwindow WebView 181 private WebView mSubView; 182 // Subwindow container 183 private View mSubViewContainer; 184 // Subwindow callback 185 private SubWindowClient mSubViewClient; 186 // Subwindow chrome callback 187 private SubWindowChromeClient mSubViewChromeClient; 188 // Saved bundle for when we are running low on memory. It contains the 189 // information needed to restore the WebView if the user goes back to 190 // the tab. 191 private Bundle mSavedState; 192 // Data used when displaying the tab in the picker. 193 private PickerData mPickerData; 194 195 // Parent Tab. This is the Tab that created this Tab, or null 196 // if the Tab was created by the UI 197 private Tab mParentTab; 198 // Tab that constructed by this Tab. This is used when this 199 // Tab is destroyed, it clears all mParentTab values in the 200 // children. 201 private Vector<Tab> mChildTabs; 202 203 private Boolean mCloseOnExit; 204 // Application identifier used to find tabs that another application 205 // wants to reuse. 206 private String mAppId; 207 // Keep the original url around to avoid killing the old WebView if the 208 // url has not changed. 209 private String mOriginalUrl; 210 211 private ErrorConsoleView mErrorConsole; 212 // the lock icon type and previous lock icon type for the tab 213 private int mSavedLockIconType; 214 private int mSavedPrevLockIconType; 215 216 // Construct a new tab 217 private Tab(WebView w, boolean closeOnExit, String appId, String url, Context context) { 218 mCloseOnExit = closeOnExit; 219 mAppId = appId; 220 mOriginalUrl = url; 221 mSavedLockIconType = BrowserActivity.LOCK_ICON_UNSECURE; 222 mSavedPrevLockIconType = BrowserActivity.LOCK_ICON_UNSECURE; 223 224 // The tab consists of a container view, which contains the main 225 // WebView, as well as any other UI elements associated with the tab. 226 LayoutInflater factory = LayoutInflater.from(context); 227 mContainer = factory.inflate(R.layout.tab, null); 228 229// mGeolocationPermissionsPrompt = 230// (GeolocationPermissionsPrompt) mContainer.findViewById( 231// R.id.geolocation_permissions_prompt); 232 233 setWebView(w); 234 } 235 236 /** 237 * Sets the WebView for this tab, correctly removing the old WebView 238 * from the container view. 239 */ 240 public void setWebView(WebView w) { 241 if (mMainView == w) { 242 return; 243 } 244 // If the WebView is changing, the page will be reloaded, so any ongoing Geolocation 245 // permission requests are void. 246 //mGeolocationPermissionsPrompt.hide(); 247 248 // Just remove the old one. 249 FrameLayout wrapper = 250 (FrameLayout) mContainer.findViewById(R.id.webview_wrapper); 251 wrapper.removeView(mMainView); 252 mMainView = w; 253 } 254 255 /** 256 * This method attaches both the WebView and any sub window to the 257 * given content view. 258 */ 259 public void attachTabToContentView(ViewGroup content) { 260 if (mMainView == null) { 261 return; 262 } 263 264 // Attach the WebView to the container and then attach the 265 // container to the content view. 266 FrameLayout wrapper = 267 (FrameLayout) mContainer.findViewById(R.id.webview_wrapper); 268 wrapper.addView(mMainView); 269 content.addView(mContainer, BrowserActivity.COVER_SCREEN_PARAMS); 270 attachSubWindow(content); 271 } 272 273 /** 274 * Remove the WebView and any sub window from the given content view. 275 */ 276 public void removeTabFromContentView(ViewGroup content) { 277 if (mMainView == null) { 278 return; 279 } 280 281 // Remove the container from the content and then remove the 282 // WebView from the container. This will trigger a focus change 283 // needed by WebView. 284 FrameLayout wrapper = 285 (FrameLayout) mContainer.findViewById(R.id.webview_wrapper); 286 wrapper.removeView(mMainView); 287 content.removeView(mContainer); 288 removeSubWindow(content); 289 } 290 291 /** 292 * Attach the sub window to the content view. 293 */ 294 public void attachSubWindow(ViewGroup content) { 295 if (mSubView != null) { 296 content.addView(mSubViewContainer, 297 BrowserActivity.COVER_SCREEN_PARAMS); 298 } 299 } 300 301 /** 302 * Remove the sub window from the content view. 303 */ 304 public void removeSubWindow(ViewGroup content) { 305 if (mSubView != null) { 306 content.removeView(mSubViewContainer); 307 } 308 } 309 310 /** 311 * Return the top window of this tab; either the subwindow if it is not 312 * null or the main window. 313 * @return The top window of this tab. 314 */ 315 public WebView getTopWindow() { 316 if (mSubView != null) { 317 return mSubView; 318 } 319 return mMainView; 320 } 321 322 /** 323 * Return the main window of this tab. Note: if a tab is freed in the 324 * background, this can return null. It is only guaranteed to be 325 * non-null for the current tab. 326 * @return The main WebView of this tab. 327 */ 328 public WebView getWebView() { 329 return mMainView; 330 } 331 332 /** 333 * @return The geolocation permissions prompt for this tab. 334 */ 335 public GeolocationPermissionsPrompt getGeolocationPermissionsPrompt() { 336 return mGeolocationPermissionsPrompt; 337 } 338 339 /** 340 * Return the subwindow of this tab or null if there is no subwindow. 341 * @return The subwindow of this tab or null. 342 */ 343 public WebView getSubWebView() { 344 return mSubView; 345 } 346 347 /** 348 * Get the url of this tab. Valid after calling populatePickerData, but 349 * before calling wipePickerData, or if the webview has been destroyed. 350 * 351 * @return The WebView's url or null. 352 */ 353 public String getUrl() { 354 if (mPickerData != null) { 355 return mPickerData.mUrl; 356 } 357 return null; 358 } 359 360 /** 361 * Get the title of this tab. Valid after calling populatePickerData, 362 * but before calling wipePickerData, or if the webview has been 363 * destroyed. If the url has no title, use the url instead. 364 * 365 * @return The WebView's title (or url) or null. 366 */ 367 public String getTitle() { 368 if (mPickerData != null) { 369 return mPickerData.mTitle; 370 } 371 return null; 372 } 373 374 public Bitmap getFavicon() { 375 if (mPickerData != null) { 376 return mPickerData.mFavicon; 377 } 378 return null; 379 } 380 381 private void setParentTab(Tab parent) { 382 mParentTab = parent; 383 // This tab may have been freed due to low memory. If that is the 384 // case, the parent tab index is already saved. If we are changing 385 // that index (most likely due to removing the parent tab) we must 386 // update the parent tab index in the saved Bundle. 387 if (mSavedState != null) { 388 if (parent == null) { 389 mSavedState.remove(PARENTTAB); 390 } else { 391 mSavedState.putInt(PARENTTAB, getTabIndex(parent)); 392 } 393 } 394 } 395 396 /** 397 * When a Tab is created through the content of another Tab, then 398 * we associate the Tabs. 399 * @param child the Tab that was created from this Tab 400 */ 401 public void addChildTab(Tab child) { 402 if (mChildTabs == null) { 403 mChildTabs = new Vector<Tab>(); 404 } 405 mChildTabs.add(child); 406 child.setParentTab(this); 407 } 408 409 private void removeFromTree() { 410 // detach the children 411 if (mChildTabs != null) { 412 for(Tab t : mChildTabs) { 413 t.setParentTab(null); 414 } 415 } 416 417 // Find myself in my parent list 418 if (mParentTab != null) { 419 mParentTab.mChildTabs.remove(this); 420 } 421 } 422 423 /** 424 * If this Tab was created through another Tab, then this method 425 * returns that Tab. 426 * @return the Tab parent or null 427 */ 428 public Tab getParentTab() { 429 return mParentTab; 430 } 431 432 /** 433 * Return whether this tab should be closed when it is backing out of 434 * the first page. 435 * @return TRUE if this tab should be closed when exit. 436 */ 437 public boolean closeOnExit() { 438 return mCloseOnExit; 439 } 440 441 void setLockIconType(int type) { 442 mSavedLockIconType = type; 443 } 444 445 int getLockIconType() { 446 return mSavedLockIconType; 447 } 448 449 void setPrevLockIconType(int type) { 450 mSavedPrevLockIconType = type; 451 } 452 453 int getPrevLockIconType() { 454 return mSavedPrevLockIconType; 455 } 456 }; 457 458 // Directory to store thumbnails for each WebView. 459 private final File mThumbnailDir; 460 461 /** 462 * Construct a new TabControl object that interfaces with the given 463 * BrowserActivity instance. 464 * @param activity A BrowserActivity instance that TabControl will interface 465 * with. 466 */ 467 TabControl(BrowserActivity activity) { 468 mActivity = activity; 469 mInflateService = 470 ((LayoutInflater) activity.getSystemService( 471 Context.LAYOUT_INFLATER_SERVICE)); 472 mThumbnailDir = activity.getDir("thumbnails", 0); 473 } 474 475 File getThumbnailDir() { 476 return mThumbnailDir; 477 } 478 479 BrowserActivity getBrowserActivity() { 480 return mActivity; 481 } 482 483 /** 484 * Return the current tab's main WebView. This will always return the main 485 * WebView for a given tab and not a subwindow. 486 * @return The current tab's WebView. 487 */ 488 WebView getCurrentWebView() { 489 Tab t = getTab(mCurrentTab); 490 if (t == null) { 491 return null; 492 } 493 return t.mMainView; 494 } 495 496 /** 497 * Return the current tab's error console. Creates the console if createIfNEcessary 498 * is true and we haven't already created the console. 499 * @param createIfNecessary Flag to indicate if the console should be created if it has 500 * not been already. 501 * @return The current tab's error console, or null if one has not been created and 502 * createIfNecessary is false. 503 */ 504 ErrorConsoleView getCurrentErrorConsole(boolean createIfNecessary) { 505 Tab t = getTab(mCurrentTab); 506 if (t == null) { 507 return null; 508 } 509 510 if (createIfNecessary && t.mErrorConsole == null) { 511 t.mErrorConsole = new ErrorConsoleView(mActivity); 512 t.mErrorConsole.setWebView(t.mMainView); 513 } 514 515 return t.mErrorConsole; 516 } 517 518 /** 519 * Return the current tab's top-level WebView. This can return a subwindow 520 * if one exists. 521 * @return The top-level WebView of the current tab. 522 */ 523 WebView getCurrentTopWebView() { 524 Tab t = getTab(mCurrentTab); 525 if (t == null) { 526 return null; 527 } 528 return t.mSubView != null ? t.mSubView : t.mMainView; 529 } 530 531 /** 532 * Return the current tab's subwindow if it exists. 533 * @return The subwindow of the current tab or null if it doesn't exist. 534 */ 535 WebView getCurrentSubWindow() { 536 Tab t = getTab(mCurrentTab); 537 if (t == null) { 538 return null; 539 } 540 return t.mSubView; 541 } 542 543 /** 544 * Return the tab at the specified index. 545 * @return The Tab for the specified index or null if the tab does not 546 * exist. 547 */ 548 Tab getTab(int index) { 549 if (index >= 0 && index < mTabs.size()) { 550 return mTabs.get(index); 551 } 552 return null; 553 } 554 555 /** 556 * Return the current tab. 557 * @return The current tab. 558 */ 559 Tab getCurrentTab() { 560 return getTab(mCurrentTab); 561 } 562 563 /** 564 * Return the current tab index. 565 * @return The current tab index 566 */ 567 int getCurrentIndex() { 568 return mCurrentTab; 569 } 570 571 /** 572 * Given a Tab, find it's index 573 * @param Tab to find 574 * @return index of Tab or -1 if not found 575 */ 576 int getTabIndex(Tab tab) { 577 if (tab == null) { 578 return -1; 579 } 580 return mTabs.indexOf(tab); 581 } 582 583 /** 584 * Create a new tab. 585 * @return The newly createTab or null if we have reached the maximum 586 * number of open tabs. 587 */ 588 Tab createNewTab(boolean closeOnExit, String appId, String url) { 589 int size = mTabs.size(); 590 // Return false if we have maxed out on tabs 591 if (MAX_TABS == size) { 592 return null; 593 } 594 final WebView w = createNewWebView(); 595 596 // Create a new tab and add it to the tab list 597 Tab t = new Tab(w, closeOnExit, appId, url, mActivity); 598 mTabs.add(t); 599 // Initially put the tab in the background. 600 putTabInBackground(t); 601 return t; 602 } 603 604 /** 605 * Create a new tab with default values for closeOnExit(false), 606 * appId(null), and url(null). 607 */ 608 Tab createNewTab() { 609 return createNewTab(false, null, null); 610 } 611 612 /** 613 * Remove the tab from the list. If the tab is the current tab shown, the 614 * last created tab will be shown. 615 * @param t The tab to be removed. 616 */ 617 boolean removeTab(Tab t) { 618 if (t == null) { 619 return false; 620 } 621 // Only remove the tab if it is the current one. 622 if (getCurrentTab() == t) { 623 putTabInBackground(t); 624 } 625 626 // Only destroy the WebView if it still exists. 627 if (t.mMainView != null) { 628 // Take down the sub window. 629 dismissSubWindow(t); 630 // Remove the WebView's settings from the BrowserSettings list of 631 // observers. 632 BrowserSettings.getInstance().deleteObserver( 633 t.mMainView.getSettings()); 634 WebView w = t.mMainView; 635 t.setWebView(null); 636 // Destroy the main view 637 w.destroy(); 638 } 639 // clear it's references to parent and children 640 t.removeFromTree(); 641 642 // Remove it from our list of tabs. 643 mTabs.remove(t); 644 645 // The tab indices have shifted, update all the saved state so we point 646 // to the correct index. 647 for (Tab tab : mTabs) { 648 if (tab.mChildTabs != null) { 649 for (Tab child : tab.mChildTabs) { 650 child.setParentTab(tab); 651 } 652 } 653 } 654 655 656 // This tab may have been pushed in to the background and then closed. 657 // If the saved state contains a picture file, delete the file. 658 if (t.mSavedState != null) { 659 if (t.mSavedState.containsKey(CURRPICTURE)) { 660 new File(t.mSavedState.getString(CURRPICTURE)).delete(); 661 } 662 } 663 664 // Remove it from the queue of viewed tabs. 665 mTabQueue.remove(t); 666 mCurrentTab = -1; 667 return true; 668 } 669 670 /** 671 * Clear the back/forward list for all the current tabs. 672 */ 673 void clearHistory() { 674 int size = getTabCount(); 675 for (int i = 0; i < size; i++) { 676 Tab t = mTabs.get(i); 677 // TODO: if a tab is freed due to low memory, its history is not 678 // cleared here. 679 if (t.mMainView != null) { 680 t.mMainView.clearHistory(); 681 } 682 if (t.mSubView != null) { 683 t.mSubView.clearHistory(); 684 } 685 } 686 } 687 688 /** 689 * Destroy all the tabs and subwindows 690 */ 691 void destroy() { 692 BrowserSettings s = BrowserSettings.getInstance(); 693 for (Tab t : mTabs) { 694 if (t.mMainView != null) { 695 dismissSubWindow(t); 696 s.deleteObserver(t.mMainView.getSettings()); 697 WebView w = t.mMainView; 698 t.setWebView(null); 699 w.destroy(); 700 } 701 } 702 mTabs.clear(); 703 mTabQueue.clear(); 704 } 705 706 /** 707 * Returns the number of tabs created. 708 * @return The number of tabs created. 709 */ 710 int getTabCount() { 711 return mTabs.size(); 712 } 713 714 // Used for saving and restoring each Tab 715 private static final String WEBVIEW = "webview"; 716 private static final String NUMTABS = "numTabs"; 717 private static final String CURRTAB = "currentTab"; 718 private static final String CURRURL = "currentUrl"; 719 private static final String CURRTITLE = "currentTitle"; 720 private static final String CURRPICTURE = "currentPicture"; 721 private static final String CLOSEONEXIT = "closeonexit"; 722 private static final String PARENTTAB = "parentTab"; 723 private static final String APPID = "appid"; 724 private static final String ORIGINALURL = "originalUrl"; 725 726 /** 727 * Save the state of all the Tabs. 728 * @param outState The Bundle to save the state to. 729 */ 730 void saveState(Bundle outState) { 731 final int numTabs = getTabCount(); 732 outState.putInt(NUMTABS, numTabs); 733 final int index = getCurrentIndex(); 734 outState.putInt(CURRTAB, (index >= 0 && index < numTabs) ? index : 0); 735 for (int i = 0; i < numTabs; i++) { 736 final Tab t = getTab(i); 737 if (saveState(t)) { 738 outState.putBundle(WEBVIEW + i, t.mSavedState); 739 } 740 } 741 } 742 743 /** 744 * Restore the state of all the tabs. 745 * @param inState The saved state of all the tabs. 746 * @return True if there were previous tabs that were restored. False if 747 * there was no saved state or restoring the state failed. 748 */ 749 boolean restoreState(Bundle inState) { 750 final int numTabs = (inState == null) 751 ? -1 : inState.getInt(NUMTABS, -1); 752 if (numTabs == -1) { 753 return false; 754 } else { 755 final int currentTab = inState.getInt(CURRTAB, -1); 756 for (int i = 0; i < numTabs; i++) { 757 if (i == currentTab) { 758 Tab t = createNewTab(); 759 // Me must set the current tab before restoring the state 760 // so that all the client classes are set. 761 setCurrentTab(t); 762 if (!restoreState(inState.getBundle(WEBVIEW + i), t)) { 763 Log.w(LOGTAG, "Fail in restoreState, load home page."); 764 t.mMainView.loadUrl(BrowserSettings.getInstance() 765 .getHomePage()); 766 } 767 } else { 768 // Create a new tab and don't restore the state yet, add it 769 // to the tab list 770 Tab t = new Tab(null, false, null, null, mActivity); 771 t.mSavedState = inState.getBundle(WEBVIEW + i); 772 if (t.mSavedState != null) { 773 populatePickerDataFromSavedState(t); 774 // Need to maintain the app id and original url so we 775 // can possibly reuse this tab. 776 t.mAppId = t.mSavedState.getString(APPID); 777 t.mOriginalUrl = t.mSavedState.getString(ORIGINALURL); 778 } 779 mTabs.add(t); 780 mTabQueue.add(t); 781 } 782 } 783 // Rebuild the tree of tabs. Do this after all tabs have been 784 // created/restored so that the parent tab exists. 785 for (int i = 0; i < numTabs; i++) { 786 final Bundle b = inState.getBundle(WEBVIEW + i); 787 final Tab t = getTab(i); 788 if (b != null && t != null) { 789 final int parentIndex = b.getInt(PARENTTAB, -1); 790 if (parentIndex != -1) { 791 final Tab parent = getTab(parentIndex); 792 if (parent != null) { 793 parent.addChildTab(t); 794 } 795 } 796 } 797 } 798 } 799 return true; 800 } 801 802 /** 803 * Free the memory in this order, 1) free the background tab; 2) free the 804 * WebView cache; 805 */ 806 void freeMemory() { 807 if (getTabCount() == 0) return; 808 809 // free the least frequently used background tab 810 Tab t = getLeastUsedTab(getCurrentTab()); 811 if (t != null) { 812 Log.w(LOGTAG, "Free a tab in the browser"); 813 freeTab(t); 814 // force a gc 815 System.gc(); 816 return; 817 } 818 819 // free the WebView's unused memory (this includes the cache) 820 Log.w(LOGTAG, "Free WebView's unused memory and cache"); 821 WebView view = getCurrentWebView(); 822 if (view != null) { 823// view.freeMemory(); 824 } 825 // force a gc 826 System.gc(); 827 } 828 829 private Tab getLeastUsedTab(Tab current) { 830 // Don't do anything if we only have 1 tab or if the current tab is 831 // null. 832 if (getTabCount() == 1 || current == null) { 833 return null; 834 } 835 836 // Rip through the queue starting at the beginning and teardown the 837 // next available tab. 838 Tab t = null; 839 int i = 0; 840 final int queueSize = mTabQueue.size(); 841 if (queueSize == 0) { 842 return null; 843 } 844 do { 845 t = mTabQueue.get(i++); 846 } while (i < queueSize 847 && ((t != null && t.mMainView == null) 848 || t == current.mParentTab)); 849 850 // Don't do anything if the last remaining tab is the current one or if 851 // the last tab has been freed already. 852 if (t == current || t.mMainView == null) { 853 return null; 854 } 855 856 return t; 857 } 858 859 private void freeTab(Tab t) { 860 // Store the WebView's state. 861 saveState(t); 862 863 // Tear down the tab. 864 dismissSubWindow(t); 865 // Remove the WebView's settings from the BrowserSettings list of 866 // observers. 867 BrowserSettings.getInstance().deleteObserver(t.mMainView.getSettings()); 868 WebView w = t.mMainView; 869 t.setWebView(null); 870 w.destroy(); 871 } 872 873 /** 874 * Create a new subwindow unless a subwindow already exists. 875 * @return True if a new subwindow was created. False if one already exists. 876 */ 877 void createSubWindow() { 878 Tab t = getTab(mCurrentTab); 879 if (t != null && t.mSubView == null) { 880 final View v = mInflateService.inflate(R.layout.browser_subwindow, null); 881 final WebView w = (WebView) v.findViewById(R.id.webview); 882 w.setMapTrackballToArrowKeys(false); // use trackball directly 883 final SubWindowClient subClient = 884 new SubWindowClient(mActivity.getWebViewClient()); 885 final SubWindowChromeClient subChromeClient = 886 new SubWindowChromeClient(t, 887 mActivity.getWebChromeClient()); 888 w.setWebViewClient(subClient); 889 w.setWebChromeClient(subChromeClient); 890 w.setDownloadListener(mActivity); 891 w.setOnCreateContextMenuListener(mActivity); 892 final BrowserSettings s = BrowserSettings.getInstance(); 893 s.addObserver(w.getSettings()).update(s, null); 894 t.mSubView = w; 895 t.mSubViewClient = subClient; 896 t.mSubViewChromeClient = subChromeClient; 897 // FIXME: I really hate having to know the name of the view 898 // containing the webview. 899 t.mSubViewContainer = v.findViewById(R.id.subwindow_container); 900 final ImageButton cancel = 901 (ImageButton) v.findViewById(R.id.subwindow_close); 902 cancel.setOnClickListener(new OnClickListener() { 903 public void onClick(View v) { 904 subChromeClient.onCloseWindow(w); 905 } 906 }); 907 } 908 } 909 910 /** 911 * Show the tab that contains the given WebView. 912 * @param view The WebView used to find the tab. 913 */ 914 Tab getTabFromView(WebView view) { 915 final int size = getTabCount(); 916 for (int i = 0; i < size; i++) { 917 final Tab t = getTab(i); 918 if (t.mSubView == view || t.mMainView == view) { 919 return t; 920 } 921 } 922 return null; 923 } 924 925 /** 926 * Return the tab with the matching application id. 927 * @param id The application identifier. 928 */ 929 Tab getTabFromId(String id) { 930 if (id == null) { 931 return null; 932 } 933 final int size = getTabCount(); 934 for (int i = 0; i < size; i++) { 935 final Tab t = getTab(i); 936 if (id.equals(t.mAppId)) { 937 return t; 938 } 939 } 940 return null; 941 } 942 943 // This method checks if a non-app tab (one created within the browser) 944 // matches the given url. 945 private boolean tabMatchesUrl(Tab t, String url) { 946 if (t.mAppId != null) { 947 return false; 948 } else if (t.mMainView == null) { 949 return false; 950 } else if (url.equals(t.mMainView.getUrl()) || 951 url.equals(t.mMainView.getOriginalUrl())) { 952 return true; 953 } 954 return false; 955 } 956 957 /** 958 * Return the tab that has no app id associated with it and the url of the 959 * tab matches the given url. 960 * @param url The url to search for. 961 */ 962 Tab findUnusedTabWithUrl(String url) { 963 if (url == null) { 964 return null; 965 } 966 // Check the current tab first. 967 Tab t = getCurrentTab(); 968 if (t != null && tabMatchesUrl(t, url)) { 969 return t; 970 } 971 // Now check all the rest. 972 final int size = getTabCount(); 973 for (int i = 0; i < size; i++) { 974 t = getTab(i); 975 if (tabMatchesUrl(t, url)) { 976 return t; 977 } 978 } 979 return null; 980 } 981 982 /** 983 * Recreate the main WebView of the given tab. Returns true if the WebView 984 * was deleted. 985 */ 986 boolean recreateWebView(Tab t, String url) { 987 final WebView w = t.mMainView; 988 if (w != null) { 989 if (url != null && url.equals(t.mOriginalUrl)) { 990 // The original url matches the current url. Just go back to the 991 // first history item so we can load it faster than if we 992 // rebuilt the WebView. 993 final WebBackForwardList list = w.copyBackForwardList(); 994 if (list != null) { 995 w.goBackOrForward(-list.getCurrentIndex()); 996 w.clearHistory(); // maintains the current page. 997 return false; 998 } 999 } 1000 // Remove the settings object from the global settings and destroy 1001 // the WebView. 1002 BrowserSettings.getInstance().deleteObserver( 1003 t.mMainView.getSettings()); 1004 t.mMainView.destroy(); 1005 } 1006 // Create a new WebView. If this tab is the current tab, we need to put 1007 // back all the clients so force it to be the current tab. 1008 t.setWebView(createNewWebView()); 1009 if (getCurrentTab() == t) { 1010 setCurrentTab(t, true); 1011 } 1012 // Clear the saved state except for the app id and close-on-exit 1013 // values. 1014 t.mSavedState = null; 1015 t.mPickerData = null; 1016 // Save the new url in order to avoid deleting the WebView. 1017 t.mOriginalUrl = url; 1018 return true; 1019 } 1020 1021 /** 1022 * Creates a new WebView and registers it with the global settings. 1023 */ 1024 private WebView createNewWebView() { 1025 // Create a new WebView 1026 WebView w = new WebView(mActivity); 1027 w.setScrollbarFadingEnabled(true); 1028 w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY); 1029 w.setMapTrackballToArrowKeys(false); // use trackball directly 1030 // Enable the built-in zoom 1031 w.getSettings().setBuiltInZoomControls(true); 1032 // Add this WebView to the settings observer list and update the 1033 // settings 1034 final BrowserSettings s = BrowserSettings.getInstance(); 1035 s.addObserver(w.getSettings()).update(s, null); 1036 return w; 1037 } 1038 1039 /** 1040 * Put the current tab in the background and set newTab as the current tab. 1041 * @param newTab The new tab. If newTab is null, the current tab is not 1042 * set. 1043 */ 1044 boolean setCurrentTab(Tab newTab) { 1045 return setCurrentTab(newTab, false); 1046 } 1047 1048 /*package*/ void pauseCurrentTab() { 1049 Tab t = getCurrentTab(); 1050 if (t != null) { 1051// t.mMainView.onPause(); 1052 if (t.mSubView != null) { 1053// t.mSubView.onPause(); 1054 } 1055 } 1056 } 1057 1058 /*package*/ void resumeCurrentTab() { 1059 Tab t = getCurrentTab(); 1060 if (t != null) { 1061// t.mMainView.onResume(); 1062 if (t.mSubView != null) { 1063// t.mSubView.onResume(); 1064 } 1065 } 1066 } 1067 1068 private void putViewInForeground(WebView v, WebViewClient vc, 1069 WebChromeClient cc) { 1070 v.setWebViewClient(vc); 1071 v.setWebChromeClient(cc); 1072 v.setOnCreateContextMenuListener(mActivity); 1073 v.setDownloadListener(mActivity); 1074// v.onResume(); 1075 } 1076 1077 private void putViewInBackground(WebView v) { 1078 // Set an empty callback so that default actions are not triggered. 1079 v.setWebViewClient(mEmptyClient); 1080 v.setWebChromeClient(mBackgroundChromeClient); 1081 v.setOnCreateContextMenuListener(null); 1082 // Leave the DownloadManager attached so that downloads can start in 1083 // a non-active window. This can happen when going to a site that does 1084 // a redirect after a period of time. The user could have switched to 1085 // another tab while waiting for the download to start. 1086 v.setDownloadListener(mActivity); 1087// v.onPause(); 1088 } 1089 1090 /** 1091 * If force is true, this method skips the check for newTab == current. 1092 */ 1093 private boolean setCurrentTab(Tab newTab, boolean force) { 1094 Tab current = getTab(mCurrentTab); 1095 if (current == newTab && !force) { 1096 return true; 1097 } 1098 if (current != null) { 1099 // Remove the current WebView and the container of the subwindow 1100 putTabInBackground(current); 1101 } 1102 1103 if (newTab == null) { 1104 return false; 1105 } 1106 1107 // Move the newTab to the end of the queue 1108 int index = mTabQueue.indexOf(newTab); 1109 if (index != -1) { 1110 mTabQueue.remove(index); 1111 } 1112 mTabQueue.add(newTab); 1113 1114 WebView mainView; 1115 1116 // Display the new current tab 1117 mCurrentTab = mTabs.indexOf(newTab); 1118 mainView = newTab.mMainView; 1119 boolean needRestore = (mainView == null); 1120 if (needRestore) { 1121 // Same work as in createNewTab() except don't do new Tab() 1122 mainView = createNewWebView(); 1123 newTab.setWebView(mainView); 1124 } 1125 putViewInForeground(mainView, mActivity.getWebViewClient(), 1126 mActivity.getWebChromeClient()); 1127 // Add the subwindow if it exists 1128 if (newTab.mSubViewContainer != null) { 1129 putViewInForeground(newTab.mSubView, newTab.mSubViewClient, 1130 newTab.mSubViewChromeClient); 1131 } 1132 if (needRestore) { 1133 // Have to finish setCurrentTab work before calling restoreState 1134 if (!restoreState(newTab.mSavedState, newTab)) { 1135 mainView.loadUrl(BrowserSettings.getInstance().getHomePage()); 1136 } 1137 } 1138 return true; 1139 } 1140 1141 /* 1142 * Put the tab in the background using all the empty/background clients. 1143 */ 1144 private void putTabInBackground(Tab t) { 1145 putViewInBackground(t.mMainView); 1146 if (t.mSubView != null) { 1147 putViewInBackground(t.mSubView); 1148 } 1149 } 1150 1151 /* 1152 * Dismiss the subwindow for the given tab. 1153 */ 1154 void dismissSubWindow(Tab t) { 1155 if (t != null && t.mSubView != null) { 1156 BrowserSettings.getInstance().deleteObserver( 1157 t.mSubView.getSettings()); 1158 t.mSubView.destroy(); 1159 t.mSubView = null; 1160 t.mSubViewContainer = null; 1161 } 1162 } 1163 1164 /** 1165 * Ensure that Tab t has data to display in the tab picker. 1166 * @param t Tab to populate. 1167 */ 1168 /* package */ void populatePickerData(Tab t) { 1169 if (t == null) { 1170 return; 1171 } 1172 1173 // mMainView == null indicates that the tab has been freed. 1174 if (t.mMainView == null) { 1175 populatePickerDataFromSavedState(t); 1176 return; 1177 } 1178 1179 // FIXME: The only place we cared about subwindow was for 1180 // bookmarking (i.e. not when saving state). Was this deliberate? 1181 final WebBackForwardList list = t.mMainView.copyBackForwardList(); 1182 final WebHistoryItem item = 1183 list != null ? list.getCurrentItem() : null; 1184 populatePickerData(t, item); 1185 } 1186 1187 // Create the PickerData and populate it using the saved state of the tab. 1188 private void populatePickerDataFromSavedState(Tab t) { 1189 if (t.mSavedState == null) { 1190 return; 1191 } 1192 1193 final PickerData data = new PickerData(); 1194 final Bundle state = t.mSavedState; 1195 data.mUrl = state.getString(CURRURL); 1196 data.mTitle = state.getString(CURRTITLE); 1197 // XXX: These keys are from WebView.savePicture so if they change, this 1198 // will break. 1199 data.mScale = state.getFloat("scale", 1.0f); 1200 data.mScrollX = state.getInt("scrollX", 0); 1201 data.mScrollY = state.getInt("scrollY", 0); 1202 1203 // Set the tab's picker data. 1204 t.mPickerData = data; 1205 } 1206 1207 // Populate the picker data using the given history item and the current 1208 // top WebView. 1209 private void populatePickerData(Tab t, WebHistoryItem item) { 1210 final PickerData data = new PickerData(); 1211 if (item != null) { 1212 data.mUrl = item.getUrl(); 1213 data.mTitle = item.getTitle(); 1214 data.mFavicon = item.getFavicon(); 1215 if (data.mTitle == null) { 1216 data.mTitle = data.mUrl; 1217 } 1218 } 1219 // We want to display the top window in the tab picker but use the url 1220 // and title of the main window. 1221 final WebView w = t.getTopWindow(); 1222 data.mScale = w.getScale(); 1223 data.mScrollX = w.getScrollX(); 1224 data.mScrollY = w.getScrollY(); 1225 1226 t.mPickerData = data; 1227 } 1228 1229 /** 1230 * Clean up the data for all tabs. 1231 */ 1232 /* package */ void wipeAllPickerData() { 1233 int size = getTabCount(); 1234 for (int i = 0; i < size; i++) { 1235 final Tab t = getTab(i); 1236 if (t != null && t.mSavedState == null) { 1237 t.mPickerData = null; 1238 } 1239 } 1240 } 1241 1242 /* 1243 * Save the state for an individual tab. 1244 */ 1245 private boolean saveState(Tab t) { 1246 if (t != null) { 1247 final WebView w = t.mMainView; 1248 // If the WebView is null it means we ran low on memory and we 1249 // already stored the saved state in mSavedState. 1250 if (w == null) { 1251 return true; 1252 } 1253 final Bundle b = new Bundle(); 1254 final WebBackForwardList list = w.saveState(b); 1255 if (list != null) { 1256 final File f = new File(mThumbnailDir, w.hashCode() 1257 + "_pic.save"); 1258 if (w.savePicture(b, f)) { 1259 b.putString(CURRPICTURE, f.getPath()); 1260 } 1261 } 1262 1263 // Store some extra info for displaying the tab in the picker. 1264 final WebHistoryItem item = 1265 list != null ? list.getCurrentItem() : null; 1266 populatePickerData(t, item); 1267 1268 // XXX: WebView.savePicture stores the scale and scroll positions 1269 // in the bundle so we don't have to do it here. 1270 final PickerData data = t.mPickerData; 1271 if (data.mUrl != null) { 1272 b.putString(CURRURL, data.mUrl); 1273 } 1274 if (data.mTitle != null) { 1275 b.putString(CURRTITLE, data.mTitle); 1276 } 1277 b.putBoolean(CLOSEONEXIT, t.mCloseOnExit); 1278 if (t.mAppId != null) { 1279 b.putString(APPID, t.mAppId); 1280 } 1281 if (t.mOriginalUrl != null) { 1282 b.putString(ORIGINALURL, t.mOriginalUrl); 1283 } 1284 1285 // Remember the parent tab so the relationship can be restored. 1286 if (t.mParentTab != null) { 1287 b.putInt(PARENTTAB, getTabIndex(t.mParentTab)); 1288 } 1289 1290 // Remember the saved state. 1291 t.mSavedState = b; 1292 return true; 1293 } 1294 return false; 1295 } 1296 1297 /* 1298 * Restore the state of the tab. 1299 */ 1300 private boolean restoreState(Bundle b, Tab t) { 1301 if (b == null) { 1302 return false; 1303 } 1304 // Restore the internal state even if the WebView fails to restore. 1305 // This will maintain the app id, original url and close-on-exit values. 1306 t.mSavedState = null; 1307 t.mPickerData = null; 1308 t.mCloseOnExit = b.getBoolean(CLOSEONEXIT); 1309 t.mAppId = b.getString(APPID); 1310 t.mOriginalUrl = b.getString(ORIGINALURL); 1311 1312 final WebView w = t.mMainView; 1313 final WebBackForwardList list = w.restoreState(b); 1314 if (list == null) { 1315 return false; 1316 } 1317 if (b.containsKey(CURRPICTURE)) { 1318 final File f = new File(b.getString(CURRPICTURE)); 1319 w.restorePicture(b, f); 1320 f.delete(); 1321 } 1322 return true; 1323 } 1324}