Home | History | Annotate | Download | only in browser
      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 {
     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