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         // Remove it from the queue of viewed tabs.
    233         mTabQueue.remove(t);
    234         return true;
    235     }
    236 
    237     /**
    238      * Destroy all the tabs and subwindows
    239      */
    240     void destroy() {
    241         for (Tab t : mTabs) {
    242             t.destroy();
    243         }
    244         mTabs.clear();
    245         mTabQueue.clear();
    246     }
    247 
    248     /**
    249      * Returns the number of tabs created.
    250      * @return The number of tabs created.
    251      */
    252     int getTabCount() {
    253         return mTabs.size();
    254     }
    255 
    256 
    257     /**
    258      * Save the state of all the Tabs.
    259      * @param outState The Bundle to save the state to.
    260      */
    261     void saveState(Bundle outState) {
    262         final int numTabs = getTabCount();
    263         outState.putInt(Tab.NUMTABS, numTabs);
    264         final int index = getCurrentIndex();
    265         outState.putInt(Tab.CURRTAB, (index >= 0 && index < numTabs) ? index : 0);
    266         for (int i = 0; i < numTabs; i++) {
    267             final Tab t = getTab(i);
    268             if (t.saveState()) {
    269                 outState.putBundle(Tab.WEBVIEW + i, t.getSavedState());
    270             }
    271         }
    272     }
    273 
    274     /**
    275      * Restore the state of all the tabs.
    276      * @param inState The saved state of all the tabs.
    277      * @return True if there were previous tabs that were restored. False if
    278      *         there was no saved state or restoring the state failed.
    279      */
    280     boolean restoreState(Bundle inState) {
    281         final int numTabs = (inState == null)
    282                 ? -1 : inState.getInt(Tab.NUMTABS, -1);
    283         if (numTabs == -1) {
    284             return false;
    285         } else {
    286             final int currentTab = inState.getInt(Tab.CURRTAB, -1);
    287             for (int i = 0; i < numTabs; i++) {
    288                 if (i == currentTab) {
    289                     Tab t = createNewTab();
    290                     // Me must set the current tab before restoring the state
    291                     // so that all the client classes are set.
    292                     setCurrentTab(t);
    293                     if (!t.restoreState(inState.getBundle(Tab.WEBVIEW + i))) {
    294                         Log.w(LOGTAG, "Fail in restoreState, load home page.");
    295                         t.getWebView().loadUrl(BrowserSettings.getInstance()
    296                                 .getHomePage());
    297                     }
    298                 } else {
    299                     // Create a new tab and don't restore the state yet, add it
    300                     // to the tab list
    301                     Tab t = new Tab(mActivity, null, false, null, null);
    302                     Bundle state = inState.getBundle(Tab.WEBVIEW + i);
    303                     if (state != null) {
    304                         t.setSavedState(state);
    305                         t.populatePickerDataFromSavedState();
    306                         // Need to maintain the app id and original url so we
    307                         // can possibly reuse this tab.
    308                         t.setAppId(state.getString(Tab.APPID));
    309                         t.setOriginalUrl(state.getString(Tab.ORIGINALURL));
    310                     }
    311                     mTabs.add(t);
    312                     // added the tab to the front as they are not current
    313                     mTabQueue.add(0, t);
    314                 }
    315             }
    316             // Rebuild the tree of tabs. Do this after all tabs have been
    317             // created/restored so that the parent tab exists.
    318             for (int i = 0; i < numTabs; i++) {
    319                 final Bundle b = inState.getBundle(Tab.WEBVIEW + i);
    320                 final Tab t = getTab(i);
    321                 if (b != null && t != null) {
    322                     final int parentIndex = b.getInt(Tab.PARENTTAB, -1);
    323                     if (parentIndex != -1) {
    324                         final Tab parent = getTab(parentIndex);
    325                         if (parent != null) {
    326                             parent.addChildTab(t);
    327                         }
    328                     }
    329                 }
    330             }
    331         }
    332         return true;
    333     }
    334 
    335     /**
    336      * Free the memory in this order, 1) free the background tabs; 2) free the
    337      * WebView cache;
    338      */
    339     void freeMemory() {
    340         if (getTabCount() == 0) return;
    341 
    342         // free the least frequently used background tabs
    343         Vector<Tab> tabs = getHalfLeastUsedTabs(getCurrentTab());
    344         if (tabs.size() > 0) {
    345             Log.w(LOGTAG, "Free " + tabs.size() + " tabs in the browser");
    346             for (Tab t : tabs) {
    347                 // store the WebView's state.
    348                 t.saveState();
    349                 // destroy the tab
    350                 t.destroy();
    351             }
    352             return;
    353         }
    354 
    355         // free the WebView's unused memory (this includes the cache)
    356         Log.w(LOGTAG, "Free WebView's unused memory and cache");
    357         WebView view = getCurrentWebView();
    358         if (view != null) {
    359             view.freeMemory();
    360         }
    361     }
    362 
    363     private Vector<Tab> getHalfLeastUsedTabs(Tab current) {
    364         Vector<Tab> tabsToGo = new Vector<Tab>();
    365 
    366         // Don't do anything if we only have 1 tab or if the current tab is
    367         // null.
    368         if (getTabCount() == 1 || current == null) {
    369             return tabsToGo;
    370         }
    371 
    372         if (mTabQueue.size() == 0) {
    373             return tabsToGo;
    374         }
    375 
    376         // Rip through the queue starting at the beginning and tear down half of
    377         // available tabs which are not the current tab or the parent of the
    378         // current tab.
    379         int openTabCount = 0;
    380         for (Tab t : mTabQueue) {
    381             if (t != null && t.getWebView() != null) {
    382                 openTabCount++;
    383                 if (t != current && t != current.getParentTab()) {
    384                     tabsToGo.add(t);
    385                 }
    386             }
    387         }
    388 
    389         openTabCount /= 2;
    390         if (tabsToGo.size() > openTabCount) {
    391             tabsToGo.setSize(openTabCount);
    392         }
    393 
    394         return tabsToGo;
    395     }
    396 
    397     /**
    398      * Show the tab that contains the given WebView.
    399      * @param view The WebView used to find the tab.
    400      */
    401     Tab getTabFromView(WebView view) {
    402         final int size = getTabCount();
    403         for (int i = 0; i < size; i++) {
    404             final Tab t = getTab(i);
    405             if (t.getSubWebView() == view || t.getWebView() == view) {
    406                 return t;
    407             }
    408         }
    409         return null;
    410     }
    411 
    412     /**
    413      * Return the tab with the matching application id.
    414      * @param id The application identifier.
    415      */
    416     Tab getTabFromId(String id) {
    417         if (id == null) {
    418             return null;
    419         }
    420         final int size = getTabCount();
    421         for (int i = 0; i < size; i++) {
    422             final Tab t = getTab(i);
    423             if (id.equals(t.getAppId())) {
    424                 return t;
    425             }
    426         }
    427         return null;
    428     }
    429 
    430     /**
    431      * Stop loading in all opened WebView including subWindows.
    432      */
    433     void stopAllLoading() {
    434         final int size = getTabCount();
    435         for (int i = 0; i < size; i++) {
    436             final Tab t = getTab(i);
    437             final WebView webview = t.getWebView();
    438             if (webview != null) {
    439                 webview.stopLoading();
    440             }
    441             final WebView subview = t.getSubWebView();
    442             if (subview != null) {
    443                 webview.stopLoading();
    444             }
    445         }
    446     }
    447 
    448     // This method checks if a non-app tab (one created within the browser)
    449     // matches the given url.
    450     private boolean tabMatchesUrl(Tab t, String url) {
    451         if (t.getAppId() != null) {
    452             return false;
    453         }
    454         WebView webview = t.getWebView();
    455         if (webview == null) {
    456             return false;
    457         } else if (url.equals(webview.getUrl())
    458                 || url.equals(webview.getOriginalUrl())) {
    459             return true;
    460         }
    461         return false;
    462     }
    463 
    464     /**
    465      * Return the tab that has no app id associated with it and the url of the
    466      * tab matches the given url.
    467      * @param url The url to search for.
    468      */
    469     Tab findUnusedTabWithUrl(String url) {
    470         if (url == null) {
    471             return null;
    472         }
    473         // Check the current tab first.
    474         Tab t = getCurrentTab();
    475         if (t != null && tabMatchesUrl(t, url)) {
    476             return t;
    477         }
    478         // Now check all the rest.
    479         final int size = getTabCount();
    480         for (int i = 0; i < size; i++) {
    481             t = getTab(i);
    482             if (tabMatchesUrl(t, url)) {
    483                 return t;
    484             }
    485         }
    486         return null;
    487     }
    488 
    489     /**
    490      * Recreate the main WebView of the given tab. Returns true if the WebView
    491      * requires a load, whether it was due to the fact that it was deleted, or
    492      * it is because it was a voice search.
    493      */
    494     boolean recreateWebView(Tab t, BrowserActivity.UrlData urlData) {
    495         final String url = urlData.mUrl;
    496         final WebView w = t.getWebView();
    497         if (w != null) {
    498             if (url != null && url.equals(t.getOriginalUrl())
    499                     // Treat a voice intent as though it is a different URL,
    500                     // since it most likely is.
    501                     && urlData.mVoiceIntent == null) {
    502                 // The original url matches the current url. Just go back to the
    503                 // first history item so we can load it faster than if we
    504                 // rebuilt the WebView.
    505                 final WebBackForwardList list = w.copyBackForwardList();
    506                 if (list != null) {
    507                     w.goBackOrForward(-list.getCurrentIndex());
    508                     w.clearHistory(); // maintains the current page.
    509                     return false;
    510                 }
    511             }
    512             t.destroy();
    513         }
    514         // Create a new WebView. If this tab is the current tab, we need to put
    515         // back all the clients so force it to be the current tab.
    516         t.setWebView(createNewWebView());
    517         if (getCurrentTab() == t) {
    518             setCurrentTab(t, true);
    519         }
    520         // Clear the saved state and picker data
    521         t.setSavedState(null);
    522         t.clearPickerData();
    523         // Save the new url in order to avoid deleting the WebView.
    524         t.setOriginalUrl(url);
    525         return true;
    526     }
    527 
    528     /**
    529      * Creates a new WebView and registers it with the global settings.
    530      */
    531     private WebView createNewWebView() {
    532         // Create a new WebView
    533         WebView w = new WebView(mActivity);
    534         w.setScrollbarFadingEnabled(true);
    535         w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
    536         w.setMapTrackballToArrowKeys(false); // use trackball directly
    537         // Enable the built-in zoom
    538         w.getSettings().setBuiltInZoomControls(true);
    539         // Add this WebView to the settings observer list and update the
    540         // settings
    541         final BrowserSettings s = BrowserSettings.getInstance();
    542         s.addObserver(w.getSettings()).update(s, null);
    543 
    544         // pick a default
    545         if (false) {
    546             MeshTracker mt = new MeshTracker(2);
    547             Paint paint = new Paint();
    548             Bitmap bm = BitmapFactory.decodeResource(mActivity.getResources(),
    549                                          R.drawable.pattern_carbon_fiber_dark);
    550             paint.setShader(new BitmapShader(bm, Shader.TileMode.REPEAT,
    551                                              Shader.TileMode.REPEAT));
    552             mt.setBGPaint(paint);
    553             w.setDragTracker(mt);
    554         }
    555         return w;
    556     }
    557 
    558     /**
    559      * Put the current tab in the background and set newTab as the current tab.
    560      * @param newTab The new tab. If newTab is null, the current tab is not
    561      *               set.
    562      */
    563     boolean setCurrentTab(Tab newTab) {
    564         return setCurrentTab(newTab, false);
    565     }
    566 
    567     void pauseCurrentTab() {
    568         Tab t = getCurrentTab();
    569         if (t != null) {
    570             t.pause();
    571         }
    572     }
    573 
    574     void resumeCurrentTab() {
    575         Tab t = getCurrentTab();
    576         if (t != null) {
    577             t.resume();
    578         }
    579     }
    580 
    581     /**
    582      * If force is true, this method skips the check for newTab == current.
    583      */
    584     private boolean setCurrentTab(Tab newTab, boolean force) {
    585         Tab current = getTab(mCurrentTab);
    586         if (current == newTab && !force) {
    587             return true;
    588         }
    589         if (current != null) {
    590             current.putInBackground();
    591             mCurrentTab = -1;
    592         }
    593         if (newTab == null) {
    594             return false;
    595         }
    596 
    597         // Move the newTab to the end of the queue
    598         int index = mTabQueue.indexOf(newTab);
    599         if (index != -1) {
    600             mTabQueue.remove(index);
    601         }
    602         mTabQueue.add(newTab);
    603 
    604         // Display the new current tab
    605         mCurrentTab = mTabs.indexOf(newTab);
    606         WebView mainView = newTab.getWebView();
    607         boolean needRestore = (mainView == null);
    608         if (needRestore) {
    609             // Same work as in createNewTab() except don't do new Tab()
    610             mainView = createNewWebView();
    611             newTab.setWebView(mainView);
    612         }
    613         newTab.putInForeground();
    614         if (needRestore) {
    615             // Have to finish setCurrentTab work before calling restoreState
    616             if (!newTab.restoreState(newTab.getSavedState())) {
    617                 mainView.loadUrl(BrowserSettings.getInstance().getHomePage());
    618             }
    619         }
    620         return true;
    621     }
    622 }
    623