TabControl.java revision dcc5eeb63eadd597587a0b2b49998c267b0bcc11
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.graphics.Bitmap;
20import android.graphics.BitmapFactory;
21import android.graphics.BitmapShader;
22import android.graphics.Paint;
23import android.graphics.Shader;
24import android.os.Bundle;
25import android.util.Log;
26import android.view.View;
27import android.webkit.WebBackForwardList;
28import android.webkit.WebView;
29
30import java.io.File;
31import java.util.ArrayList;
32import java.util.Vector;
33
34class TabControl {
35    // Log Tag
36    private static final String LOGTAG = "TabControl";
37    // Maximum number of tabs.
38    private static final int MAX_TABS = 8;
39    // Private array of WebViews that are used as tabs.
40    private ArrayList<Tab> mTabs = new ArrayList<Tab>(MAX_TABS);
41    // Queue of most recently viewed tabs.
42    private ArrayList<Tab> mTabQueue = new ArrayList<Tab>(MAX_TABS);
43    // Current position in mTabs.
44    private int mCurrentTab = -1;
45    // A private instance of BrowserActivity to interface with when adding and
46    // switching between tabs.
47    private final BrowserActivity mActivity;
48    // Directory to store thumbnails for each WebView.
49    private final File mThumbnailDir;
50
51    /**
52     * Construct a new TabControl object that interfaces with the given
53     * BrowserActivity instance.
54     * @param activity A BrowserActivity instance that TabControl will interface
55     *                 with.
56     */
57    TabControl(BrowserActivity activity) {
58        mActivity = activity;
59        mThumbnailDir = activity.getDir("thumbnails", 0);
60    }
61
62    File getThumbnailDir() {
63        return mThumbnailDir;
64    }
65
66    BrowserActivity getBrowserActivity() {
67        return mActivity;
68    }
69
70    /**
71     * Return the current tab's main WebView. This will always return the main
72     * WebView for a given tab and not a subwindow.
73     * @return The current tab's WebView.
74     */
75    WebView getCurrentWebView() {
76        Tab t = getTab(mCurrentTab);
77        if (t == null) {
78            return null;
79        }
80        return t.getWebView();
81    }
82
83    /**
84     * Return the current tab's top-level WebView. This can return a subwindow
85     * if one exists.
86     * @return The top-level WebView of the current tab.
87     */
88    WebView getCurrentTopWebView() {
89        Tab t = getTab(mCurrentTab);
90        if (t == null) {
91            return null;
92        }
93        return t.getTopWindow();
94    }
95
96    /**
97     * Return the current tab's subwindow if it exists.
98     * @return The subwindow of the current tab or null if it doesn't exist.
99     */
100    WebView getCurrentSubWindow() {
101        Tab t = getTab(mCurrentTab);
102        if (t == null) {
103            return null;
104        }
105        return t.getSubWebView();
106    }
107
108    /**
109     * Return the tab at the specified index.
110     * @return The Tab for the specified index or null if the tab does not
111     *         exist.
112     */
113    Tab getTab(int index) {
114        if (index >= 0 && index < mTabs.size()) {
115            return mTabs.get(index);
116        }
117        return null;
118    }
119
120    /**
121     * Return the current tab.
122     * @return The current tab.
123     */
124    Tab getCurrentTab() {
125        return getTab(mCurrentTab);
126    }
127
128    /**
129     * Return the current tab index.
130     * @return The current tab index
131     */
132    int getCurrentIndex() {
133        return mCurrentTab;
134    }
135
136    /**
137     * Given a Tab, find it's index
138     * @param Tab to find
139     * @return index of Tab or -1 if not found
140     */
141    int getTabIndex(Tab tab) {
142        if (tab == null) {
143            return -1;
144        }
145        return mTabs.indexOf(tab);
146    }
147
148    boolean canCreateNewTab() {
149        return MAX_TABS != mTabs.size();
150    }
151
152    /**
153     * Create a new tab.
154     * @return The newly createTab or null if we have reached the maximum
155     *         number of open tabs.
156     */
157    Tab createNewTab(boolean closeOnExit, String appId, String url) {
158        int size = mTabs.size();
159        // Return false if we have maxed out on tabs
160        if (MAX_TABS == size) {
161            return null;
162        }
163        final WebView w = createNewWebView();
164
165        // Create a new tab and add it to the tab list
166        Tab t = new Tab(mActivity, w, closeOnExit, appId, url);
167        mTabs.add(t);
168        // Initially put the tab in the background.
169        t.putInBackground();
170        return t;
171    }
172
173    /**
174     * Create a new tab with default values for closeOnExit(false),
175     * appId(null), and url(null).
176     */
177    Tab createNewTab() {
178        return createNewTab(false, null, null);
179    }
180
181    /**
182     * Remove the parent child relationships from all tabs.
183     */
184    void removeParentChildRelationShips() {
185        for (Tab tab : mTabs) {
186            tab.removeFromTree();
187        }
188    }
189
190    /**
191     * Remove the tab from the list. If the tab is the current tab shown, the
192     * last created tab will be shown.
193     * @param t The tab to be removed.
194     */
195    boolean removeTab(Tab t) {
196        if (t == null) {
197            return false;
198        }
199
200        // Grab the current tab before modifying the list.
201        Tab current = getCurrentTab();
202
203        // Remove t from our list of tabs.
204        mTabs.remove(t);
205
206        // Put the tab in the background only if it is the current one.
207        if (current == t) {
208            t.putInBackground();
209            mCurrentTab = -1;
210        } else {
211            // If a tab that is earlier in the list gets removed, the current
212            // index no longer points to the correct tab.
213            mCurrentTab = getTabIndex(current);
214        }
215
216        // destroy the tab
217        t.destroy();
218        // clear it's references to parent and children
219        t.removeFromTree();
220
221        // The tab indices have shifted, update all the saved state so we point
222        // to the correct index.
223        for (Tab tab : mTabs) {
224            Vector<Tab> children = tab.getChildTabs();
225            if (children != null) {
226                for (Tab child : children) {
227                    child.setParentTab(tab);
228                }
229            }
230        }
231
232        // This tab may have been pushed in to the background and then closed.
233        // If the saved state contains a picture file, delete the file.
234        Bundle savedState = t.getSavedState();
235        if (savedState != null) {
236            if (savedState.containsKey(Tab.CURRPICTURE)) {
237                new File(savedState.getString(Tab.CURRPICTURE)).delete();
238            }
239        }
240
241        // Remove it from the queue of viewed tabs.
242        mTabQueue.remove(t);
243        return true;
244    }
245
246    /**
247     * Destroy all the tabs and subwindows
248     */
249    void destroy() {
250        for (Tab t : mTabs) {
251            t.destroy();
252        }
253        mTabs.clear();
254        mTabQueue.clear();
255    }
256
257    /**
258     * Returns the number of tabs created.
259     * @return The number of tabs created.
260     */
261    int getTabCount() {
262        return mTabs.size();
263    }
264
265
266    /**
267     * Save the state of all the Tabs.
268     * @param outState The Bundle to save the state to.
269     */
270    void saveState(Bundle outState) {
271        final int numTabs = getTabCount();
272        outState.putInt(Tab.NUMTABS, numTabs);
273        final int index = getCurrentIndex();
274        outState.putInt(Tab.CURRTAB, (index >= 0 && index < numTabs) ? index : 0);
275        for (int i = 0; i < numTabs; i++) {
276            final Tab t = getTab(i);
277            if (t.saveState()) {
278                outState.putBundle(Tab.WEBVIEW + i, t.getSavedState());
279            }
280        }
281    }
282
283    /**
284     * Restore the state of all the tabs.
285     * @param inState The saved state of all the tabs.
286     * @return True if there were previous tabs that were restored. False if
287     *         there was no saved state or restoring the state failed.
288     */
289    boolean restoreState(Bundle inState) {
290        final int numTabs = (inState == null)
291                ? -1 : inState.getInt(Tab.NUMTABS, -1);
292        if (numTabs == -1) {
293            return false;
294        } else {
295            final int currentTab = inState.getInt(Tab.CURRTAB, -1);
296            for (int i = 0; i < numTabs; i++) {
297                if (i == currentTab) {
298                    Tab t = createNewTab();
299                    // Me must set the current tab before restoring the state
300                    // so that all the client classes are set.
301                    setCurrentTab(t);
302                    if (!t.restoreState(inState.getBundle(Tab.WEBVIEW + i))) {
303                        Log.w(LOGTAG, "Fail in restoreState, load home page.");
304                        t.getWebView().loadUrl(BrowserSettings.getInstance()
305                                .getHomePage());
306                    }
307                } else {
308                    // Create a new tab and don't restore the state yet, add it
309                    // to the tab list
310                    Tab t = new Tab(mActivity, null, false, null, null);
311                    Bundle state = inState.getBundle(Tab.WEBVIEW + i);
312                    if (state != null) {
313                        t.setSavedState(state);
314                        t.populatePickerDataFromSavedState();
315                        // Need to maintain the app id and original url so we
316                        // can possibly reuse this tab.
317                        t.setAppId(state.getString(Tab.APPID));
318                        t.setOriginalUrl(state.getString(Tab.ORIGINALURL));
319                    }
320                    mTabs.add(t);
321                    mTabQueue.add(t);
322                }
323            }
324            // Rebuild the tree of tabs. Do this after all tabs have been
325            // created/restored so that the parent tab exists.
326            for (int i = 0; i < numTabs; i++) {
327                final Bundle b = inState.getBundle(Tab.WEBVIEW + i);
328                final Tab t = getTab(i);
329                if (b != null && t != null) {
330                    final int parentIndex = b.getInt(Tab.PARENTTAB, -1);
331                    if (parentIndex != -1) {
332                        final Tab parent = getTab(parentIndex);
333                        if (parent != null) {
334                            parent.addChildTab(t);
335                        }
336                    }
337                }
338            }
339        }
340        return true;
341    }
342
343    /**
344     * Free the memory in this order, 1) free the background tab; 2) free the
345     * WebView cache;
346     */
347    void freeMemory() {
348        if (getTabCount() == 0) return;
349
350        // free the least frequently used background tab
351        Tab t = getLeastUsedTab(getCurrentTab());
352        if (t != null) {
353            Log.w(LOGTAG, "Free a tab in the browser");
354            // store the WebView's state.
355            t.saveState();
356            // destroy the tab
357            t.destroy();
358            return;
359        }
360
361        // free the WebView's unused memory (this includes the cache)
362        Log.w(LOGTAG, "Free WebView's unused memory and cache");
363        WebView view = getCurrentWebView();
364        if (view != null) {
365            view.freeMemory();
366        }
367    }
368
369    private Tab getLeastUsedTab(Tab current) {
370        // Don't do anything if we only have 1 tab or if the current tab is
371        // null.
372        if (getTabCount() == 1 || current == null) {
373            return null;
374        }
375
376        // Rip through the queue starting at the beginning and tear down the
377        // next available tab.
378        Tab t = null;
379        int i = 0;
380        final int queueSize = mTabQueue.size();
381        if (queueSize == 0) {
382            return null;
383        }
384        do {
385            t = mTabQueue.get(i++);
386        } while (i < queueSize
387                && ((t != null && t.getWebView() == null)
388                    || t == current.getParentTab()));
389
390        // Don't do anything if the last remaining tab is the current one or if
391        // the last tab has been freed already.
392        if (t == current || t.getWebView() == null) {
393            return null;
394        }
395
396        return t;
397    }
398
399    /**
400     * Show the tab that contains the given WebView.
401     * @param view The WebView used to find the tab.
402     */
403    Tab getTabFromView(WebView view) {
404        final int size = getTabCount();
405        for (int i = 0; i < size; i++) {
406            final Tab t = getTab(i);
407            if (t.getSubWebView() == view || t.getWebView() == view) {
408                return t;
409            }
410        }
411        return null;
412    }
413
414    /**
415     * Return the tab with the matching application id.
416     * @param id The application identifier.
417     */
418    Tab getTabFromId(String id) {
419        if (id == null) {
420            return null;
421        }
422        final int size = getTabCount();
423        for (int i = 0; i < size; i++) {
424            final Tab t = getTab(i);
425            if (id.equals(t.getAppId())) {
426                return t;
427            }
428        }
429        return null;
430    }
431
432    /**
433     * Stop loading in all opened WebView including subWindows.
434     */
435    void stopAllLoading() {
436        final int size = getTabCount();
437        for (int i = 0; i < size; i++) {
438            final Tab t = getTab(i);
439            final WebView webview = t.getWebView();
440            if (webview != null) {
441                webview.stopLoading();
442            }
443            final WebView subview = t.getSubWebView();
444            if (subview != null) {
445                webview.stopLoading();
446            }
447        }
448    }
449
450    // This method checks if a non-app tab (one created within the browser)
451    // matches the given url.
452    private boolean tabMatchesUrl(Tab t, String url) {
453        if (t.getAppId() != null) {
454            return false;
455        }
456        WebView webview = t.getWebView();
457        if (webview == null) {
458            return false;
459        } else if (url.equals(webview.getUrl())
460                || url.equals(webview.getOriginalUrl())) {
461            return true;
462        }
463        return false;
464    }
465
466    /**
467     * Return the tab that has no app id associated with it and the url of the
468     * tab matches the given url.
469     * @param url The url to search for.
470     */
471    Tab findUnusedTabWithUrl(String url) {
472        if (url == null) {
473            return null;
474        }
475        // Check the current tab first.
476        Tab t = getCurrentTab();
477        if (t != null && tabMatchesUrl(t, url)) {
478            return t;
479        }
480        // Now check all the rest.
481        final int size = getTabCount();
482        for (int i = 0; i < size; i++) {
483            t = getTab(i);
484            if (tabMatchesUrl(t, url)) {
485                return t;
486            }
487        }
488        return null;
489    }
490
491    /**
492     * Recreate the main WebView of the given tab. Returns true if the WebView
493     * was deleted.
494     */
495    boolean recreateWebView(Tab t, String url) {
496        final WebView w = t.getWebView();
497        if (w != null) {
498            if (url != null && url.equals(t.getOriginalUrl())) {
499                // The original url matches the current url. Just go back to the
500                // first history item so we can load it faster than if we
501                // rebuilt the WebView.
502                final WebBackForwardList list = w.copyBackForwardList();
503                if (list != null) {
504                    w.goBackOrForward(-list.getCurrentIndex());
505                    w.clearHistory(); // maintains the current page.
506                    return false;
507                }
508            }
509            t.destroy();
510        }
511        // Create a new WebView. If this tab is the current tab, we need to put
512        // back all the clients so force it to be the current tab.
513        t.setWebView(createNewWebView());
514        if (getCurrentTab() == t) {
515            setCurrentTab(t, true);
516        }
517        // Clear the saved state and picker data
518        t.setSavedState(null);
519        t.clearPickerData();
520        // Save the new url in order to avoid deleting the WebView.
521        t.setOriginalUrl(url);
522        return true;
523    }
524
525    /**
526     * Creates a new WebView and registers it with the global settings.
527     */
528    private WebView createNewWebView() {
529        // Create a new WebView
530        WebView w = new WebView(mActivity);
531        w.setScrollbarFadingEnabled(true);
532        w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
533        w.setMapTrackballToArrowKeys(false); // use trackball directly
534        // Enable the built-in zoom
535        w.getSettings().setBuiltInZoomControls(true);
536        // Add this WebView to the settings observer list and update the
537        // settings
538        final BrowserSettings s = BrowserSettings.getInstance();
539        s.addObserver(w.getSettings()).update(s, null);
540
541        // pick a default
542        if (false) {
543            MeshTracker mt = new MeshTracker(2);
544            Paint paint = new Paint();
545            Bitmap bm = BitmapFactory.decodeResource(mActivity.getResources(),
546                                         R.drawable.pattern_carbon_fiber_dark);
547            paint.setShader(new BitmapShader(bm, Shader.TileMode.REPEAT,
548                                             Shader.TileMode.REPEAT));
549            mt.setBGPaint(paint);
550            w.setDragTracker(mt);
551        }
552        return w;
553    }
554
555    /**
556     * Put the current tab in the background and set newTab as the current tab.
557     * @param newTab The new tab. If newTab is null, the current tab is not
558     *               set.
559     */
560    boolean setCurrentTab(Tab newTab) {
561        return setCurrentTab(newTab, false);
562    }
563
564    void pauseCurrentTab() {
565        Tab t = getCurrentTab();
566        if (t != null) {
567            t.pause();
568        }
569    }
570
571    void resumeCurrentTab() {
572        Tab t = getCurrentTab();
573        if (t != null) {
574            t.resume();
575        }
576    }
577
578    /**
579     * If force is true, this method skips the check for newTab == current.
580     */
581    private boolean setCurrentTab(Tab newTab, boolean force) {
582        Tab current = getTab(mCurrentTab);
583        if (current == newTab && !force) {
584            return true;
585        }
586        if (current != null) {
587            current.putInBackground();
588            mCurrentTab = -1;
589        }
590        if (newTab == null) {
591            return false;
592        }
593
594        // Move the newTab to the end of the queue
595        int index = mTabQueue.indexOf(newTab);
596        if (index != -1) {
597            mTabQueue.remove(index);
598        }
599        mTabQueue.add(newTab);
600
601        // Display the new current tab
602        mCurrentTab = mTabs.indexOf(newTab);
603        WebView mainView = newTab.getWebView();
604        boolean needRestore = (mainView == null);
605        if (needRestore) {
606            // Same work as in createNewTab() except don't do new Tab()
607            mainView = createNewWebView();
608            newTab.setWebView(mainView);
609        }
610        newTab.putInForeground();
611        if (needRestore) {
612            // Have to finish setCurrentTab work before calling restoreState
613            if (!newTab.restoreState(newTab.getSavedState())) {
614                mainView.loadUrl(BrowserSettings.getInstance().getHomePage());
615            }
616        }
617        return true;
618    }
619}
620