diff options
author | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
---|---|---|
committer | The Android Open Source Project <initial-contribution@android.com> | 2008-10-21 07:00:00 -0700 |
commit | ba6d7b853c32ad6c3be26c443daa61f322bcfdc2 (patch) | |
tree | 1bb508b3b9cecf0b9f69271d4c4e953a5b5556f5 /src/com/android/browser/TabControl.java | |
download | android_packages_apps_Gello-ba6d7b853c32ad6c3be26c443daa61f322bcfdc2.tar.gz android_packages_apps_Gello-ba6d7b853c32ad6c3be26c443daa61f322bcfdc2.tar.bz2 android_packages_apps_Gello-ba6d7b853c32ad6c3be26c443daa61f322bcfdc2.zip |
Initial Contribution
Diffstat (limited to 'src/com/android/browser/TabControl.java')
-rw-r--r-- | src/com/android/browser/TabControl.java | 840 |
1 files changed, 840 insertions, 0 deletions
diff --git a/src/com/android/browser/TabControl.java b/src/com/android/browser/TabControl.java new file mode 100644 index 00000000..66adf3c0 --- /dev/null +++ b/src/com/android/browser/TabControl.java @@ -0,0 +1,840 @@ +/* + * Copyright (C) 2007 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.browser; + +import android.content.Context; +import android.os.Bundle; +import android.util.Config; +import android.util.Log; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnClickListener; +import android.webkit.JsPromptResult; +import android.webkit.JsResult; +import android.webkit.WebBackForwardList; +import android.webkit.WebChromeClient; +import android.webkit.WebHistoryItem; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; +import android.widget.ImageButton; + +import java.util.ArrayList; +import java.util.Vector; + +class TabControl { + // Log Tag + private static final String LOGTAG = "TabControl"; + // Maximum number of tabs. + static final int MAX_TABS = 8; + // Static instance of an empty callback. + private static final WebViewClient mEmptyClient = + new WebViewClient(); + // Instance of BackgroundChromeClient for background tabs. + private final BackgroundChromeClient mBackgroundChromeClient = + new BackgroundChromeClient(); + // Private array of WebViews that are used as tabs. + private ArrayList<Tab> mTabs = new ArrayList<Tab>(MAX_TABS); + // Queue of most recently viewed tabs. + private ArrayList<Tab> mTabQueue = new ArrayList<Tab>(MAX_TABS); + // Current position in mTabs. + private int mCurrentTab = -1; + // A private instance of BrowserActivity to interface with when adding and + // switching between tabs. + private final BrowserActivity mActivity; + // Inflation service for making subwindows. + private final LayoutInflater mInflateService; + // Subclass of WebViewClient used in subwindows to notify the main + // WebViewClient of certain WebView activities. + private class SubWindowClient extends WebViewClient { + // The main WebViewClient. + private final WebViewClient mClient; + + SubWindowClient(WebViewClient client) { + mClient = client; + } + @Override + public void doUpdateVisitedHistory(WebView view, String url, + boolean isReload) { + mClient.doUpdateVisitedHistory(view, url, isReload); + } + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + return mClient.shouldOverrideUrlLoading(view, url); + } + } + // Subclass of WebChromeClient to display javascript dialogs. + private class SubWindowChromeClient extends WebChromeClient { + // This subwindow's tab. + private final Tab mTab; + // The main WebChromeClient. + private final WebChromeClient mClient; + + SubWindowChromeClient(Tab t, WebChromeClient client) { + mTab = t; + mClient = client; + } + @Override + public void onProgressChanged(WebView view, int newProgress) { + mClient.onProgressChanged(view, newProgress); + } + @Override + public boolean onCreateWindow(WebView view, boolean dialog, + boolean userGesture, android.os.Message resultMsg) { + return mClient.onCreateWindow(view, dialog, userGesture, resultMsg); + } + @Override + public void onCloseWindow(WebView window) { + if (Config.DEBUG && window != mTab.mSubView) { + throw new AssertionError("Can't close the window"); + } + mActivity.dismissSubWindow(mTab); + } + // JavaScript functions + @Override + public boolean onJsAlert(WebView view, String url, String message, + JsResult result) { + return mClient.onJsAlert(view, url, message, result); + } + @Override + public boolean onJsConfirm(WebView view, String url, String message, + JsResult result) { + return mClient.onJsConfirm(view, url, message, result); + } + @Override + public boolean onJsPrompt(WebView view, String url, String message, + String initValue, JsPromptResult result) { + return mClient.onJsPrompt(view, url, message, initValue, result); + } + @Override + public boolean onJsBeforeUnload(WebView view, String url, + String message, JsResult result) { + return mClient.onJsBeforeUnload(view, url, message, result); + } + } + // Background WebChromeClient for focusing tabs + private class BackgroundChromeClient extends WebChromeClient { + @Override + public void onRequestFocus(WebView view) { + Tab t = getTabFromView(view); + if (t != getCurrentTab()) { + mActivity.showTab(t); + } + } + } + + /** + * Private class for maintaining Tabs with a main WebView and a subwindow. + */ + public class Tab { + // Main WebView + private WebView mMainView; + // Subwindow WebView + private WebView mSubView; + // Subwindow container + private View mSubViewContainer; + // Subwindow callback + private SubWindowClient mSubViewClient; + // Subwindow chrome callback + private SubWindowChromeClient mSubViewChromeClient; + // Saved bundle for when we are running low on memory. It contains the + // information needed to restore the WebView if the user goes back to + // the tab. + private Bundle mSavedState; + // Extra saved information for displaying the tab in the picker. + private String mUrl; + private String mTitle; + + // Parent Tab. This is the Tab that created this Tab, or null + // if the Tab was created by the UI + private Tab mParentTab; + // Tab that constructed by this Tab. This is used when this + // Tab is destroyed, it clears all mParentTab values in the + // children. + private Vector<Tab> mChildTabs; + + // Construct a new tab + private Tab(WebView w) { + mMainView = w; + } + + /** + * Return the top window of this tab; either the subwindow if it is not + * null or the main window. + * @return The top window of this tab. + */ + public WebView getTopWindow() { + if (mSubView != null) { + return mSubView; + } + return mMainView; + } + + /** + * Return the main window of this tab. Note: if a tab is freed in the + * background, this can return null. It is only guaranteed to be + * non-null for the current tab. + * @return The main WebView of this tab. + */ + public WebView getWebView() { + return mMainView; + } + + /** + * Return the subwindow of this tab or null if there is no subwindow. + * @return The subwindow of this tab or null. + */ + public WebView getSubWebView() { + return mSubView; + } + + /** + * Return the subwindow container of this tab or null if there is no + * subwindow. + * @return The subwindow's container View. + */ + public View getSubWebViewContainer() { + return mSubViewContainer; + } + + /** + * Get the url of this tab. Valid after calling populatePickerData, but + * before calling wipePickerData, or if the webview has been destroyed. + * + * @return The WebView's url or null. + */ + public String getUrl() { + return mUrl; + } + + /** + * Get the title of this tab. Valid after calling populatePickerData, + * but before calling wipePickerData, or if the webview has been + * destroyed. If the url has no title, use the url instead. + * + * @return The WebView's title (or url) or null. + */ + public String getTitle() { + return mTitle; + } + + private void setParentTab(Tab parent) { + mParentTab = parent; + } + + /** + * When a Tab is created through the content of another Tab, then + * we associate the Tabs. + * @param child the Tab that was created from this Tab + */ + public void addChildTab(Tab child) { + if (mChildTabs == null) { + mChildTabs = new Vector<Tab>(); + } + mChildTabs.add(child); + child.setParentTab(this); + } + + private void removeFromTree() { + // detach the children + if (mChildTabs != null) { + for(Tab t : mChildTabs) { + t.setParentTab(null); + } + } + + // Find myself in my parent list + if (mParentTab != null) { + mParentTab.mChildTabs.remove(this); + } + } + + /** + * If this Tab was created through another Tab, then this method + * returns that Tab. + * @return the Tab parent or null + */ + public Tab getParentTab() { + return mParentTab; + } + }; + + /** + * Construct a new TabControl object that interfaces with the given + * BrowserActivity instance. + * @param activity A BrowserActivity instance that TabControl will interface + * with. + */ + TabControl(BrowserActivity activity) { + mActivity = activity; + mInflateService = + ((LayoutInflater) activity.getSystemService( + Context.LAYOUT_INFLATER_SERVICE)); + } + + /** + * Return the current tab's main WebView. This will always return the main + * WebView for a given tab and not a subwindow. + * @return The current tab's WebView. + */ + WebView getCurrentWebView() { + Tab t = getTab(mCurrentTab); + if (t == null) { + return null; + } + return t.mMainView; + } + + /** + * Return the current tab's top-level WebView. This can return a subwindow + * if one exists. + * @return The top-level WebView of the current tab. + */ + WebView getCurrentTopWebView() { + Tab t = getTab(mCurrentTab); + if (t == null) { + return null; + } + return t.mSubView != null ? t.mSubView : t.mMainView; + } + + /** + * Return the current tab's subwindow if it exists. + * @return The subwindow of the current tab or null if it doesn't exist. + */ + WebView getCurrentSubWindow() { + Tab t = getTab(mCurrentTab); + if (t == null) { + return null; + } + return t.mSubView; + } + + /** + * Return the tab at the specified index. + * @return The Tab for the specified index or null if the tab does not + * exist. + */ + Tab getTab(int index) { + if (index >= 0 && index < mTabs.size()) { + return mTabs.get(index); + } + return null; + } + + /** + * Return the current tab. + * @return The current tab. + */ + Tab getCurrentTab() { + return getTab(mCurrentTab); + } + + /** + * Return the current tab index. + * @return The current tab index + */ + int getCurrentIndex() { + return mCurrentTab; + } + + /** + * Given a Tab, find it's index + * @param Tab to find + * @return index of Tab or -1 if not found + */ + int getTabIndex(Tab tab) { + return mTabs.indexOf(tab); + } + + /** + * Create a new tab and display the new tab immediately. + * @return The newly createTab or null if we have reached the maximum + * number of open tabs. + */ + Tab createNewTab() { + int size = mTabs.size(); + // Return false if we have maxed out on tabs + if (MAX_TABS == size) { + return null; + } + // Create a new WebView + WebView w = new WebView(mActivity); + w.setMapTrackballToArrowKeys(false); // use trackball directly + // Add this WebView to the settings observer list and update the + // settings + final BrowserSettings s = BrowserSettings.getInstance(); + s.addObserver(w.getSettings()).update(s, null); + // Create a new tab and add it to the tab list + Tab t = new Tab(w); + mTabs.add(t); + return t; + } + + /** + * Remove the tab from the list. If the tab is the current tab shown, the + * last created tab will be shown. + * @param t The tab to be removed. + */ + boolean removeTab(Tab t) { + if (t == null) { + return false; + } + // Only remove the tab if it is the current one. + if (getCurrentTab() == t) { + putTabInBackground(t); + } + + // Only destroy the WebView if it still exists. + if (t.mMainView != null) { + // Take down the sub window. + dismissSubWindow(t); + // Remove the WebView's settings from the BrowserSettings list of + // observers. + BrowserSettings.getInstance().deleteObserver( + t.mMainView.getSettings()); + // Destroy the main view and subview + t.mMainView.destroy(); + t.mMainView = null; + } + // clear it's references to parent and children + t.removeFromTree(); + + // Remove it from our list of tabs. + mTabs.remove(t); + // Remove it from the queue of viewed tabs. + mTabQueue.remove(t); + mCurrentTab = -1; + return true; + } + + /** + * Clear the back/forward list for all the current tabs. + */ + void clearHistory() { + int size = getTabCount(); + for (int i = 0; i < size; i++) { + Tab t = mTabs.get(i); + // TODO: if a tab is freed due to low memory, its history is not + // cleared here. + if (t.mMainView != null) { + t.mMainView.clearHistory(); + } + if (t.mSubView != null) { + t.mSubView.clearHistory(); + } + } + } + + /** + * Destroy all the tabs and subwindows + */ + void destroy() { + BrowserSettings s = BrowserSettings.getInstance(); + for (Tab t : mTabs) { + if (t.mMainView != null) { + dismissSubWindow(t); + s.deleteObserver(t.mMainView.getSettings()); + t.mMainView.destroy(); + t.mMainView = null; + } + } + mTabs.clear(); + mTabQueue.clear(); + } + + /** + * Returns the number of tabs created. + * @return The number of tabs created. + */ + int getTabCount() { + return mTabs.size(); + } + + // Used for saving and restoring each Tab + private static final String WEBVIEW = "webview"; + private static final String NUMTABS = "numTabs"; + private static final String CURRTAB = "currentTab"; + private static final String CURRURL = "currentUrl"; + private static final String CURRTITLE = "currentTitle"; + + /** + * Save the state of all the Tabs. + * @param outState The Bundle to save the state to. + */ + void saveState(Bundle outState) { + final int numTabs = getTabCount(); + outState.putInt(NUMTABS, numTabs); + final int index = getCurrentIndex(); + outState.putInt(CURRTAB, (index >= 0 && index < numTabs) ? index : 0); + for (int i = 0; i < numTabs; i++) { + final Tab t = getTab(i); + if (saveState(t)) { + outState.putBundle(WEBVIEW + i, t.mSavedState); + } + } + } + + /** + * Restore the state of all the tabs. + * @param inState The saved state of all the tabs. + * @return True if there were previous tabs that were restored. False if + * there was no saved state or restoring the state failed. + */ + boolean restoreState(Bundle inState) { + final int numTabs = (inState == null) + ? -1 : inState.getInt(NUMTABS, -1); + if (numTabs == -1) { + return false; + } else { + final int currentTab = inState.getInt(CURRTAB, -1); + for (int i = 0; i < numTabs; i++) { + if (i == currentTab) { + Tab t = createNewTab(); + // Me must set the current tab before restoring the state + // so that all the client classes are set. + setCurrentTab(t); + if (!restoreState(inState.getBundle(WEBVIEW + i), t)) { + Log.w(LOGTAG, "Fail in restoreState, load home page."); + t.mMainView.loadUrl(BrowserSettings.getInstance() + .getHomePage()); + } + } else { + // Create a new tab and don't restore the state yet, add it + // to the tab list + Tab t = new Tab(null); + t.mSavedState = inState.getBundle(WEBVIEW + i); + if (t.mSavedState != null) { + t.mUrl = t.mSavedState.getString(CURRURL); + t.mTitle = t.mSavedState.getString(CURRTITLE); + } + mTabs.add(t); + mTabQueue.add(t); + } + } + } + return true; + } + + /** + * Free the memory in this order, 1) free the background tab; 2) free the + * WebView cache; + */ + void freeMemory() { + // free the least frequently used background tab + Tab t = getLeastUsedTab(); + if (t != null) { + Log.w(LOGTAG, "Free a tab in the browser"); + freeTab(t); + // force a gc + System.gc(); + return; + } + + // free the WebView cache + Log.w(LOGTAG, "Free WebView cache"); + WebView view = getCurrentWebView(); + view.clearCache(false); + // force a gc + System.gc(); + } + + private Tab getLeastUsedTab() { + // Don't do anything if we only have 1 tab. + if (getTabCount() == 1) { + return null; + } + + // Rip through the queue starting at the beginning and teardown the + // next available tab. + Tab t = null; + int i = 0; + final int queueSize = mTabQueue.size(); + if (queueSize == 0) { + return null; + } + do { + t = mTabQueue.get(i++); + } while (i < queueSize && t != null && t.mMainView == null); + + // Don't do anything if the last remaining tab is the current one. + if (t == getCurrentTab()) { + return null; + } + + return t; + } + + private void freeTab(Tab t) { + // Store the WebView's state. + saveState(t); + + // Tear down the tab. + dismissSubWindow(t); + // Remove the WebView's settings from the BrowserSettings list of + // observers. + BrowserSettings.getInstance().deleteObserver(t.mMainView.getSettings()); + t.mMainView.destroy(); + t.mMainView = null; + } + + /** + * Create a new subwindow unless a subwindow already exists. + * @return True if a new subwindow was created. False if one already exists. + */ + void createSubWindow() { + Tab t = getTab(mCurrentTab); + if (t != null && t.mSubView == null) { + final View v = mInflateService.inflate(R.layout.browser_subwindow, null); + final WebView w = (WebView) v.findViewById(R.id.webview); + w.setMapTrackballToArrowKeys(false); // use trackball directly + final SubWindowClient subClient = + new SubWindowClient(mActivity.getWebViewClient()); + final SubWindowChromeClient subChromeClient = + new SubWindowChromeClient(t, + mActivity.getWebChromeClient()); + w.setWebViewClient(subClient); + w.setWebChromeClient(subChromeClient); + w.setDownloadListener(mActivity); + w.setOnCreateContextMenuListener(mActivity); + final BrowserSettings s = BrowserSettings.getInstance(); + s.addObserver(w.getSettings()).update(s, null); + t.mSubView = w; + t.mSubViewClient = subClient; + t.mSubViewChromeClient = subChromeClient; + // FIXME: I really hate having to know the name of the view + // containing the webview. + t.mSubViewContainer = v.findViewById(R.id.subwindow_container); + final ImageButton cancel = + (ImageButton) v.findViewById(R.id.subwindow_close); + cancel.setOnClickListener(new OnClickListener() { + public void onClick(View v) { + subChromeClient.onCloseWindow(w); + } + }); + } + } + + /** + * Show the tab that contains the given WebView. + * @param view The WebView used to find the tab. + */ + Tab getTabFromView(WebView view) { + final int size = getTabCount(); + for (int i = 0; i < size; i++) { + final Tab t = getTab(i); + if (t.mSubView == view || t.mMainView == view) { + return t; + } + } + return null; + } + + /** + * Put the current tab in the background and set newTab as the current tab. + * @param newTab The new tab. If newTab is null, the current tab is not + * set. + */ + boolean setCurrentTab(Tab newTab) { + Tab current = getTab(mCurrentTab); + if (current == newTab) { + return true; + } + if (current != null) { + // Remove the current WebView and the container of the subwindow + putTabInBackground(current); + } + + if (newTab == null) { + return false; + } + + // Move the newTab to the end of the queue + int index = mTabQueue.indexOf(newTab); + if (index != -1) { + mTabQueue.remove(index); + } + mTabQueue.add(newTab); + + WebView mainView; + WebView subView; + + // Display the new current tab + mCurrentTab = mTabs.indexOf(newTab); + mainView = newTab.mMainView; + boolean needRestore = (mainView == null); + if (needRestore) { + // Same work as in createNewTab() except don't do new Tab() + newTab.mMainView = mainView = new WebView(mActivity); + mainView.setMapTrackballToArrowKeys(false); // use t-ball directly + + // Add this WebView to the settings observer list and update the + // settings + final BrowserSettings s = BrowserSettings.getInstance(); + s.addObserver(mainView.getSettings()).update(s, null); + } + mainView.setWebViewClient(mActivity.getWebViewClient()); + mainView.setWebChromeClient(mActivity.getWebChromeClient()); + mainView.setOnCreateContextMenuListener(mActivity); + mainView.setDownloadListener(mActivity); + // Add the subwindow if it exists + if (newTab.mSubViewContainer != null) { + subView = newTab.mSubView; + subView.setWebViewClient(newTab.mSubViewClient); + subView.setWebChromeClient(newTab.mSubViewChromeClient); + subView.setOnCreateContextMenuListener(mActivity); + subView.setDownloadListener(mActivity); + } + if (needRestore) { + // Have to finish setCurrentTab work before calling restoreState + if (!restoreState(newTab.mSavedState, newTab)) { + mainView.loadUrl(BrowserSettings.getInstance().getHomePage()); + } + } + return true; + } + + /* + * Put the tab in the background using all the empty/background clients. + */ + private void putTabInBackground(Tab t) { + WebView mainView = t.mMainView; + // Set an empty callback so that default actions are not triggered. + mainView.setWebViewClient(mEmptyClient); + mainView.setWebChromeClient(mBackgroundChromeClient); + mainView.setOnCreateContextMenuListener(null); + // Leave the DownloadManager attached so that downloads can start in + // a non-active window. This can happen when going to a site that does + // a redirect after a period of time. The user could have switched to + // another tab while waiting for the download to start. + mainView.setDownloadListener(mActivity); + WebView subView = t.mSubView; + if (subView != null) { + // Set an empty callback so that default actions are not triggered. + subView.setWebViewClient(mEmptyClient); + subView.setWebChromeClient(mBackgroundChromeClient); + subView.setOnCreateContextMenuListener(null); + subView.setDownloadListener(mActivity); + } + } + + /* + * Dismiss the subwindow for the given tab. + */ + void dismissSubWindow(Tab t) { + if (t != null && t.mSubView != null) { + BrowserSettings.getInstance().deleteObserver( + t.mSubView.getSettings()); + t.mSubView.destroy(); + t.mSubView = null; + t.mSubViewContainer = null; + } + } + + /** + * Ensure that Tab t has a title, url, and favicon. + * @param t Tab to populate. + */ + /* package */ void populatePickerData(Tab t) { + if (t == null || t.mMainView == null) { + return; + } + // FIXME: The only place we cared about subwindow was for + // bookmarking (i.e. not when saving state). Was this deliberate? + final WebBackForwardList list = t.mMainView.copyBackForwardList(); + final WebHistoryItem item = + list != null ? list.getCurrentItem() : null; + populatePickerData(t, item); + } + + // Populate the picker data + private void populatePickerData(Tab t, WebHistoryItem item) { + if (item != null) { + t.mUrl = item.getUrl(); + t.mTitle = item.getTitle(); + if (t.mTitle == null) { + t.mTitle = t.mUrl; + } + } + } + + /** + * Clean up the data for all tabs. + */ + /* package */ void wipeAllPickerData() { + int size = getTabCount(); + for (int i = 0; i < size; i++) { + final Tab t = getTab(i); + if (t != null && t.mSavedState == null) { + t.mUrl = null; + t.mTitle = null; + } + } + } + + /* + * Save the state for an individual tab. + */ + private boolean saveState(Tab t) { + if (t != null) { + final WebView w = t.mMainView; + // If the WebView is null it means we ran low on memory and we + // already stored the saved state in mSavedState. + if (w == null) { + return true; + } + final Bundle b = new Bundle(); + final WebBackForwardList list = w.saveState(b); + + // Store some extra info for displaying the tab in the picker. + final WebHistoryItem item = + list != null ? list.getCurrentItem() : null; + populatePickerData(t, item); + if (t.mUrl != null) { + b.putString(CURRURL, t.mUrl); + } + if (t.mTitle != null) { + b.putString(CURRTITLE, t.mTitle); + } + + // Remember the saved state. + t.mSavedState = b; + return true; + } + return false; + } + + /* + * Restore the state of the tab. + */ + private boolean restoreState(Bundle b, Tab t) { + if (b == null) { + return false; + } + final WebView w = t.mMainView; + final WebBackForwardList list = w.restoreState(b); + if (list == null) { + return false; + } + t.mSavedState = null; + t.mUrl = null; + t.mTitle = null; + return true; + } +} |