• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
SubWindowClient(WebViewClient client)77         SubWindowClient(WebViewClient client) {
78             mClient = client;
79         }
80         @Override
doUpdateVisitedHistory(WebView view, String url, boolean isReload)81         public void doUpdateVisitedHistory(WebView view, String url,
82                 boolean isReload) {
83             mClient.doUpdateVisitedHistory(view, url, isReload);
84         }
85         @Override
shouldOverrideUrlLoading(WebView view, String url)86         public boolean shouldOverrideUrlLoading(WebView view, String url) {
87             return mClient.shouldOverrideUrlLoading(view, url);
88         }
89         @Override
onReceivedSslError(WebView view, SslErrorHandler handler, SslError error)90         public void onReceivedSslError(WebView view, SslErrorHandler handler,
91                 SslError error) {
92             mClient.onReceivedSslError(view, handler, error);
93         }
94         @Override
onReceivedHttpAuthRequest(WebView view, HttpAuthHandler handler, String host, String realm)95         public void onReceivedHttpAuthRequest(WebView view,
96                 HttpAuthHandler handler, String host, String realm) {
97             mClient.onReceivedHttpAuthRequest(view, handler, host, realm);
98         }
99         @Override
onFormResubmission(WebView view, Message dontResend, Message resend)100         public void onFormResubmission(WebView view, Message dontResend,
101                 Message resend) {
102             mClient.onFormResubmission(view, dontResend, resend);
103         }
104         @Override
onReceivedError(WebView view, int errorCode, String description, String failingUrl)105         public void onReceivedError(WebView view, int errorCode,
106                 String description, String failingUrl) {
107             mClient.onReceivedError(view, errorCode, description, failingUrl);
108         }
109         @Override
shouldOverrideKeyEvent(WebView view, android.view.KeyEvent event)110         public boolean shouldOverrideKeyEvent(WebView view,
111                 android.view.KeyEvent event) {
112             return mClient.shouldOverrideKeyEvent(view, event);
113         }
114         @Override
onUnhandledKeyEvent(WebView view, android.view.KeyEvent event)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 
SubWindowChromeClient(Tab t, WebChromeClient client)127         SubWindowChromeClient(Tab t, WebChromeClient client) {
128             mTab = t;
129             mClient = client;
130         }
131         @Override
onProgressChanged(WebView view, int newProgress)132         public void onProgressChanged(WebView view, int newProgress) {
133             mClient.onProgressChanged(view, newProgress);
134         }
135         @Override
onCreateWindow(WebView view, boolean dialog, boolean userGesture, android.os.Message resultMsg)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
onCloseWindow(WebView window)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
onRequestFocus(WebView view)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
Tab(WebView w, boolean closeOnExit, String appId, String url, Context context)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          */
setWebView(WebView w)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          */
attachTabToContentView(ViewGroup content)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          */
removeTabFromContentView(ViewGroup content)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          */
attachSubWindow(ViewGroup content)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          */
removeSubWindow(ViewGroup content)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          */
getTopWindow()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          */
getWebView()326         public WebView getWebView() {
327             return mMainView;
328         }
329 
330         /**
331          * @return The geolocation permissions prompt for this tab.
332          */
getGeolocationPermissionsPrompt()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          */
getSubWebView()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          */
getUrl()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          */
getTitle()365         public String getTitle() {
366             if (mPickerData != null) {
367                 return mPickerData.mTitle;
368             }
369             return null;
370         }
371 
getFavicon()372         public Bitmap getFavicon() {
373             if (mPickerData != null) {
374                 return mPickerData.mFavicon;
375             }
376             return null;
377         }
378 
setParentTab(Tab parent)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          */
addChildTab(Tab child)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 
removeFromTree()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          */
getParentTab()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          */
closeOnExit()435         public boolean closeOnExit() {
436             return mCloseOnExit;
437         }
438 
setLockIconType(int type)439         void setLockIconType(int type) {
440             mSavedLockIconType = type;
441         }
442 
getLockIconType()443         int getLockIconType() {
444             return mSavedLockIconType;
445         }
446 
setPrevLockIconType(int type)447         void setPrevLockIconType(int type) {
448             mSavedPrevLockIconType = type;
449         }
450 
getPrevLockIconType()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      */
TabControl(BrowserActivity activity)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 
getThumbnailDir()473     File getThumbnailDir() {
474         return mThumbnailDir;
475     }
476 
getBrowserActivity()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      */
getCurrentWebView()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      */
getCurrentErrorConsole(boolean createIfNecessary)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      */
getCurrentTopWebView()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      */
getCurrentSubWindow()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      */
getTab(int index)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      */
getCurrentTab()557     Tab getCurrentTab() {
558         return getTab(mCurrentTab);
559     }
560 
561     /**
562      * Return the current tab index.
563      * @return The current tab index
564      */
getCurrentIndex()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      */
getTabIndex(Tab tab)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      */
createNewTab(boolean closeOnExit, String appId, String url)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      */
createNewTab()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      */
removeTab(Tab t)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      */
clearHistory()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      */
destroy()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      */
getTabCount()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      */
saveState(Bundle outState)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      */
restoreState(Bundle inState)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      */
freeMemory()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 
getLeastUsedTab(Tab current)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 
freeTab(Tab t)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      */
createSubWindow()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      */
getTabFromView(WebView view)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      */
getTabFromId(String id)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.
tabMatchesUrl(Tab t, String 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      */
findUnusedTabWithUrl(String url)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      */
recreateWebView(Tab t, String url)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      */
createNewWebView()1022     private WebView createNewWebView() {
1023         // Create a new WebView
1024         WebView w = new WebView(mActivity);
1025         w.setScrollbarFadingEnabled(true);
1026         w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
1027         w.setMapTrackballToArrowKeys(false); // use trackball directly
1028         // Enable the built-in zoom
1029         w.getSettings().setBuiltInZoomControls(true);
1030         // Add this WebView to the settings observer list and update the
1031         // settings
1032         final BrowserSettings s = BrowserSettings.getInstance();
1033         s.addObserver(w.getSettings()).update(s, null);
1034         return w;
1035     }
1036 
1037     /**
1038      * Put the current tab in the background and set newTab as the current tab.
1039      * @param newTab The new tab. If newTab is null, the current tab is not
1040      *               set.
1041      */
setCurrentTab(Tab newTab)1042     boolean setCurrentTab(Tab newTab) {
1043         return setCurrentTab(newTab, false);
1044     }
1045 
pauseCurrentTab()1046     /*package*/ void pauseCurrentTab() {
1047         Tab t = getCurrentTab();
1048         if (t != null) {
1049             t.mMainView.onPause();
1050             if (t.mSubView != null) {
1051                 t.mSubView.onPause();
1052             }
1053         }
1054     }
1055 
resumeCurrentTab()1056     /*package*/ void resumeCurrentTab() {
1057         Tab t = getCurrentTab();
1058         if (t != null) {
1059             t.mMainView.onResume();
1060             if (t.mSubView != null) {
1061                 t.mSubView.onResume();
1062             }
1063         }
1064     }
1065 
putViewInForeground(WebView v, WebViewClient vc, WebChromeClient cc)1066     private void putViewInForeground(WebView v, WebViewClient vc,
1067                                      WebChromeClient cc) {
1068         v.setWebViewClient(vc);
1069         v.setWebChromeClient(cc);
1070         v.setOnCreateContextMenuListener(mActivity);
1071         v.setDownloadListener(mActivity);
1072         v.onResume();
1073     }
1074 
putViewInBackground(WebView v)1075     private void putViewInBackground(WebView v) {
1076         // Set an empty callback so that default actions are not triggered.
1077         v.setWebViewClient(mEmptyClient);
1078         v.setWebChromeClient(mBackgroundChromeClient);
1079         v.setOnCreateContextMenuListener(null);
1080         // Leave the DownloadManager attached so that downloads can start in
1081         // a non-active window. This can happen when going to a site that does
1082         // a redirect after a period of time. The user could have switched to
1083         // another tab while waiting for the download to start.
1084         v.setDownloadListener(mActivity);
1085         v.onPause();
1086     }
1087 
1088     /**
1089      * If force is true, this method skips the check for newTab == current.
1090      */
setCurrentTab(Tab newTab, boolean force)1091     private boolean setCurrentTab(Tab newTab, boolean force) {
1092         Tab current = getTab(mCurrentTab);
1093         if (current == newTab && !force) {
1094             return true;
1095         }
1096         if (current != null) {
1097             // Remove the current WebView and the container of the subwindow
1098             putTabInBackground(current);
1099         }
1100 
1101         if (newTab == null) {
1102             return false;
1103         }
1104 
1105         // Move the newTab to the end of the queue
1106         int index = mTabQueue.indexOf(newTab);
1107         if (index != -1) {
1108             mTabQueue.remove(index);
1109         }
1110         mTabQueue.add(newTab);
1111 
1112         WebView mainView;
1113 
1114         // Display the new current tab
1115         mCurrentTab = mTabs.indexOf(newTab);
1116         mainView = newTab.mMainView;
1117         boolean needRestore = (mainView == null);
1118         if (needRestore) {
1119             // Same work as in createNewTab() except don't do new Tab()
1120             mainView = createNewWebView();
1121             newTab.setWebView(mainView);
1122         }
1123         putViewInForeground(mainView, mActivity.getWebViewClient(),
1124                             mActivity.getWebChromeClient());
1125         // Add the subwindow if it exists
1126         if (newTab.mSubViewContainer != null) {
1127             putViewInForeground(newTab.mSubView, newTab.mSubViewClient,
1128                                 newTab.mSubViewChromeClient);
1129         }
1130         if (needRestore) {
1131             // Have to finish setCurrentTab work before calling restoreState
1132             if (!restoreState(newTab.mSavedState, newTab)) {
1133                 mainView.loadUrl(BrowserSettings.getInstance().getHomePage());
1134             }
1135         }
1136         return true;
1137     }
1138 
1139     /*
1140      * Put the tab in the background using all the empty/background clients.
1141      */
putTabInBackground(Tab t)1142     private void putTabInBackground(Tab t) {
1143         putViewInBackground(t.mMainView);
1144         if (t.mSubView != null) {
1145             putViewInBackground(t.mSubView);
1146         }
1147     }
1148 
1149     /*
1150      * Dismiss the subwindow for the given tab.
1151      */
dismissSubWindow(Tab t)1152     void dismissSubWindow(Tab t) {
1153         if (t != null && t.mSubView != null) {
1154             BrowserSettings.getInstance().deleteObserver(
1155                     t.mSubView.getSettings());
1156             t.mSubView.destroy();
1157             t.mSubView = null;
1158             t.mSubViewContainer = null;
1159         }
1160     }
1161 
1162     /**
1163      * Ensure that Tab t has data to display in the tab picker.
1164      * @param  t   Tab to populate.
1165      */
populatePickerData(Tab t)1166     /* package */ void populatePickerData(Tab t) {
1167         if (t == null) {
1168             return;
1169         }
1170 
1171         // mMainView == null indicates that the tab has been freed.
1172         if (t.mMainView == null) {
1173             populatePickerDataFromSavedState(t);
1174             return;
1175         }
1176 
1177         // FIXME: The only place we cared about subwindow was for
1178         // bookmarking (i.e. not when saving state). Was this deliberate?
1179         final WebBackForwardList list = t.mMainView.copyBackForwardList();
1180         final WebHistoryItem item =
1181                 list != null ? list.getCurrentItem() : null;
1182         populatePickerData(t, item);
1183     }
1184 
1185     // Create the PickerData and populate it using the saved state of the tab.
populatePickerDataFromSavedState(Tab t)1186     private void populatePickerDataFromSavedState(Tab t) {
1187         if (t.mSavedState == null) {
1188             return;
1189         }
1190 
1191         final PickerData data = new PickerData();
1192         final Bundle state = t.mSavedState;
1193         data.mUrl = state.getString(CURRURL);
1194         data.mTitle = state.getString(CURRTITLE);
1195         // XXX: These keys are from WebView.savePicture so if they change, this
1196         // will break.
1197         data.mScale = state.getFloat("scale", 1.0f);
1198         data.mScrollX = state.getInt("scrollX", 0);
1199         data.mScrollY = state.getInt("scrollY", 0);
1200 
1201         // Set the tab's picker data.
1202         t.mPickerData = data;
1203     }
1204 
1205     // Populate the picker data using the given history item and the current
1206     // top WebView.
populatePickerData(Tab t, WebHistoryItem item)1207     private void populatePickerData(Tab t, WebHistoryItem item) {
1208         final PickerData data = new PickerData();
1209         if (item != null) {
1210             data.mUrl = item.getUrl();
1211             data.mTitle = item.getTitle();
1212             data.mFavicon = item.getFavicon();
1213             if (data.mTitle == null) {
1214                 data.mTitle = data.mUrl;
1215             }
1216         }
1217         // We want to display the top window in the tab picker but use the url
1218         // and title of the main window.
1219         final WebView w = t.getTopWindow();
1220         data.mScale = w.getScale();
1221         data.mScrollX = w.getScrollX();
1222         data.mScrollY = w.getScrollY();
1223 
1224         t.mPickerData = data;
1225     }
1226 
1227     /**
1228      * Clean up the data for all tabs.
1229      */
wipeAllPickerData()1230     /* package */ void wipeAllPickerData() {
1231         int size = getTabCount();
1232         for (int i = 0; i < size; i++) {
1233             final Tab t = getTab(i);
1234             if (t != null && t.mSavedState == null) {
1235                 t.mPickerData = null;
1236             }
1237         }
1238     }
1239 
1240     /*
1241      * Save the state for an individual tab.
1242      */
saveState(Tab t)1243     private boolean saveState(Tab t) {
1244         if (t != null) {
1245             final WebView w = t.mMainView;
1246             // If the WebView is null it means we ran low on memory and we
1247             // already stored the saved state in mSavedState.
1248             if (w == null) {
1249                 return true;
1250             }
1251             final Bundle b = new Bundle();
1252             final WebBackForwardList list = w.saveState(b);
1253             if (list != null) {
1254                 final File f = new File(mThumbnailDir, w.hashCode()
1255                         + "_pic.save");
1256                 if (w.savePicture(b, f)) {
1257                     b.putString(CURRPICTURE, f.getPath());
1258                 }
1259             }
1260 
1261             // Store some extra info for displaying the tab in the picker.
1262             final WebHistoryItem item =
1263                     list != null ? list.getCurrentItem() : null;
1264             populatePickerData(t, item);
1265 
1266             // XXX: WebView.savePicture stores the scale and scroll positions
1267             // in the bundle so we don't have to do it here.
1268             final PickerData data = t.mPickerData;
1269             if (data.mUrl != null) {
1270                 b.putString(CURRURL, data.mUrl);
1271             }
1272             if (data.mTitle != null) {
1273                 b.putString(CURRTITLE, data.mTitle);
1274             }
1275             b.putBoolean(CLOSEONEXIT, t.mCloseOnExit);
1276             if (t.mAppId != null) {
1277                 b.putString(APPID, t.mAppId);
1278             }
1279             if (t.mOriginalUrl != null) {
1280                 b.putString(ORIGINALURL, t.mOriginalUrl);
1281             }
1282 
1283             // Remember the parent tab so the relationship can be restored.
1284             if (t.mParentTab != null) {
1285                 b.putInt(PARENTTAB, getTabIndex(t.mParentTab));
1286             }
1287 
1288             // Remember the saved state.
1289             t.mSavedState = b;
1290             return true;
1291         }
1292         return false;
1293     }
1294 
1295     /*
1296      * Restore the state of the tab.
1297      */
restoreState(Bundle b, Tab t)1298     private boolean restoreState(Bundle b, Tab t) {
1299         if (b == null) {
1300             return false;
1301         }
1302         // Restore the internal state even if the WebView fails to restore.
1303         // This will maintain the app id, original url and close-on-exit values.
1304         t.mSavedState = null;
1305         t.mPickerData = null;
1306         t.mCloseOnExit = b.getBoolean(CLOSEONEXIT);
1307         t.mAppId = b.getString(APPID);
1308         t.mOriginalUrl = b.getString(ORIGINALURL);
1309 
1310         final WebView w = t.mMainView;
1311         final WebBackForwardList list = w.restoreState(b);
1312         if (list == null) {
1313             return false;
1314         }
1315         if (b.containsKey(CURRPICTURE)) {
1316             final File f = new File(b.getString(CURRPICTURE));
1317             w.restorePicture(b, f);
1318             f.delete();
1319         }
1320         return true;
1321     }
1322 }
1323