OSDN Git Service

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