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.graphics.Bitmap;
     20 import android.graphics.BitmapFactory;
     21 import android.graphics.BitmapShader;
     22 import android.graphics.Paint;
     23 import android.graphics.Shader;
     24 import android.os.Bundle;
     25 import android.util.Log;
     26 import android.view.View;
     27 import android.webkit.WebBackForwardList;
     28 import android.webkit.WebView;
     29 
     30 import java.io.File;
     31 import java.util.ArrayList;
     32 import java.util.Vector;
     33 
     34 class 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                     // added the tab to the front as they are not current
    322                     mTabQueue.add(0, t);
    323                 }
    324             }
    325             // Rebuild the tree of tabs. Do this after all tabs have been
    326             // created/restored so that the parent tab exists.
    327             for (int i = 0; i < numTabs; i++) {
    328                 final Bundle b = inState.getBundle(Tab.WEBVIEW + i);
    329                 final Tab t = getTab(i);
    330                 if (b != null && t != null) {
    331                     final int parentIndex = b.getInt(Tab.PARENTTAB, -1);
    332                     if (parentIndex != -1) {
    333                         final Tab parent = getTab(parentIndex);
    334                         if (parent != null) {
    335                             parent.addChildTab(t);
    336                         }
    337                     }
    338                 }
    339             }
    340         }
    341         return true;
    342     }
    343 
    344     /**
    345      * Free the memory in this order, 1) free the background tabs; 2) free the
    346      * WebView cache;
    347      */
    348     void freeMemory() {
    349         if (getTabCount() == 0) return;
    350 
    351         // free the least frequently used background tabs
    352         Vector<Tab> tabs = getHalfLeastUsedTabs(getCurrentTab());
    353         if (tabs.size() > 0) {
    354             Log.w(LOGTAG, "Free " + tabs.size() + " tabs in the browser");
    355             for (Tab t : tabs) {
    356                 // store the WebView's state.
    357                 t.saveState();
    358                 // destroy the tab
    359                 t.destroy();
    360             }
    361             return;
    362         }
    363 
    364         // free the WebView's unused memory (this includes the cache)
    365         Log.w(LOGTAG, "Free WebView's unused memory and cache");
    366         WebView view = getCurrentWebView();
    367         if (view != null) {
    368             view.freeMemory();
    369         }
    370     }
    371 
    372     private Vector<Tab> getHalfLeastUsedTabs(Tab current) {
    373         Vector<Tab> tabsToGo = new Vector<Tab>();
    374 
    375         // Don't do anything if we only have 1 tab or if the current tab is
    376         // null.
    377         if (getTabCount() == 1 || current == null) {
    378             return tabsToGo;
    379         }
    380 
    381         if (mTabQueue.size() == 0) {
    382             return tabsToGo;
    383         }
    384 
    385         // Rip through the queue starting at the beginning and tear down half of
    386         // available tabs which are not the current tab or the parent of the
    387         // current tab.
    388         int openTabCount = 0;
    389         for (Tab t : mTabQueue) {
    390             if (t != null && t.getWebView() != null) {
    391                 openTabCount++;
    392                 if (t != current && t != current.getParentTab()) {
    393                     tabsToGo.add(t);
    394                 }
    395             }
    396         }
    397 
    398         openTabCount /= 2;
    399         if (tabsToGo.size() > openTabCount) {
    400             tabsToGo.setSize(openTabCount);
    401         }
    402 
    403         return tabsToGo;
    404     }
    405 
    406     /**
    407      * Show the tab that contains the given WebView.
    408      * @param view The WebView used to find the tab.
    409      */
    410     Tab getTabFromView(WebView view) {
    411         final int size = getTabCount();
    412         for (int i = 0; i < size; i++) {
    413             final Tab t = getTab(i);
    414             if (t.getSubWebView() == view || t.getWebView() == view) {
    415                 return t;
    416             }
    417         }
    418         return null;
    419     }
    420 
    421     /**
    422      * Return the tab with the matching application id.
    423      * @param id The application identifier.
    424      */
    425     Tab getTabFromId(String id) {
    426         if (id == null) {
    427             return null;
    428         }
    429         final int size = getTabCount();
    430         for (int i = 0; i < size; i++) {
    431             final Tab t = getTab(i);
    432             if (id.equals(t.getAppId())) {
    433                 return t;
    434             }
    435         }
    436         return null;
    437     }
    438 
    439     /**
    440      * Stop loading in all opened WebView including subWindows.
    441      */
    442     void stopAllLoading() {
    443         final int size = getTabCount();
    444         for (int i = 0; i < size; i++) {
    445             final Tab t = getTab(i);
    446             final WebView webview = t.getWebView();
    447             if (webview != null) {
    448                 webview.stopLoading();
    449             }
    450             final WebView subview = t.getSubWebView();
    451             if (subview != null) {
    452                 webview.stopLoading();
    453             }
    454         }
    455     }
    456 
    457     // This method checks if a non-app tab (one created within the browser)
    458     // matches the given url.
    459     private boolean tabMatchesUrl(Tab t, String url) {
    460         if (t.getAppId() != null) {
    461             return false;
    462         }
    463         WebView webview = t.getWebView();
    464         if (webview == null) {
    465             return false;
    466         } else if (url.equals(webview.getUrl())
    467                 || url.equals(webview.getOriginalUrl())) {
    468             return true;
    469         }
    470         return false;
    471     }
    472 
    473     /**
    474      * Return the tab that has no app id associated with it and the url of the
    475      * tab matches the given url.
    476      * @param url The url to search for.
    477      */
    478     Tab findUnusedTabWithUrl(String url) {
    479         if (url == null) {
    480             return null;
    481         }
    482         // Check the current tab first.
    483         Tab t = getCurrentTab();
    484         if (t != null && tabMatchesUrl(t, url)) {
    485             return t;
    486         }
    487         // Now check all the rest.
    488         final int size = getTabCount();
    489         for (int i = 0; i < size; i++) {
    490             t = getTab(i);
    491             if (tabMatchesUrl(t, url)) {
    492                 return t;
    493             }
    494         }
    495         return null;
    496     }
    497 
    498     /**
    499      * Recreate the main WebView of the given tab. Returns true if the WebView
    500      * requires a load, whether it was due to the fact that it was deleted, or
    501      * it is because it was a voice search.
    502      */
    503     boolean recreateWebView(Tab t, BrowserActivity.UrlData urlData) {
    504         final String url = urlData.mUrl;
    505         final WebView w = t.getWebView();
    506         if (w != null) {
    507             if (url != null && url.equals(t.getOriginalUrl())
    508                     // Treat a voice intent as though it is a different URL,
    509                     // since it most likely is.
    510                     && urlData.mVoiceIntent == null) {
    511                 // The original url matches the current url. Just go back to the
    512                 // first history item so we can load it faster than if we
    513                 // rebuilt the WebView.
    514                 final WebBackForwardList list = w.copyBackForwardList();
    515                 if (list != null) {
    516                     w.goBackOrForward(-list.getCurrentIndex());
    517                     w.clearHistory(); // maintains the current page.
    518                     return false;
    519                 }
    520             }
    521             t.destroy();
    522         }
    523         // Create a new WebView. If this tab is the current tab, we need to put
    524         // back all the clients so force it to be the current tab.
    525         t.setWebView(createNewWebView());
    526         if (getCurrentTab() == t) {
    527             setCurrentTab(t, true);
    528         }
    529         // Clear the saved state and picker data
    530         t.setSavedState(null);
    531         t.clearPickerData();
    532         // Save the new url in order to avoid deleting the WebView.
    533         t.setOriginalUrl(url);
    534         return true;
    535     }
    536 
    537     /**
    538      * Creates a new WebView and registers it with the global settings.
    539      */
    540     private WebView createNewWebView() {
    541         // Create a new WebView
    542         WebView w = new WebView(mActivity);
    543         w.setScrollbarFadingEnabled(true);
    544         w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
    545         w.setMapTrackballToArrowKeys(false); // use trackball directly
    546         // Enable the built-in zoom
    547         w.getSettings().setBuiltInZoomControls(true);
    548         // Add this WebView to the settings observer list and update the
    549         // settings
    550         final BrowserSettings s = BrowserSettings.getInstance();
    551         s.addObserver(w.getSettings()).update(s, null);
    552 
    553         // pick a default
    554         if (false) {
    555             MeshTracker mt = new MeshTracker(2);
    556             Paint paint = new Paint();
    557             Bitmap bm = BitmapFactory.decodeResource(mActivity.getResources(),
    558                                          R.drawable.pattern_carbon_fiber_dark);
    559             paint.setShader(new BitmapShader(bm, Shader.TileMode.REPEAT,
    560                                              Shader.TileMode.REPEAT));
    561             mt.setBGPaint(paint);
    562             w.setDragTracker(mt);
    563         }
    564         return w;
    565     }
    566 
    567     /**
    568      * Put the current tab in the background and set newTab as the current tab.
    569      * @param newTab The new tab. If newTab is null, the current tab is not
    570      *               set.
    571      */
    572     boolean setCurrentTab(Tab newTab) {
    573         return setCurrentTab(newTab, false);
    574     }
    575 
    576     void pauseCurrentTab() {
    577         Tab t = getCurrentTab();
    578         if (t != null) {
    579             t.pause();
    580         }
    581     }
    582 
    583     void resumeCurrentTab() {
    584         Tab t = getCurrentTab();
    585         if (t != null) {
    586             t.resume();
    587         }
    588     }
    589 
    590     /**
    591      * If force is true, this method skips the check for newTab == current.
    592      */
    593     private boolean setCurrentTab(Tab newTab, boolean force) {
    594         Tab current = getTab(mCurrentTab);
    595         if (current == newTab && !force) {
    596             return true;
    597         }
    598         if (current != null) {
    599             current.putInBackground();
    600             mCurrentTab = -1;
    601         }
    602         if (newTab == null) {
    603             return false;
    604         }
    605 
    606         // Move the newTab to the end of the queue
    607         int index = mTabQueue.indexOf(newTab);
    608         if (index != -1) {
    609             mTabQueue.remove(index);
    610         }
    611         mTabQueue.add(newTab);
    612 
    613         // Display the new current tab
    614         mCurrentTab = mTabs.indexOf(newTab);
    615         WebView mainView = newTab.getWebView();
    616         boolean needRestore = (mainView == null);
    617         if (needRestore) {
    618             // Same work as in createNewTab() except don't do new Tab()
    619             mainView = createNewWebView();
    620             newTab.setWebView(mainView);
    621         }
    622         newTab.putInForeground();
    623         if (needRestore) {
    624             // Have to finish setCurrentTab work before calling restoreState
    625             if (!newTab.restoreState(newTab.getSavedState())) {
    626                 mainView.loadUrl(BrowserSettings.getInstance().getHomePage());
    627             }
    628         }
    629         return true;
    630     }
    631 }
    632