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