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     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                 webview.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