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.os.Bundle;
20 import android.util.Log;
21 import android.webkit.WebView;
22 
23 import java.util.ArrayList;
24 import java.util.HashMap;
25 import java.util.List;
26 import java.util.Vector;
27 
28 class TabControl {
29     // Log Tag
30     private static final String LOGTAG = "TabControl";
31 
32     // next Tab ID, starting at 1
33     private static long sNextId = 1;
34 
35     private static final String POSITIONS = "positions";
36     private static final String CURRENT = "current";
37 
38     public static interface OnThumbnailUpdatedListener {
onThumbnailUpdated(Tab t)39         void onThumbnailUpdated(Tab t);
40     }
41 
42     // Maximum number of tabs.
43     private int mMaxTabs;
44     // Private array of WebViews that are used as tabs.
45     private ArrayList<Tab> mTabs;
46     // Queue of most recently viewed tabs.
47     private ArrayList<Tab> mTabQueue;
48     // Current position in mTabs.
49     private int mCurrentTab = -1;
50     // the main browser controller
51     private final Controller mController;
52 
53     private OnThumbnailUpdatedListener mOnThumbnailUpdatedListener;
54 
55     /**
56      * Construct a new TabControl object
57      */
TabControl(Controller controller)58     TabControl(Controller controller) {
59         mController = controller;
60         mMaxTabs = mController.getMaxTabs();
61         mTabs = new ArrayList<Tab>(mMaxTabs);
62         mTabQueue = new ArrayList<Tab>(mMaxTabs);
63     }
64 
getNextId()65     synchronized static long getNextId() {
66         return sNextId++;
67     }
68 
69     /**
70      * Return the current tab's main WebView. This will always return the main
71      * WebView for a given tab and not a subwindow.
72      * @return The current tab's WebView.
73      */
getCurrentWebView()74     WebView getCurrentWebView() {
75         Tab t = getTab(mCurrentTab);
76         if (t == null) {
77             return null;
78         }
79         return t.getWebView();
80     }
81 
82     /**
83      * Return the current tab's top-level WebView. This can return a subwindow
84      * if one exists.
85      * @return The top-level WebView of the current tab.
86      */
getCurrentTopWebView()87     WebView getCurrentTopWebView() {
88         Tab t = getTab(mCurrentTab);
89         if (t == null) {
90             return null;
91         }
92         return t.getTopWindow();
93     }
94 
95     /**
96      * Return the current tab's subwindow if it exists.
97      * @return The subwindow of the current tab or null if it doesn't exist.
98      */
getCurrentSubWindow()99     WebView getCurrentSubWindow() {
100         Tab t = getTab(mCurrentTab);
101         if (t == null) {
102             return null;
103         }
104         return t.getSubWebView();
105     }
106 
107     /**
108      * return the list of tabs
109      */
getTabs()110     List<Tab> getTabs() {
111         return mTabs;
112     }
113 
114     /**
115      * Return the tab at the specified position.
116      * @return The Tab for the specified position or null if the tab does not
117      *         exist.
118      */
getTab(int position)119     Tab getTab(int position) {
120         if (position >= 0 && position < mTabs.size()) {
121             return mTabs.get(position);
122         }
123         return null;
124     }
125 
126     /**
127      * Return the current tab.
128      * @return The current tab.
129      */
getCurrentTab()130     Tab getCurrentTab() {
131         return getTab(mCurrentTab);
132     }
133 
134     /**
135      * Return the current tab position.
136      * @return The current tab position
137      */
getCurrentPosition()138     int getCurrentPosition() {
139         return mCurrentTab;
140     }
141 
142     /**
143      * Given a Tab, find it's position
144      * @param Tab to find
145      * @return position of Tab or -1 if not found
146      */
getTabPosition(Tab tab)147     int getTabPosition(Tab tab) {
148         if (tab == null) {
149             return -1;
150         }
151         return mTabs.indexOf(tab);
152     }
153 
canCreateNewTab()154     boolean canCreateNewTab() {
155         return mMaxTabs > mTabs.size();
156     }
157 
158     /**
159      * Returns true if there are any incognito tabs open.
160      * @return True when any incognito tabs are open, false otherwise.
161      */
hasAnyOpenIncognitoTabs()162     boolean hasAnyOpenIncognitoTabs() {
163         for (Tab tab : mTabs) {
164             if (tab.getWebView() != null
165                     && tab.getWebView().isPrivateBrowsingEnabled()) {
166                 return true;
167             }
168         }
169         return false;
170     }
171 
addPreloadedTab(Tab tab)172     void addPreloadedTab(Tab tab) {
173         for (Tab current : mTabs) {
174             if (current != null && current.getId() == tab.getId()) {
175                 throw new IllegalStateException("Tab with id " + tab.getId() + " already exists: "
176                         + current.toString());
177             }
178         }
179         mTabs.add(tab);
180         tab.setController(mController);
181         mController.onSetWebView(tab, tab.getWebView());
182         tab.putInBackground();
183     }
184 
185     /**
186      * Create a new tab.
187      * @return The newly createTab or null if we have reached the maximum
188      *         number of open tabs.
189      */
createNewTab(boolean privateBrowsing)190     Tab createNewTab(boolean privateBrowsing) {
191         return createNewTab(null, privateBrowsing);
192     }
193 
createNewTab(Bundle state, boolean privateBrowsing)194     Tab createNewTab(Bundle state, boolean privateBrowsing) {
195         int size = mTabs.size();
196         // Return false if we have maxed out on tabs
197         if (!canCreateNewTab()) {
198             return null;
199         }
200 
201         final WebView w = createNewWebView(privateBrowsing);
202 
203         // Create a new tab and add it to the tab list
204         Tab t = new Tab(mController, w, state);
205         mTabs.add(t);
206         // Initially put the tab in the background.
207         t.putInBackground();
208         return t;
209     }
210 
211     /**
212      * Create a new tab with default values for closeOnExit(false),
213      * appId(null), url(null), and privateBrowsing(false).
214      */
createNewTab()215     Tab createNewTab() {
216         return createNewTab(false);
217     }
218 
219     /**
220      * Remove the parent child relationships from all tabs.
221      */
removeParentChildRelationShips()222     void removeParentChildRelationShips() {
223         for (Tab tab : mTabs) {
224             tab.removeFromTree();
225         }
226     }
227 
228     /**
229      * Remove the tab from the list. If the tab is the current tab shown, the
230      * last created tab will be shown.
231      * @param t The tab to be removed.
232      */
removeTab(Tab t)233     boolean removeTab(Tab t) {
234         if (t == null) {
235             return false;
236         }
237 
238         // Grab the current tab before modifying the list.
239         Tab current = getCurrentTab();
240 
241         // Remove t from our list of tabs.
242         mTabs.remove(t);
243 
244         // Put the tab in the background only if it is the current one.
245         if (current == t) {
246             t.putInBackground();
247             mCurrentTab = -1;
248         } else {
249             // If a tab that is earlier in the list gets removed, the current
250             // index no longer points to the correct tab.
251             mCurrentTab = getTabPosition(current);
252         }
253 
254         // destroy the tab
255         t.destroy();
256         // clear it's references to parent and children
257         t.removeFromTree();
258 
259         // Remove it from the queue of viewed tabs.
260         mTabQueue.remove(t);
261         return true;
262     }
263 
264     /**
265      * Destroy all the tabs and subwindows
266      */
destroy()267     void destroy() {
268         for (Tab t : mTabs) {
269             t.destroy();
270         }
271         mTabs.clear();
272         mTabQueue.clear();
273     }
274 
275     /**
276      * Returns the number of tabs created.
277      * @return The number of tabs created.
278      */
getTabCount()279     int getTabCount() {
280         return mTabs.size();
281     }
282 
283     /**
284      * save the tab state:
285      * current position
286      * position sorted array of tab ids
287      * for each tab id, save the tab state
288      * @param outState
289      * @param saveImages
290      */
saveState(Bundle outState)291     void saveState(Bundle outState) {
292         final int numTabs = getTabCount();
293         if (numTabs == 0) {
294             return;
295         }
296         long[] ids = new long[numTabs];
297         int i = 0;
298         for (Tab tab : mTabs) {
299             Bundle tabState = tab.saveState();
300             if (tabState != null) {
301                 ids[i++] = tab.getId();
302                 String key = Long.toString(tab.getId());
303                 if (outState.containsKey(key)) {
304                     // Dump the tab state for debugging purposes
305                     for (Tab dt : mTabs) {
306                         Log.e(LOGTAG, dt.toString());
307                     }
308                     throw new IllegalStateException(
309                             "Error saving state, duplicate tab ids!");
310                 }
311                 outState.putBundle(key, tabState);
312             } else {
313                 ids[i++] = -1;
314                 // Since we won't be restoring the thumbnail, delete it
315                 tab.deleteThumbnail();
316             }
317         }
318         if (!outState.isEmpty()) {
319             outState.putLongArray(POSITIONS, ids);
320             Tab current = getCurrentTab();
321             long cid = -1;
322             if (current != null) {
323                 cid = current.getId();
324             }
325             outState.putLong(CURRENT, cid);
326         }
327     }
328 
329     /**
330      * Check if the state can be restored.  If the state can be restored, the
331      * current tab id is returned.  This can be passed to restoreState below
332      * in order to restore the correct tab.  Otherwise, -1 is returned and the
333      * state cannot be restored.
334      */
canRestoreState(Bundle inState, boolean restoreIncognitoTabs)335     long canRestoreState(Bundle inState, boolean restoreIncognitoTabs) {
336         final long[] ids = (inState == null) ? null : inState.getLongArray(POSITIONS);
337         if (ids == null) {
338             return -1;
339         }
340         final long oldcurrent = inState.getLong(CURRENT);
341         long current = -1;
342         if (restoreIncognitoTabs || (hasState(oldcurrent, inState) && !isIncognito(oldcurrent, inState))) {
343             current = oldcurrent;
344         } else {
345             // pick first non incognito tab
346             for (long id : ids) {
347                 if (hasState(id, inState) && !isIncognito(id, inState)) {
348                     current = id;
349                     break;
350                 }
351             }
352         }
353         return current;
354     }
355 
hasState(long id, Bundle state)356     private boolean hasState(long id, Bundle state) {
357         if (id == -1) return false;
358         Bundle tab = state.getBundle(Long.toString(id));
359         return ((tab != null) && !tab.isEmpty());
360     }
361 
isIncognito(long id, Bundle state)362     private boolean isIncognito(long id, Bundle state) {
363         Bundle tabstate = state.getBundle(Long.toString(id));
364         if ((tabstate != null) && !tabstate.isEmpty()) {
365             return tabstate.getBoolean(Tab.INCOGNITO);
366         }
367         return false;
368     }
369 
370     /**
371      * Restore the state of all the tabs.
372      * @param currentId The tab id to restore.
373      * @param inState The saved state of all the tabs.
374      * @param restoreIncognitoTabs Restoring private browsing tabs
375      * @param restoreAll All webviews get restored, not just the current tab
376      *        (this does not override handling of incognito tabs)
377      */
restoreState(Bundle inState, long currentId, boolean restoreIncognitoTabs, boolean restoreAll)378     void restoreState(Bundle inState, long currentId,
379             boolean restoreIncognitoTabs, boolean restoreAll) {
380         if (currentId == -1) {
381             return;
382         }
383         long[] ids = inState.getLongArray(POSITIONS);
384         long maxId = -Long.MAX_VALUE;
385         HashMap<Long, Tab> tabMap = new HashMap<Long, Tab>();
386         for (long id : ids) {
387             if (id > maxId) {
388                 maxId = id;
389             }
390             final String idkey = Long.toString(id);
391             Bundle state = inState.getBundle(idkey);
392             if (state == null || state.isEmpty()) {
393                 // Skip tab
394                 continue;
395             } else if (!restoreIncognitoTabs
396                     && state.getBoolean(Tab.INCOGNITO)) {
397                 // ignore tab
398             } else if (id == currentId || restoreAll) {
399                 Tab t = createNewTab(state, false);
400                 if (t == null) {
401                     // We could "break" at this point, but we want
402                     // sNextId to be set correctly.
403                     continue;
404                 }
405                 tabMap.put(id, t);
406                 // Me must set the current tab before restoring the state
407                 // so that all the client classes are set.
408                 if (id == currentId) {
409                     setCurrentTab(t);
410                 }
411             } else {
412                 // Create a new tab and don't restore the state yet, add it
413                 // to the tab list
414                 Tab t = new Tab(mController, state);
415                 tabMap.put(id, t);
416                 mTabs.add(t);
417                 // added the tab to the front as they are not current
418                 mTabQueue.add(0, t);
419             }
420         }
421 
422         // make sure that there is no id overlap between the restored
423         // and new tabs
424         sNextId = maxId + 1;
425 
426         if (mCurrentTab == -1) {
427             if (getTabCount() > 0) {
428                 setCurrentTab(getTab(0));
429             }
430         }
431         // restore parent/child relationships
432         for (long id : ids) {
433             final Tab tab = tabMap.get(id);
434             final Bundle b = inState.getBundle(Long.toString(id));
435             if ((b != null) && (tab != null)) {
436                 final long parentId = b.getLong(Tab.PARENTTAB, -1);
437                 if (parentId != -1) {
438                     final Tab parent = tabMap.get(parentId);
439                     if (parent != null) {
440                         parent.addChildTab(tab);
441                     }
442                 }
443             }
444         }
445     }
446 
447     /**
448      * Free the memory in this order, 1) free the background tabs; 2) free the
449      * WebView cache;
450      */
freeMemory()451     void freeMemory() {
452         if (getTabCount() == 0) return;
453 
454         // free the least frequently used background tabs
455         Vector<Tab> tabs = getHalfLeastUsedTabs(getCurrentTab());
456         if (tabs.size() > 0) {
457             Log.w(LOGTAG, "Free " + tabs.size() + " tabs in the browser");
458             for (Tab t : tabs) {
459                 // store the WebView's state.
460                 t.saveState();
461                 // destroy the tab
462                 t.destroy();
463             }
464             return;
465         }
466 
467         // free the WebView's unused memory (this includes the cache)
468         Log.w(LOGTAG, "Free WebView's unused memory and cache");
469         WebView view = getCurrentWebView();
470         if (view != null) {
471             view.freeMemory();
472         }
473     }
474 
getHalfLeastUsedTabs(Tab current)475     private Vector<Tab> getHalfLeastUsedTabs(Tab current) {
476         Vector<Tab> tabsToGo = new Vector<Tab>();
477 
478         // Don't do anything if we only have 1 tab or if the current tab is
479         // null.
480         if (getTabCount() == 1 || current == null) {
481             return tabsToGo;
482         }
483 
484         if (mTabQueue.size() == 0) {
485             return tabsToGo;
486         }
487 
488         // Rip through the queue starting at the beginning and tear down half of
489         // available tabs which are not the current tab or the parent of the
490         // current tab.
491         int openTabCount = 0;
492         for (Tab t : mTabQueue) {
493             if (t != null && t.getWebView() != null) {
494                 openTabCount++;
495                 if (t != current && t != current.getParent()) {
496                     tabsToGo.add(t);
497                 }
498             }
499         }
500 
501         openTabCount /= 2;
502         if (tabsToGo.size() > openTabCount) {
503             tabsToGo.setSize(openTabCount);
504         }
505 
506         return tabsToGo;
507     }
508 
getLeastUsedTab(Tab current)509     Tab getLeastUsedTab(Tab current) {
510         if (getTabCount() == 1 || current == null) {
511             return null;
512         }
513         if (mTabQueue.size() == 0) {
514             return null;
515         }
516         // find a tab which is not the current tab or the parent of the
517         // current tab
518         for (Tab t : mTabQueue) {
519             if (t != null && t.getWebView() != null) {
520                 if (t != current && t != current.getParent()) {
521                     return t;
522                 }
523             }
524         }
525         return null;
526     }
527 
528     /**
529      * Show the tab that contains the given WebView.
530      * @param view The WebView used to find the tab.
531      */
getTabFromView(WebView view)532     Tab getTabFromView(WebView view) {
533         for (Tab t : mTabs) {
534             if (t.getSubWebView() == view || t.getWebView() == view) {
535                 return t;
536             }
537         }
538         return null;
539     }
540 
541     /**
542      * Return the tab with the matching application id.
543      * @param id The application identifier.
544      */
getTabFromAppId(String id)545     Tab getTabFromAppId(String id) {
546         if (id == null) {
547             return null;
548         }
549         for (Tab t : mTabs) {
550             if (id.equals(t.getAppId())) {
551                 return t;
552             }
553         }
554         return null;
555     }
556 
557     /**
558      * Stop loading in all opened WebView including subWindows.
559      */
stopAllLoading()560     void stopAllLoading() {
561         for (Tab t : mTabs) {
562             final WebView webview = t.getWebView();
563             if (webview != null) {
564                 webview.stopLoading();
565             }
566             final WebView subview = t.getSubWebView();
567             if (subview != null) {
568                 subview.stopLoading();
569             }
570         }
571     }
572 
573     // This method checks if a tab matches the given url.
tabMatchesUrl(Tab t, String url)574     private boolean tabMatchesUrl(Tab t, String url) {
575         return url.equals(t.getUrl()) || url.equals(t.getOriginalUrl());
576     }
577 
578     /**
579      * Return the tab that matches the given url.
580      * @param url The url to search for.
581      */
findTabWithUrl(String url)582     Tab findTabWithUrl(String url) {
583         if (url == null) {
584             return null;
585         }
586         // Check the current tab first.
587         Tab currentTab = getCurrentTab();
588         if (currentTab != null && tabMatchesUrl(currentTab, url)) {
589             return currentTab;
590         }
591         // Now check all the rest.
592         for (Tab tab : mTabs) {
593             if (tabMatchesUrl(tab, url)) {
594                 return tab;
595             }
596         }
597         return null;
598     }
599 
600     /**
601      * Recreate the main WebView of the given tab.
602      */
recreateWebView(Tab t)603     void recreateWebView(Tab t) {
604         final WebView w = t.getWebView();
605         if (w != null) {
606             t.destroy();
607         }
608         // Create a new WebView. If this tab is the current tab, we need to put
609         // back all the clients so force it to be the current tab.
610         t.setWebView(createNewWebView(), false);
611         if (getCurrentTab() == t) {
612             setCurrentTab(t, true);
613         }
614     }
615 
616     /**
617      * Creates a new WebView and registers it with the global settings.
618      */
createNewWebView()619     private WebView createNewWebView() {
620         return createNewWebView(false);
621     }
622 
623     /**
624      * Creates a new WebView and registers it with the global settings.
625      * @param privateBrowsing When true, enables private browsing in the new
626      *        WebView.
627      */
createNewWebView(boolean privateBrowsing)628     private WebView createNewWebView(boolean privateBrowsing) {
629         return mController.getWebViewFactory().createWebView(privateBrowsing);
630     }
631 
632     /**
633      * Put the current tab in the background and set newTab as the current tab.
634      * @param newTab The new tab. If newTab is null, the current tab is not
635      *               set.
636      */
setCurrentTab(Tab newTab)637     boolean setCurrentTab(Tab newTab) {
638         return setCurrentTab(newTab, false);
639     }
640 
641     /**
642      * If force is true, this method skips the check for newTab == current.
643      */
setCurrentTab(Tab newTab, boolean force)644     private boolean setCurrentTab(Tab newTab, boolean force) {
645         Tab current = getTab(mCurrentTab);
646         if (current == newTab && !force) {
647             return true;
648         }
649         if (current != null) {
650             current.putInBackground();
651             mCurrentTab = -1;
652         }
653         if (newTab == null) {
654             return false;
655         }
656 
657         // Move the newTab to the end of the queue
658         int index = mTabQueue.indexOf(newTab);
659         if (index != -1) {
660             mTabQueue.remove(index);
661         }
662         mTabQueue.add(newTab);
663 
664         // Display the new current tab
665         mCurrentTab = mTabs.indexOf(newTab);
666         WebView mainView = newTab.getWebView();
667         boolean needRestore = mainView == null;
668         if (needRestore) {
669             // Same work as in createNewTab() except don't do new Tab()
670             mainView = createNewWebView();
671             newTab.setWebView(mainView);
672         }
673         newTab.putInForeground();
674         return true;
675     }
676 
setOnThumbnailUpdatedListener(OnThumbnailUpdatedListener listener)677     public void setOnThumbnailUpdatedListener(OnThumbnailUpdatedListener listener) {
678         mOnThumbnailUpdatedListener = listener;
679         for (Tab t : mTabs) {
680             WebView web = t.getWebView();
681             if (web != null) {
682                 web.setPictureListener(listener != null ? t : null);
683             }
684         }
685     }
686 
getOnThumbnailUpdatedListener()687     public OnThumbnailUpdatedListener getOnThumbnailUpdatedListener() {
688         return mOnThumbnailUpdatedListener;
689     }
690 
691 }
692