OSDN Git Service

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