TabControl.java revision bff2d603c022691237c31d9a57ad8c217c6e7e11
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        // free the least frequently used background tab
702        Tab t = getLeastUsedTab();
703        if (t != null) {
704            Log.w(LOGTAG, "Free a tab in the browser");
705            freeTab(t);
706            // force a gc
707            System.gc();
708            return;
709        }
710
711        // free the WebView's unused memory (this includes the cache)
712        Log.w(LOGTAG, "Free WebView's unused memory and cache");
713        WebView view = getCurrentWebView();
714        if (view != null) {
715            view.freeMemory();
716        }
717        // force a gc
718        System.gc();
719    }
720
721    private Tab getLeastUsedTab() {
722        // Don't do anything if we only have 1 tab.
723        if (getTabCount() == 1) {
724            return null;
725        }
726
727        // Rip through the queue starting at the beginning and teardown the
728        // next available tab.
729        Tab t = null;
730        int i = 0;
731        final int queueSize = mTabQueue.size();
732        if (queueSize == 0) {
733            return null;
734        }
735        do {
736            t = mTabQueue.get(i++);
737        } while (i < queueSize && t != null && t.mMainView == null);
738
739        // Don't do anything if the last remaining tab is the current one or if
740        // the last tab has been freed already.
741        if (t == getCurrentTab() || t.mMainView == null) {
742            return null;
743        }
744
745        return t;
746    }
747
748    private void freeTab(Tab t) {
749        // Store the WebView's state.
750        saveState(t);
751
752        // Tear down the tab.
753        dismissSubWindow(t);
754        // Remove the WebView's settings from the BrowserSettings list of
755        // observers.
756        BrowserSettings.getInstance().deleteObserver(t.mMainView.getSettings());
757        t.mMainView.destroy();
758        t.mMainView = null;
759    }
760
761    /**
762     * Create a new subwindow unless a subwindow already exists.
763     * @return True if a new subwindow was created. False if one already exists.
764     */
765    void createSubWindow() {
766        Tab t = getTab(mCurrentTab);
767        if (t != null && t.mSubView == null) {
768            final View v = mInflateService.inflate(R.layout.browser_subwindow, null);
769            final WebView w = (WebView) v.findViewById(R.id.webview);
770            w.setMapTrackballToArrowKeys(false); // use trackball directly
771            final SubWindowClient subClient =
772                    new SubWindowClient(mActivity.getWebViewClient());
773            final SubWindowChromeClient subChromeClient =
774                    new SubWindowChromeClient(t,
775                            mActivity.getWebChromeClient());
776            w.setWebViewClient(subClient);
777            w.setWebChromeClient(subChromeClient);
778            w.setDownloadListener(mActivity);
779            w.setOnCreateContextMenuListener(mActivity);
780            final BrowserSettings s = BrowserSettings.getInstance();
781            s.addObserver(w.getSettings()).update(s, null);
782            t.mSubView = w;
783            t.mSubViewClient = subClient;
784            t.mSubViewChromeClient = subChromeClient;
785            // FIXME: I really hate having to know the name of the view
786            // containing the webview.
787            t.mSubViewContainer = v.findViewById(R.id.subwindow_container);
788            final ImageButton cancel =
789                    (ImageButton) v.findViewById(R.id.subwindow_close);
790            cancel.setOnClickListener(new OnClickListener() {
791                    public void onClick(View v) {
792                        subChromeClient.onCloseWindow(w);
793                    }
794                });
795        }
796    }
797
798    /**
799     * Show the tab that contains the given WebView.
800     * @param view The WebView used to find the tab.
801     */
802    Tab getTabFromView(WebView view) {
803        final int size = getTabCount();
804        for (int i = 0; i < size; i++) {
805            final Tab t = getTab(i);
806            if (t.mSubView == view || t.mMainView == view) {
807                return t;
808            }
809        }
810        return null;
811    }
812
813    /**
814     * Return the tab with the matching application id.
815     * @param id The application identifier.
816     */
817    Tab getTabFromId(String id) {
818        if (id == null) {
819            return null;
820        }
821        final int size = getTabCount();
822        for (int i = 0; i < size; i++) {
823            final Tab t = getTab(i);
824            if (id.equals(t.mAppId)) {
825                return t;
826            }
827        }
828        return null;
829    }
830
831    // This method checks if a non-app tab (one created within the browser)
832    // matches the given url.
833    private boolean tabMatchesUrl(Tab t, String url) {
834        if (t.mAppId != null) {
835            return false;
836        } else if (t.mMainView == null) {
837            return false;
838        } else if (url.equals(t.mMainView.getUrl()) ||
839                url.equals(t.mMainView.getOriginalUrl())) {
840            return true;
841        }
842        return false;
843    }
844
845    /**
846     * Return the tab that has no app id associated with it and the url of the
847     * tab matches the given url.
848     * @param url The url to search for.
849     */
850    Tab findUnusedTabWithUrl(String url) {
851        if (url == null) {
852            return null;
853        }
854        // Check the current tab first.
855        Tab t = getCurrentTab();
856        if (t != null && tabMatchesUrl(t, url)) {
857            return t;
858        }
859        // Now check all the rest.
860        final int size = getTabCount();
861        for (int i = 0; i < size; i++) {
862            t = getTab(i);
863            if (tabMatchesUrl(t, url)) {
864                return t;
865            }
866        }
867        return null;
868    }
869
870    /**
871     * Recreate the main WebView of the given tab. Returns true if the WebView
872     * was deleted.
873     */
874    boolean recreateWebView(Tab t, String url) {
875        final WebView w = t.mMainView;
876        if (w != null) {
877            if (url != null && url.equals(t.mOriginalUrl)) {
878                // The original url matches the current url. Just go back to the
879                // first history item so we can load it faster than if we
880                // rebuilt the WebView.
881                final WebBackForwardList list = w.copyBackForwardList();
882                if (list != null) {
883                    w.goBackOrForward(-list.getCurrentIndex());
884                    w.clearHistory(); // maintains the current page.
885                    return false;
886                }
887            }
888            // Remove the settings object from the global settings and destroy
889            // the WebView.
890            BrowserSettings.getInstance().deleteObserver(
891                    t.mMainView.getSettings());
892            t.mMainView.destroy();
893        }
894        // Create a new WebView. If this tab is the current tab, we need to put
895        // back all the clients so force it to be the current tab.
896        t.mMainView = createNewWebView();
897        if (getCurrentTab() == t) {
898            setCurrentTab(t, true);
899        }
900        // Clear the saved state except for the app id and close-on-exit
901        // values.
902        t.mSavedState = null;
903        t.mPickerData = null;
904        // Save the new url in order to avoid deleting the WebView.
905        t.mOriginalUrl = url;
906        return true;
907    }
908
909    /**
910     * Creates a new WebView and registers it with the global settings.
911     */
912    private WebView createNewWebView() {
913        // Create a new WebView
914        WebView w = new WebView(mActivity);
915        w.setMapTrackballToArrowKeys(false); // use trackball directly
916        // Enable the built-in zoom
917        w.getSettings().setBuiltInZoomControls(true);
918        // Add this WebView to the settings observer list and update the
919        // settings
920        final BrowserSettings s = BrowserSettings.getInstance();
921        s.addObserver(w.getSettings()).update(s, null);
922        return w;
923    }
924
925    /**
926     * Put the current tab in the background and set newTab as the current tab.
927     * @param newTab The new tab. If newTab is null, the current tab is not
928     *               set.
929     */
930    boolean setCurrentTab(Tab newTab) {
931        return setCurrentTab(newTab, false);
932    }
933
934    /*package*/ void pauseCurrentTab() {
935        Tab t = getCurrentTab();
936        if (t != null) {
937            t.mMainView.onPause();
938            if (t.mSubView != null) {
939                t.mSubView.onPause();
940            }
941        }
942    }
943
944    /*package*/ void resumeCurrentTab() {
945        Tab t = getCurrentTab();
946        if (t != null) {
947            t.mMainView.onResume();
948            if (t.mSubView != null) {
949                t.mSubView.onResume();
950            }
951        }
952    }
953
954    private void putViewInForeground(WebView v, WebViewClient vc,
955                                     WebChromeClient cc) {
956        v.setWebViewClient(vc);
957        v.setWebChromeClient(cc);
958        v.setOnCreateContextMenuListener(mActivity);
959        v.setDownloadListener(mActivity);
960        v.onResume();
961    }
962
963    private void putViewInBackground(WebView v) {
964        // Set an empty callback so that default actions are not triggered.
965        v.setWebViewClient(mEmptyClient);
966        v.setWebChromeClient(mBackgroundChromeClient);
967        v.setOnCreateContextMenuListener(null);
968        // Leave the DownloadManager attached so that downloads can start in
969        // a non-active window. This can happen when going to a site that does
970        // a redirect after a period of time. The user could have switched to
971        // another tab while waiting for the download to start.
972        v.setDownloadListener(mActivity);
973        v.onPause();
974    }
975
976    /**
977     * If force is true, this method skips the check for newTab == current.
978     */
979    private boolean setCurrentTab(Tab newTab, boolean force) {
980        Tab current = getTab(mCurrentTab);
981        if (current == newTab && !force) {
982            return true;
983        }
984        if (current != null) {
985            // Remove the current WebView and the container of the subwindow
986            putTabInBackground(current);
987        }
988
989        if (newTab == null) {
990            return false;
991        }
992
993        // Move the newTab to the end of the queue
994        int index = mTabQueue.indexOf(newTab);
995        if (index != -1) {
996            mTabQueue.remove(index);
997        }
998        mTabQueue.add(newTab);
999
1000        WebView mainView;
1001
1002        // Display the new current tab
1003        mCurrentTab = mTabs.indexOf(newTab);
1004        mainView = newTab.mMainView;
1005        boolean needRestore = (mainView == null);
1006        if (needRestore) {
1007            // Same work as in createNewTab() except don't do new Tab()
1008            newTab.mMainView = mainView = createNewWebView();
1009        }
1010        putViewInForeground(mainView, mActivity.getWebViewClient(),
1011                            mActivity.getWebChromeClient());
1012        // Add the subwindow if it exists
1013        if (newTab.mSubViewContainer != null) {
1014            putViewInForeground(newTab.mSubView, newTab.mSubViewClient,
1015                                newTab.mSubViewChromeClient);
1016        }
1017        if (needRestore) {
1018            // Have to finish setCurrentTab work before calling restoreState
1019            if (!restoreState(newTab.mSavedState, newTab)) {
1020                mainView.loadUrl(BrowserSettings.getInstance().getHomePage());
1021            }
1022        }
1023        return true;
1024    }
1025
1026    /*
1027     * Put the tab in the background using all the empty/background clients.
1028     */
1029    private void putTabInBackground(Tab t) {
1030        putViewInBackground(t.mMainView);
1031        if (t.mSubView != null) {
1032            putViewInBackground(t.mSubView);
1033        }
1034    }
1035
1036    /*
1037     * Dismiss the subwindow for the given tab.
1038     */
1039    void dismissSubWindow(Tab t) {
1040        if (t != null && t.mSubView != null) {
1041            BrowserSettings.getInstance().deleteObserver(
1042                    t.mSubView.getSettings());
1043            t.mSubView.destroy();
1044            t.mSubView = null;
1045            t.mSubViewContainer = null;
1046        }
1047    }
1048
1049    /**
1050     * Ensure that Tab t has data to display in the tab picker.
1051     * @param  t   Tab to populate.
1052     */
1053    /* package */ void populatePickerData(Tab t) {
1054        if (t == null) {
1055            return;
1056        }
1057
1058        // mMainView == null indicates that the tab has been freed.
1059        if (t.mMainView == null) {
1060            populatePickerDataFromSavedState(t);
1061            return;
1062        }
1063
1064        // FIXME: The only place we cared about subwindow was for
1065        // bookmarking (i.e. not when saving state). Was this deliberate?
1066        final WebBackForwardList list = t.mMainView.copyBackForwardList();
1067        final WebHistoryItem item =
1068                list != null ? list.getCurrentItem() : null;
1069        populatePickerData(t, item);
1070
1071        // This method is only called during the tab picker creation. At this
1072        // point we need to listen for new pictures since the WebView is still
1073        // active.
1074        final WebView w = t.getTopWindow();
1075        w.setPictureListener(t);
1076        // Capture the picture here instead of populatePickerData since it can
1077        // be called when saving the state of a tab.
1078        t.mPickerData.mPicture = w.capturePicture();
1079    }
1080
1081    // Create the PickerData and populate it using the saved state of the tab.
1082    private void populatePickerDataFromSavedState(Tab t) {
1083        if (t.mSavedState == null) {
1084            return;
1085        }
1086
1087        final PickerData data = new PickerData();
1088        final Bundle state = t.mSavedState;
1089        data.mUrl = state.getString(CURRURL);
1090        data.mTitle = state.getString(CURRTITLE);
1091        data.mWidth = state.getInt(CURRWIDTH, 0);
1092        // XXX: These keys are from WebView.savePicture so if they change, this
1093        // will break.
1094        data.mScale = state.getFloat("scale", 1.0f);
1095        data.mScrollX = state.getInt("scrollX", 0);
1096        data.mScrollY = state.getInt("scrollY", 0);
1097
1098        if (state.containsKey(CURRPICTURE)) {
1099            final File f = new File(t.mSavedState.getString(CURRPICTURE));
1100            try {
1101                final FileInputStream in = new FileInputStream(f);
1102                data.mPicture = Picture.createFromStream(in);
1103                in.close();
1104            } catch (Exception ex) {
1105                // Ignore any problems with inflating the picture. We just
1106                // won't draw anything.
1107            }
1108        }
1109
1110        // Set the tab's picker data.
1111        t.mPickerData = data;
1112    }
1113
1114    // Populate the picker data using the given history item and the current
1115    // top WebView.
1116    private void populatePickerData(Tab t, WebHistoryItem item) {
1117        final PickerData data = new PickerData();
1118        if (item != null) {
1119            data.mUrl = item.getUrl();
1120            data.mTitle = item.getTitle();
1121            if (data.mTitle == null) {
1122                data.mTitle = data.mUrl;
1123            }
1124        }
1125        // We want to display the top window in the tab picker but use the url
1126        // and title of the main window.
1127        final WebView w = t.getTopWindow();
1128        data.mWidth = w.getWidth();
1129        data.mScale = w.getScale();
1130        data.mScrollX = w.getScrollX();
1131        data.mScrollY = w.getScrollY();
1132
1133        // Remember the old picture if possible.
1134        if (t.mPickerData != null) {
1135            data.mPicture = t.mPickerData.mPicture;
1136        }
1137        t.mPickerData = data;
1138    }
1139
1140    /**
1141     * Clean up the data for all tabs.
1142     */
1143    /* package */ void wipeAllPickerData() {
1144        int size = getTabCount();
1145        for (int i = 0; i < size; i++) {
1146            final Tab t = getTab(i);
1147            if (t != null && t.mSavedState == null) {
1148                t.mPickerData = null;
1149            }
1150            if (t.mMainView != null) {
1151                // Clear the picture listeners.
1152                t.mMainView.setPictureListener(null);
1153                if (t.mSubView != null) {
1154                    t.mSubView.setPictureListener(null);
1155                }
1156            }
1157        }
1158    }
1159
1160    /*
1161     * Save the state for an individual tab.
1162     */
1163    private boolean saveState(Tab t) {
1164        if (t != null) {
1165            final WebView w = t.mMainView;
1166            // If the WebView is null it means we ran low on memory and we
1167            // already stored the saved state in mSavedState.
1168            if (w == null) {
1169                return true;
1170            }
1171            final Bundle b = new Bundle();
1172            final WebBackForwardList list = w.saveState(b);
1173            if (list != null) {
1174                final File f = new File(mThumbnailDir, w.hashCode()
1175                        + "_pic.save");
1176                if (w.savePicture(b, f)) {
1177                    b.putString(CURRPICTURE, f.getPath());
1178                }
1179            }
1180
1181            // Store some extra info for displaying the tab in the picker.
1182            final WebHistoryItem item =
1183                    list != null ? list.getCurrentItem() : null;
1184            populatePickerData(t, item);
1185
1186            // XXX: WebView.savePicture stores the scale and scroll positions
1187            // in the bundle so we don't have to do it here.
1188            final PickerData data = t.mPickerData;
1189            if (data.mUrl != null) {
1190                b.putString(CURRURL, data.mUrl);
1191            }
1192            if (data.mTitle != null) {
1193                b.putString(CURRTITLE, data.mTitle);
1194            }
1195            b.putInt(CURRWIDTH, data.mWidth);
1196            b.putBoolean(CLOSEONEXIT, t.mCloseOnExit);
1197            if (t.mAppId != null) {
1198                b.putString(APPID, t.mAppId);
1199            }
1200            if (t.mOriginalUrl != null) {
1201                b.putString(ORIGINALURL, t.mOriginalUrl);
1202            }
1203
1204            // Remember the parent tab so the relationship can be restored.
1205            if (t.mParentTab != null) {
1206                b.putInt(PARENTTAB, getTabIndex(t.mParentTab));
1207            }
1208
1209            // Remember the saved state.
1210            t.mSavedState = b;
1211            return true;
1212        }
1213        return false;
1214    }
1215
1216    /*
1217     * Restore the state of the tab.
1218     */
1219    private boolean restoreState(Bundle b, Tab t) {
1220        if (b == null) {
1221            return false;
1222        }
1223        // Restore the internal state even if the WebView fails to restore.
1224        // This will maintain the app id, original url and close-on-exit values.
1225        t.mSavedState = null;
1226        t.mPickerData = null;
1227        t.mCloseOnExit = b.getBoolean(CLOSEONEXIT);
1228        t.mAppId = b.getString(APPID);
1229        t.mOriginalUrl = b.getString(ORIGINALURL);
1230
1231        final WebView w = t.mMainView;
1232        final WebBackForwardList list = w.restoreState(b);
1233        if (list == null) {
1234            return false;
1235        }
1236        if (b.containsKey(CURRPICTURE)) {
1237            final File f = new File(b.getString(CURRPICTURE));
1238            w.restorePicture(b, f);
1239            f.delete();
1240        }
1241        return true;
1242    }
1243}
1244