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
17package com.android.browser;
18
19import android.os.Bundle;
20import android.util.Log;
21import android.webkit.WebView;
22
23import java.util.ArrayList;
24import java.util.HashMap;
25import java.util.List;
26import java.util.Vector;
27
28class 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 {
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     */
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
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     */
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     */
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     */
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     */
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     */
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     */
130    Tab getCurrentTab() {
131        return getTab(mCurrentTab);
132    }
133
134    /**
135     * Return the current tab position.
136     * @return The current tab position
137     */
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     */
147    int getTabPosition(Tab tab) {
148        if (tab == null) {
149            return -1;
150        }
151        return mTabs.indexOf(tab);
152    }
153
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     */
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
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     */
190    Tab createNewTab(boolean privateBrowsing) {
191        return createNewTab(null, privateBrowsing);
192    }
193
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     */
215    Tab createNewTab() {
216        return createNewTab(false);
217    }
218
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     */
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     */
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     */
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     */
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     */
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     */
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
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
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     */
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     */
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
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
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     */
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     */
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     */
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.
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     */
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     */
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     */
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     */
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     */
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     */
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
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
693    public OnThumbnailUpdatedListener getOnThumbnailUpdatedListener() {
694        return mOnThumbnailUpdatedListener;
695    }
696
697}
698