PageRenderTime 4ms CodeModel.GetById 16ms app.highlight 57ms RepoModel.GetById 1ms app.codeStats 0ms

/WebVox/src/com/marvin/webvox/TabControl.java

http://eyes-free.googlecode.com/
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}