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    /**
220     * Remove the parent child relationships from all tabs.
221     */
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     */
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     */
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     */
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     */
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     */
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
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
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     */
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     */
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
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
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     */
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     */
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     */
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.
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     */
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     */
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     */
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     */
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     */
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     */
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
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
687    public OnThumbnailUpdatedListener getOnThumbnailUpdatedListener() {
688        return mOnThumbnailUpdatedListener;
689    }
690
691}
692