• 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.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 
createSnapshotTab(long snapshotId)219     SnapshotTab createSnapshotTab(long snapshotId) {
220         SnapshotTab t = new SnapshotTab(mController, snapshotId);
221         mTabs.add(t);
222         return t;
223     }
224 
225     /**
226      * Remove the parent child relationships from all tabs.
227      */
removeParentChildRelationShips()228     void removeParentChildRelationShips() {
229         for (Tab tab : mTabs) {
230             tab.removeFromTree();
231         }
232     }
233 
234     /**
235      * Remove the tab from the list. If the tab is the current tab shown, the
236      * last created tab will be shown.
237      * @param t The tab to be removed.
238      */
removeTab(Tab t)239     boolean removeTab(Tab t) {
240         if (t == null) {
241             return false;
242         }
243 
244         // Grab the current tab before modifying the list.
245         Tab current = getCurrentTab();
246 
247         // Remove t from our list of tabs.
248         mTabs.remove(t);
249 
250         // Put the tab in the background only if it is the current one.
251         if (current == t) {
252             t.putInBackground();
253             mCurrentTab = -1;
254         } else {
255             // If a tab that is earlier in the list gets removed, the current
256             // index no longer points to the correct tab.
257             mCurrentTab = getTabPosition(current);
258         }
259 
260         // destroy the tab
261         t.destroy();
262         // clear it's references to parent and children
263         t.removeFromTree();
264 
265         // Remove it from the queue of viewed tabs.
266         mTabQueue.remove(t);
267         return true;
268     }
269 
270     /**
271      * Destroy all the tabs and subwindows
272      */
destroy()273     void destroy() {
274         for (Tab t : mTabs) {
275             t.destroy();
276         }
277         mTabs.clear();
278         mTabQueue.clear();
279     }
280 
281     /**
282      * Returns the number of tabs created.
283      * @return The number of tabs created.
284      */
getTabCount()285     int getTabCount() {
286         return mTabs.size();
287     }
288 
289     /**
290      * save the tab state:
291      * current position
292      * position sorted array of tab ids
293      * for each tab id, save the tab state
294      * @param outState
295      * @param saveImages
296      */
saveState(Bundle outState)297     void saveState(Bundle outState) {
298         final int numTabs = getTabCount();
299         if (numTabs == 0) {
300             return;
301         }
302         long[] ids = new long[numTabs];
303         int i = 0;
304         for (Tab tab : mTabs) {
305             Bundle tabState = tab.saveState();
306             if (tabState != null) {
307                 ids[i++] = tab.getId();
308                 String key = Long.toString(tab.getId());
309                 if (outState.containsKey(key)) {
310                     // Dump the tab state for debugging purposes
311                     for (Tab dt : mTabs) {
312                         Log.e(LOGTAG, dt.toString());
313                     }
314                     throw new IllegalStateException(
315                             "Error saving state, duplicate tab ids!");
316                 }
317                 outState.putBundle(key, tabState);
318             } else {
319                 ids[i++] = -1;
320                 // Since we won't be restoring the thumbnail, delete it
321                 tab.deleteThumbnail();
322             }
323         }
324         if (!outState.isEmpty()) {
325             outState.putLongArray(POSITIONS, ids);
326             Tab current = getCurrentTab();
327             long cid = -1;
328             if (current != null) {
329                 cid = current.getId();
330             }
331             outState.putLong(CURRENT, cid);
332         }
333     }
334 
335     /**
336      * Check if the state can be restored.  If the state can be restored, the
337      * current tab id is returned.  This can be passed to restoreState below
338      * in order to restore the correct tab.  Otherwise, -1 is returned and the
339      * state cannot be restored.
340      */
canRestoreState(Bundle inState, boolean restoreIncognitoTabs)341     long canRestoreState(Bundle inState, boolean restoreIncognitoTabs) {
342         final long[] ids = (inState == null) ? null : inState.getLongArray(POSITIONS);
343         if (ids == null) {
344             return -1;
345         }
346         final long oldcurrent = inState.getLong(CURRENT);
347         long current = -1;
348         if (restoreIncognitoTabs || (hasState(oldcurrent, inState) && !isIncognito(oldcurrent, inState))) {
349             current = oldcurrent;
350         } else {
351             // pick first non incognito tab
352             for (long id : ids) {
353                 if (hasState(id, inState) && !isIncognito(id, inState)) {
354                     current = id;
355                     break;
356                 }
357             }
358         }
359         return current;
360     }
361 
hasState(long id, Bundle state)362     private boolean hasState(long id, Bundle state) {
363         if (id == -1) return false;
364         Bundle tab = state.getBundle(Long.toString(id));
365         return ((tab != null) && !tab.isEmpty());
366     }
367 
isIncognito(long id, Bundle state)368     private boolean isIncognito(long id, Bundle state) {
369         Bundle tabstate = state.getBundle(Long.toString(id));
370         if ((tabstate != null) && !tabstate.isEmpty()) {
371             return tabstate.getBoolean(Tab.INCOGNITO);
372         }
373         return false;
374     }
375 
376     /**
377      * Restore the state of all the tabs.
378      * @param currentId The tab id to restore.
379      * @param inState The saved state of all the tabs.
380      * @param restoreIncognitoTabs Restoring private browsing tabs
381      * @param restoreAll All webviews get restored, not just the current tab
382      *        (this does not override handling of incognito tabs)
383      */
restoreState(Bundle inState, long currentId, boolean restoreIncognitoTabs, boolean restoreAll)384     void restoreState(Bundle inState, long currentId,
385             boolean restoreIncognitoTabs, boolean restoreAll) {
386         if (currentId == -1) {
387             return;
388         }
389         long[] ids = inState.getLongArray(POSITIONS);
390         long maxId = -Long.MAX_VALUE;
391         HashMap<Long, Tab> tabMap = new HashMap<Long, Tab>();
392         for (long id : ids) {
393             if (id > maxId) {
394                 maxId = id;
395             }
396             final String idkey = Long.toString(id);
397             Bundle state = inState.getBundle(idkey);
398             if (state == null || state.isEmpty()) {
399                 // Skip tab
400                 continue;
401             } else if (!restoreIncognitoTabs
402                     && state.getBoolean(Tab.INCOGNITO)) {
403                 // ignore tab
404             } else if (id == currentId || restoreAll) {
405                 Tab t = createNewTab(state, false);
406                 if (t == null) {
407                     // We could "break" at this point, but we want
408                     // sNextId to be set correctly.
409                     continue;
410                 }
411                 tabMap.put(id, t);
412                 // Me must set the current tab before restoring the state
413                 // so that all the client classes are set.
414                 if (id == currentId) {
415                     setCurrentTab(t);
416                 }
417             } else {
418                 // Create a new tab and don't restore the state yet, add it
419                 // to the tab list
420                 Tab t = new Tab(mController, state);
421                 tabMap.put(id, t);
422                 mTabs.add(t);
423                 // added the tab to the front as they are not current
424                 mTabQueue.add(0, t);
425             }
426         }
427 
428         // make sure that there is no id overlap between the restored
429         // and new tabs
430         sNextId = maxId + 1;
431 
432         if (mCurrentTab == -1) {
433             if (getTabCount() > 0) {
434                 setCurrentTab(getTab(0));
435             }
436         }
437         // restore parent/child relationships
438         for (long id : ids) {
439             final Tab tab = tabMap.get(id);
440             final Bundle b = inState.getBundle(Long.toString(id));
441             if ((b != null) && (tab != null)) {
442                 final long parentId = b.getLong(Tab.PARENTTAB, -1);
443                 if (parentId != -1) {
444                     final Tab parent = tabMap.get(parentId);
445                     if (parent != null) {
446                         parent.addChildTab(tab);
447                     }
448                 }
449             }
450         }
451     }
452 
453     /**
454      * Free the memory in this order, 1) free the background tabs; 2) free the
455      * WebView cache;
456      */
freeMemory()457     void freeMemory() {
458         if (getTabCount() == 0) return;
459 
460         // free the least frequently used background tabs
461         Vector<Tab> tabs = getHalfLeastUsedTabs(getCurrentTab());
462         if (tabs.size() > 0) {
463             Log.w(LOGTAG, "Free " + tabs.size() + " tabs in the browser");
464             for (Tab t : tabs) {
465                 // store the WebView's state.
466                 t.saveState();
467                 // destroy the tab
468                 t.destroy();
469             }
470             return;
471         }
472 
473         // free the WebView's unused memory (this includes the cache)
474         Log.w(LOGTAG, "Free WebView's unused memory and cache");
475         WebView view = getCurrentWebView();
476         if (view != null) {
477             view.freeMemory();
478         }
479     }
480 
getHalfLeastUsedTabs(Tab current)481     private Vector<Tab> getHalfLeastUsedTabs(Tab current) {
482         Vector<Tab> tabsToGo = new Vector<Tab>();
483 
484         // Don't do anything if we only have 1 tab or if the current tab is
485         // null.
486         if (getTabCount() == 1 || current == null) {
487             return tabsToGo;
488         }
489 
490         if (mTabQueue.size() == 0) {
491             return tabsToGo;
492         }
493 
494         // Rip through the queue starting at the beginning and tear down half of
495         // available tabs which are not the current tab or the parent of the
496         // current tab.
497         int openTabCount = 0;
498         for (Tab t : mTabQueue) {
499             if (t != null && t.getWebView() != null) {
500                 openTabCount++;
501                 if (t != current && t != current.getParent()) {
502                     tabsToGo.add(t);
503                 }
504             }
505         }
506 
507         openTabCount /= 2;
508         if (tabsToGo.size() > openTabCount) {
509             tabsToGo.setSize(openTabCount);
510         }
511 
512         return tabsToGo;
513     }
514 
getLeastUsedTab(Tab current)515     Tab getLeastUsedTab(Tab current) {
516         if (getTabCount() == 1 || current == null) {
517             return null;
518         }
519         if (mTabQueue.size() == 0) {
520             return null;
521         }
522         // find a tab which is not the current tab or the parent of the
523         // current tab
524         for (Tab t : mTabQueue) {
525             if (t != null && t.getWebView() != null) {
526                 if (t != current && t != current.getParent()) {
527                     return t;
528                 }
529             }
530         }
531         return null;
532     }
533 
534     /**
535      * Show the tab that contains the given WebView.
536      * @param view The WebView used to find the tab.
537      */
getTabFromView(WebView view)538     Tab getTabFromView(WebView view) {
539         for (Tab t : mTabs) {
540             if (t.getSubWebView() == view || t.getWebView() == view) {
541                 return t;
542             }
543         }
544         return null;
545     }
546 
547     /**
548      * Return the tab with the matching application id.
549      * @param id The application identifier.
550      */
getTabFromAppId(String id)551     Tab getTabFromAppId(String id) {
552         if (id == null) {
553             return null;
554         }
555         for (Tab t : mTabs) {
556             if (id.equals(t.getAppId())) {
557                 return t;
558             }
559         }
560         return null;
561     }
562 
563     /**
564      * Stop loading in all opened WebView including subWindows.
565      */
stopAllLoading()566     void stopAllLoading() {
567         for (Tab t : mTabs) {
568             final WebView webview = t.getWebView();
569             if (webview != null) {
570                 webview.stopLoading();
571             }
572             final WebView subview = t.getSubWebView();
573             if (subview != null) {
574                 subview.stopLoading();
575             }
576         }
577     }
578 
579     // This method checks if a tab matches the given url.
tabMatchesUrl(Tab t, String url)580     private boolean tabMatchesUrl(Tab t, String url) {
581         return url.equals(t.getUrl()) || url.equals(t.getOriginalUrl());
582     }
583 
584     /**
585      * Return the tab that matches the given url.
586      * @param url The url to search for.
587      */
findTabWithUrl(String url)588     Tab findTabWithUrl(String url) {
589         if (url == null) {
590             return null;
591         }
592         // Check the current tab first.
593         Tab currentTab = getCurrentTab();
594         if (currentTab != null && tabMatchesUrl(currentTab, url)) {
595             return currentTab;
596         }
597         // Now check all the rest.
598         for (Tab tab : mTabs) {
599             if (tabMatchesUrl(tab, url)) {
600                 return tab;
601             }
602         }
603         return null;
604     }
605 
606     /**
607      * Recreate the main WebView of the given tab.
608      */
recreateWebView(Tab t)609     void recreateWebView(Tab t) {
610         final WebView w = t.getWebView();
611         if (w != null) {
612             t.destroy();
613         }
614         // Create a new WebView. If this tab is the current tab, we need to put
615         // back all the clients so force it to be the current tab.
616         t.setWebView(createNewWebView(), false);
617         if (getCurrentTab() == t) {
618             setCurrentTab(t, true);
619         }
620     }
621 
622     /**
623      * Creates a new WebView and registers it with the global settings.
624      */
createNewWebView()625     private WebView createNewWebView() {
626         return createNewWebView(false);
627     }
628 
629     /**
630      * Creates a new WebView and registers it with the global settings.
631      * @param privateBrowsing When true, enables private browsing in the new
632      *        WebView.
633      */
createNewWebView(boolean privateBrowsing)634     private WebView createNewWebView(boolean privateBrowsing) {
635         return mController.getWebViewFactory().createWebView(privateBrowsing);
636     }
637 
638     /**
639      * Put the current tab in the background and set newTab as the current tab.
640      * @param newTab The new tab. If newTab is null, the current tab is not
641      *               set.
642      */
setCurrentTab(Tab newTab)643     boolean setCurrentTab(Tab newTab) {
644         return setCurrentTab(newTab, false);
645     }
646 
647     /**
648      * If force is true, this method skips the check for newTab == current.
649      */
setCurrentTab(Tab newTab, boolean force)650     private boolean setCurrentTab(Tab newTab, boolean force) {
651         Tab current = getTab(mCurrentTab);
652         if (current == newTab && !force) {
653             return true;
654         }
655         if (current != null) {
656             current.putInBackground();
657             mCurrentTab = -1;
658         }
659         if (newTab == null) {
660             return false;
661         }
662 
663         // Move the newTab to the end of the queue
664         int index = mTabQueue.indexOf(newTab);
665         if (index != -1) {
666             mTabQueue.remove(index);
667         }
668         mTabQueue.add(newTab);
669 
670         // Display the new current tab
671         mCurrentTab = mTabs.indexOf(newTab);
672         WebView mainView = newTab.getWebView();
673         boolean needRestore = !newTab.isSnapshot() && (mainView == null);
674         if (needRestore) {
675             // Same work as in createNewTab() except don't do new Tab()
676             mainView = createNewWebView();
677             newTab.setWebView(mainView);
678         }
679         newTab.putInForeground();
680         return true;
681     }
682 
setOnThumbnailUpdatedListener(OnThumbnailUpdatedListener listener)683     public void setOnThumbnailUpdatedListener(OnThumbnailUpdatedListener listener) {
684         mOnThumbnailUpdatedListener = listener;
685         for (Tab t : mTabs) {
686             WebView web = t.getWebView();
687             if (web != null) {
688                 web.setPictureListener(listener != null ? t : null);
689             }
690         }
691     }
692 
getOnThumbnailUpdatedListener()693     public OnThumbnailUpdatedListener getOnThumbnailUpdatedListener() {
694         return mOnThumbnailUpdatedListener;
695     }
696 
697 }
698