OSDN Git Service

Allow going back to a voice search to work when choosing from n-best.
[android-x86/packages-apps-Browser.git] / src / com / android / browser / Tab.java
index 3bb136c..535e8e7 100644 (file)
 package com.android.browser;
 
 import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedList;
+import java.util.Map;
 import java.util.Vector;
 
 import android.app.AlertDialog;
+import android.app.SearchManager;
 import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteException;
@@ -34,20 +40,25 @@ import android.net.http.SslError;
 import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Message;
+import android.os.SystemClock;
 import android.provider.Browser;
+import android.speech.RecognizerResultsIntent;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.View.OnClickListener;
+import android.webkit.ConsoleMessage;
 import android.webkit.CookieSyncManager;
+import android.webkit.DownloadListener;
 import android.webkit.GeolocationPermissions;
 import android.webkit.HttpAuthHandler;
 import android.webkit.SslErrorHandler;
 import android.webkit.URLUtil;
 import android.webkit.ValueCallback;
 import android.webkit.WebBackForwardList;
+import android.webkit.WebBackForwardListClient;
 import android.webkit.WebChromeClient;
 import android.webkit.WebHistoryItem;
 import android.webkit.WebIconDatabase;
@@ -59,12 +70,19 @@ import android.widget.ImageButton;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import com.android.common.speech.LoggingEvents;
+
 /**
  * Class for maintaining Tabs with a main WebView and a subwindow.
  */
 class Tab {
     // Log Tag
     private static final String LOGTAG = "Tab";
+    // Special case the logtag for messages for the Console to make it easier to
+    // filter them and match the logtag used for these messages in older versions
+    // of the browser.
+    private static final String CONSOLE_LOGTAG = "browser";
+
     // The Geolocation permissions prompt
     private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt;
     // Main WebView wrapper
@@ -93,6 +111,8 @@ class Tab {
     private boolean mInForeground;
     // If true, the tab is in loading state.
     private boolean mInLoad;
+    // The time the load started, used to find load page time
+    private long mLoadStartTime;
     // Application identifier used to find tabs that another application wants
     // to reuse.
     private String mAppId;
@@ -108,6 +128,11 @@ class Tab {
     private final LayoutInflater mInflateService;
     // The BrowserActivity which owners the Tab
     private final BrowserActivity mActivity;
+    // The listener that gets invoked when a download is started from the
+    // mMainView
+    private final DownloadListener mDownloadListener;
+    // Listener used to know when we move forward or back in the history list.
+    private final WebBackForwardListClient mWebBackForwardListClient;
 
     // AsyncTask for downloading touch icons
     DownloadTouchIcon mTouchIconLoader;
@@ -133,6 +158,218 @@ class Tab {
 
     // -------------------------------------------------------------------------
 
+    /**
+     * Private information regarding the latest voice search.  If the Tab is not
+     * in voice search mode, this will be null.
+     */
+    private VoiceSearchData mVoiceSearchData;
+    /**
+     * Return whether the tab is in voice search mode.
+     */
+    public boolean isInVoiceSearchMode() {
+        return mVoiceSearchData != null;
+    }
+    /**
+     * Return true if the voice search Intent came with a String identifying
+     * that Google provided the Intent.
+     */
+    public boolean voiceSearchSourceIsGoogle() {
+        return mVoiceSearchData != null && mVoiceSearchData.mSourceIsGoogle;
+    }
+    /**
+     * Get the title to display for the current voice search page.  If the Tab
+     * is not in voice search mode, return null.
+     */
+    public String getVoiceDisplayTitle() {
+        if (mVoiceSearchData == null) return null;
+        return mVoiceSearchData.mLastVoiceSearchTitle;
+    }
+    /**
+     * Get the latest array of voice search results, to be passed to the
+     * BrowserProvider.  If the Tab is not in voice search mode, return null.
+     */
+    public ArrayList<String> getVoiceSearchResults() {
+        if (mVoiceSearchData == null) return null;
+        return mVoiceSearchData.mVoiceSearchResults;
+    }
+    /**
+     * Activate voice search mode.
+     * @param intent Intent which has the results to use, or an index into the
+     *      results when reusing the old results.
+     */
+    /* package */ void activateVoiceSearchMode(Intent intent) {
+        int index = 0;
+        ArrayList<String> results = intent.getStringArrayListExtra(
+                    RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_STRINGS);
+        if (results != null) {
+            ArrayList<String> urls = intent.getStringArrayListExtra(
+                        RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_URLS);
+            ArrayList<String> htmls = intent.getStringArrayListExtra(
+                        RecognizerResultsIntent.EXTRA_VOICE_SEARCH_RESULT_HTML);
+            ArrayList<String> baseUrls = intent.getStringArrayListExtra(
+                        RecognizerResultsIntent
+                        .EXTRA_VOICE_SEARCH_RESULT_HTML_BASE_URLS);
+            // This tab is now entering voice search mode for the first time, or
+            // a new voice search was done.
+            int size = results.size();
+            if (urls == null || size != urls.size()) {
+                throw new AssertionError("improper extras passed in Intent");
+            }
+            if (htmls == null || htmls.size() != size || baseUrls == null ||
+                    (baseUrls.size() != size && baseUrls.size() != 1)) {
+                // If either of these arrays are empty/incorrectly sized, ignore
+                // them.
+                htmls = null;
+                baseUrls = null;
+            }
+            mVoiceSearchData = new VoiceSearchData(results, urls, htmls,
+                    baseUrls);
+            mVoiceSearchData.mHeaders = intent.getParcelableArrayListExtra(
+                    RecognizerResultsIntent
+                    .EXTRA_VOICE_SEARCH_RESULT_HTTP_HEADERS);
+            mVoiceSearchData.mSourceIsGoogle = intent.getBooleanExtra(
+                    VoiceSearchData.SOURCE_IS_GOOGLE, false);
+            mVoiceSearchData.mVoiceSearchIntent = new Intent(intent);
+        }
+        String extraData = intent.getStringExtra(
+                SearchManager.EXTRA_DATA_KEY);
+        if (extraData != null) {
+            index = Integer.parseInt(extraData);
+            if (index >= mVoiceSearchData.mVoiceSearchResults.size()) {
+                throw new AssertionError("index must be less than "
+                        + "size of mVoiceSearchResults");
+            }
+            if (mVoiceSearchData.mSourceIsGoogle) {
+                Intent logIntent = new Intent(
+                        LoggingEvents.ACTION_LOG_EVENT);
+                logIntent.putExtra(LoggingEvents.EXTRA_EVENT,
+                        LoggingEvents.VoiceSearch.N_BEST_CHOOSE);
+                logIntent.putExtra(
+                        LoggingEvents.VoiceSearch.EXTRA_N_BEST_CHOOSE_INDEX,
+                        index);
+                mActivity.sendBroadcast(logIntent);
+            }
+            if (mVoiceSearchData.mVoiceSearchIntent != null) {
+                // Copy the Intent, so that each history item will have its own
+                // Intent, with different (or none) extra data.
+                Intent latest = new Intent(mVoiceSearchData.mVoiceSearchIntent);
+                latest.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+                mVoiceSearchData.mVoiceSearchIntent = latest;
+            }
+        }
+        mVoiceSearchData.mLastVoiceSearchTitle
+                = mVoiceSearchData.mVoiceSearchResults.get(index);
+        if (mInForeground) {
+            mActivity.showVoiceTitleBar(mVoiceSearchData.mLastVoiceSearchTitle);
+        }
+        if (mVoiceSearchData.mVoiceSearchHtmls != null) {
+            // When index was found it was already ensured that it was valid
+            String uriString = mVoiceSearchData.mVoiceSearchHtmls.get(index);
+            if (uriString != null) {
+                Uri dataUri = Uri.parse(uriString);
+                if (RecognizerResultsIntent.URI_SCHEME_INLINE.equals(
+                        dataUri.getScheme())) {
+                    // If there is only one base URL, use it.  If there are
+                    // more, there will be one for each index, so use the base
+                    // URL corresponding to the index.
+                    String baseUrl = mVoiceSearchData.mVoiceSearchBaseUrls.get(
+                            mVoiceSearchData.mVoiceSearchBaseUrls.size() > 1 ?
+                            index : 0);
+                    mVoiceSearchData.mLastVoiceSearchUrl = baseUrl;
+                    mMainView.loadDataWithBaseURL(baseUrl,
+                            uriString.substring(RecognizerResultsIntent
+                            .URI_SCHEME_INLINE.length() + 1), "text/html",
+                            "utf-8", baseUrl);
+                    return;
+                }
+            }
+        }
+        mVoiceSearchData.mLastVoiceSearchUrl
+                = mVoiceSearchData.mVoiceSearchUrls.get(index);
+        if (null == mVoiceSearchData.mLastVoiceSearchUrl) {
+            mVoiceSearchData.mLastVoiceSearchUrl = mActivity.smartUrlFilter(
+                    mVoiceSearchData.mLastVoiceSearchTitle);
+        }
+        Map<String, String> headers = null;
+        if (mVoiceSearchData.mHeaders != null) {
+            int bundleIndex = mVoiceSearchData.mHeaders.size() == 1 ? 0
+                    : index;
+            Bundle bundle = mVoiceSearchData.mHeaders.get(bundleIndex);
+            if (bundle != null && !bundle.isEmpty()) {
+                Iterator<String> iter = bundle.keySet().iterator();
+                headers = new HashMap<String, String>();
+                while (iter.hasNext()) {
+                    String key = iter.next();
+                    headers.put(key, bundle.getString(key));
+                }
+            }
+        }
+        mMainView.loadUrl(mVoiceSearchData.mLastVoiceSearchUrl, headers);
+    }
+    /* package */ static class VoiceSearchData {
+        public VoiceSearchData(ArrayList<String> results,
+                ArrayList<String> urls, ArrayList<String> htmls,
+                ArrayList<String> baseUrls) {
+            mVoiceSearchResults = results;
+            mVoiceSearchUrls = urls;
+            mVoiceSearchHtmls = htmls;
+            mVoiceSearchBaseUrls = baseUrls;
+        }
+        /*
+         * ArrayList of suggestions to be displayed when opening the
+         * SearchManager
+         */
+        public ArrayList<String> mVoiceSearchResults;
+        /*
+         * ArrayList of urls, associated with the suggestions in
+         * mVoiceSearchResults.
+         */
+        public ArrayList<String> mVoiceSearchUrls;
+        /*
+         * ArrayList holding content to load for each item in
+         * mVoiceSearchResults.
+         */
+        public ArrayList<String> mVoiceSearchHtmls;
+        /*
+         * ArrayList holding base urls for the items in mVoiceSearchResults.
+         * If non null, this will either have the same size as
+         * mVoiceSearchResults or have a size of 1, in which case all will use
+         * the same base url
+         */
+        public ArrayList<String> mVoiceSearchBaseUrls;
+        /*
+         * The last url provided by voice search.  Used for comparison to see if
+         * we are going to a page by some method besides voice search.
+         */
+        public String mLastVoiceSearchUrl;
+        /**
+         * The last title used for voice search.  Needed to update the title bar
+         * when switching tabs.
+         */
+        public String mLastVoiceSearchTitle;
+        /**
+         * Whether the Intent which turned on voice search mode contained the
+         * String signifying that Google was the source.
+         */
+        public boolean mSourceIsGoogle;
+        /**
+         * List of headers to be passed into the WebView containing location
+         * information
+         */
+        public ArrayList<Bundle> mHeaders;
+        /**
+         * The Intent used to invoke voice search.  Placed on the
+         * WebHistoryItem so that when coming back to a previous voice search
+         * page we can again activate voice search.
+         */
+        public Intent mVoiceSearchIntent;
+        /**
+         * String used to identify Google as the source of voice search.
+         */
+        public static String SOURCE_IS_GOOGLE
+                = "android.speech.extras.SOURCE_IS_GOOGLE";
+    }
+
     // Container class for the next error dialog that needs to be displayed
     private class ErrorDialog {
         public final int mTitle;
@@ -206,9 +443,24 @@ class Tab {
     // -------------------------------------------------------------------------
 
     private final WebViewClient mWebViewClient = new WebViewClient() {
+        private Message mDontResend;
+        private Message mResend;
         @Override
         public void onPageStarted(WebView view, String url, Bitmap favicon) {
             mInLoad = true;
+            mLoadStartTime = SystemClock.uptimeMillis();
+            if (mVoiceSearchData != null
+                    && !url.equals(mVoiceSearchData.mLastVoiceSearchUrl)) {
+                if (mVoiceSearchData.mSourceIsGoogle) {
+                    Intent i = new Intent(LoggingEvents.ACTION_LOG_EVENT);
+                    i.putExtra(LoggingEvents.EXTRA_FLUSH, true);
+                    mActivity.sendBroadcast(i);
+                }
+                mVoiceSearchData = null;
+                if (mInForeground) {
+                    mActivity.revertVoiceTitleBar();
+                }
+            }
 
             // We've started to load a new page. If there was a pending message
             // to save a screenshot then we will now take the new page and save
@@ -255,6 +507,8 @@ class Tab {
 
         @Override
         public void onPageFinished(WebView view, String url) {
+            LogTag.logPageFinishedLoading(
+                    url, SystemClock.uptimeMillis() - mLoadStartTime);
             mInLoad = false;
 
             if (mInForeground && !mActivity.didUserStopLoading()
@@ -304,37 +558,6 @@ class Tab {
         }
 
         /**
-         * Show the dialog if it is in the foreground, asking the user if they
-         * would like to continue after an excessive number of HTTP redirects.
-         * Cancel if it is in the background.
-         */
-        @Override
-        public void onTooManyRedirects(WebView view, final Message cancelMsg,
-                final Message continueMsg) {
-            if (!mInForeground) {
-                cancelMsg.sendToTarget();
-                return;
-            }
-            new AlertDialog.Builder(mActivity).setTitle(
-                    R.string.browserFrameRedirect).setMessage(
-                    R.string.browserFrame307Post).setPositiveButton(
-                    R.string.ok, new DialogInterface.OnClickListener() {
-                        public void onClick(DialogInterface dialog, int which) {
-                            continueMsg.sendToTarget();
-                        }
-                    }).setNegativeButton(R.string.cancel,
-                    new DialogInterface.OnClickListener() {
-                        public void onClick(DialogInterface dialog, int which) {
-                            cancelMsg.sendToTarget();
-                        }
-                    }).setOnCancelListener(new OnCancelListener() {
-                public void onCancel(DialogInterface dialog) {
-                    cancelMsg.sendToTarget();
-                }
-            }).show();
-        }
-
-        /**
          * Show a dialog informing the user of the network error reported by
          * WebCore if it is in the foreground.
          */
@@ -368,6 +591,14 @@ class Tab {
                 dontResend.sendToTarget();
                 return;
             }
+            if (mDontResend != null) {
+                Log.w(LOGTAG, "onFormResubmission should not be called again "
+                        + "while dialog is still up");
+                dontResend.sendToTarget();
+                return;
+            }
+            mDontResend = dontResend;
+            mResend = resend;
             new AlertDialog.Builder(mActivity).setTitle(
                     R.string.browserFrameFormResubmitLabel).setMessage(
                     R.string.browserFrameFormResubmitMessage)
@@ -375,17 +606,29 @@ class Tab {
                             new DialogInterface.OnClickListener() {
                                 public void onClick(DialogInterface dialog,
                                         int which) {
-                                    resend.sendToTarget();
+                                    if (mResend != null) {
+                                        mResend.sendToTarget();
+                                        mResend = null;
+                                        mDontResend = null;
+                                    }
                                 }
                             }).setNegativeButton(R.string.cancel,
                             new DialogInterface.OnClickListener() {
                                 public void onClick(DialogInterface dialog,
                                         int which) {
-                                    dontResend.sendToTarget();
+                                    if (mDontResend != null) {
+                                        mDontResend.sendToTarget();
+                                        mResend = null;
+                                        mDontResend = null;
+                                    }
                                 }
                             }).setOnCancelListener(new OnCancelListener() {
                         public void onCancel(DialogInterface dialog) {
-                            dontResend.sendToTarget();
+                            if (mDontResend != null) {
+                                mDontResend.sendToTarget();
+                                mResend = null;
+                                mDontResend = null;
+                            }
                         }
                     }).show();
         }
@@ -691,45 +934,60 @@ class Tab {
         }
 
         @Override
-        public void onReceivedTitle(WebView view, String title) {
-            String url = view.getUrl();
+        public void onReceivedTitle(WebView view, final String title) {
+            final String pageUrl = view.getUrl();
             if (mInForeground) {
                 // here, if url is null, we want to reset the title
-                mActivity.setUrlTitle(url, title);
+                mActivity.setUrlTitle(pageUrl, title);
             }
-            if (url == null ||
-                url.length() >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) {
+            if (pageUrl == null || pageUrl.length()
+                    >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) {
                 return;
             }
-            // See if we can find the current url in our history database and
-            // add the new title to it.
-            if (url.startsWith("http://www.")) {
-                url = url.substring(11);
-            } else if (url.startsWith("http://")) {
-                url = url.substring(4);
-            }
-            try {
-                final ContentResolver cr = mActivity.getContentResolver();
-                url = "%" + url;
-                String [] selArgs = new String[] { url };
-                String where = Browser.BookmarkColumns.URL + " LIKE ? AND "
-                        + Browser.BookmarkColumns.BOOKMARK + " = 0";
-                Cursor c = cr.query(Browser.BOOKMARKS_URI,
-                        Browser.HISTORY_PROJECTION, where, selArgs, null);
-                if (c.moveToFirst()) {
-                    // Current implementation of database only has one entry per
-                    // url.
-                    ContentValues map = new ContentValues();
-                    map.put(Browser.BookmarkColumns.TITLE, title);
-                    cr.update(Browser.BOOKMARKS_URI, map, "_id = "
-                            + c.getInt(0), null);
+            new AsyncTask<Void, Void, Void>() {
+                protected Void doInBackground(Void... unused) {
+                    // See if we can find the current url in our history
+                    // database and add the new title to it.
+                    String url = pageUrl;
+                    if (url.startsWith("http://www.")) {
+                        url = url.substring(11);
+                    } else if (url.startsWith("http://")) {
+                        url = url.substring(4);
+                    }
+                    Cursor c = null;
+                    try {
+                        final ContentResolver cr
+                                = mActivity.getContentResolver();
+                        url = "%" + url;
+                        String [] selArgs = new String[] { url };
+                        String where = Browser.BookmarkColumns.URL
+                                + " LIKE ? AND "
+                                + Browser.BookmarkColumns.BOOKMARK + " = 0";
+                        c = cr.query(Browser.BOOKMARKS_URI, new String[]
+                                { Browser.BookmarkColumns._ID }, where, selArgs,
+                                null);
+                        if (c.moveToFirst()) {
+                            // Current implementation of database only has one
+                            // entry per url.
+                            ContentValues map = new ContentValues();
+                            map.put(Browser.BookmarkColumns.TITLE, title);
+                            String[] projection = new String[]
+                                    { Integer.valueOf(c.getInt(0)).toString() };
+                            cr.update(Browser.BOOKMARKS_URI, map, "_id = ?",
+                                    projection);
+                        }
+                    } catch (IllegalStateException e) {
+                        Log.e(LOGTAG, "Tab onReceived title", e);
+                    } catch (SQLiteException ex) {
+                        Log.e(LOGTAG,
+                                "onReceivedTitle() caught SQLiteException: ",
+                                ex);
+                    } finally {
+                        if (c != null) c.close();
+                    }
+                    return null;
                 }
-                c.close();
-            } catch (IllegalStateException e) {
-                Log.e(LOGTAG, "Tab onReceived title", e);
-            } catch (SQLiteException ex) {
-                Log.e(LOGTAG, "onReceivedTitle() caught SQLiteException: ", ex);
-            }
+            }.execute();
         }
 
         @Override
@@ -763,6 +1021,8 @@ class Tab {
                         mTouchIconLoader = new DownloadTouchIcon(Tab.this, cr,
                                 c, view);
                         mTouchIconLoader.execute(url);
+                    } else {
+                        c.close();
                     }
                 } else {
                     c.close();
@@ -846,25 +1106,46 @@ class Tab {
             }
         }
 
-        /* Adds a JavaScript error message to the system log.
-         * @param message The error message to report.
-         * @param lineNumber The line number of the error.
-         * @param sourceID The name of the source file that caused the error.
+        /* Adds a JavaScript error message to the system log and if the JS
+         * console is enabled in the about:debug options, to that console
+         * also.
+         * @param consoleMessage the message object.
          */
         @Override
-        public void addMessageToConsole(String message, int lineNumber,
-                String sourceID) {
+        public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
             if (mInForeground) {
                 // call getErrorConsole(true) so it will create one if needed
                 ErrorConsoleView errorConsole = getErrorConsole(true);
-                errorConsole.addErrorMessage(message, sourceID, lineNumber);
+                errorConsole.addErrorMessage(consoleMessage);
                 if (mActivity.shouldShowErrorConsole()
                         && errorConsole.getShowState() != ErrorConsoleView.SHOW_MAXIMIZED) {
                     errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED);
                 }
             }
-            Log.w(LOGTAG, "Console: " + message + " " + sourceID + ":"
-                    + lineNumber);
+
+            String message = "Console: " + consoleMessage.message() + " "
+                    + consoleMessage.sourceId() +  ":"
+                    + consoleMessage.lineNumber();
+
+            switch (consoleMessage.messageLevel()) {
+                case TIP:
+                    Log.v(CONSOLE_LOGTAG, message);
+                    break;
+                case LOG:
+                    Log.i(CONSOLE_LOGTAG, message);
+                    break;
+                case WARNING:
+                    Log.w(CONSOLE_LOGTAG, message);
+                    break;
+                case ERROR:
+                    Log.e(CONSOLE_LOGTAG, message);
+                    break;
+                case DEBUG:
+                    Log.d(CONSOLE_LOGTAG, message);
+                    break;
+            }
+
+            return true;
         }
 
         /**
@@ -1027,6 +1308,42 @@ class Tab {
             (GeolocationPermissionsPrompt) mContainer.findViewById(
                 R.id.geolocation_permissions_prompt);
 
+        mDownloadListener = new DownloadListener() {
+            public void onDownloadStart(String url, String userAgent,
+                    String contentDisposition, String mimetype,
+                    long contentLength) {
+                mActivity.onDownloadStart(url, userAgent, contentDisposition,
+                        mimetype, contentLength);
+                if (mMainView.copyBackForwardList().getSize() == 0) {
+                    // This Tab was opened for the sole purpose of downloading a
+                    // file. Remove it.
+                    if (mActivity.getTabControl().getCurrentWebView()
+                            == mMainView) {
+                        // In this case, the Tab is still on top.
+                        mActivity.goBackOnePageOrQuit();
+                    } else {
+                        // In this case, it is not.
+                        mActivity.closeTab(Tab.this);
+                    }
+                }
+            }
+        };
+        mWebBackForwardListClient = new WebBackForwardListClient() {
+            @Override
+            public void onNewHistoryItem(WebHistoryItem item) {
+                if (isInVoiceSearchMode()) {
+                    item.setCustomData(mVoiceSearchData.mVoiceSearchIntent);
+                }
+            }
+            @Override
+            public void onIndexChanged(WebHistoryItem item, int index) {
+                Object data = item.getCustomData();
+                if (data != null && data instanceof Intent) {
+                    activateVoiceSearchMode((Intent) data);
+                }
+            }
+        };
+
         setWebView(w);
     }
 
@@ -1049,10 +1366,16 @@ class Tab {
 
         // set the new one
         mMainView = w;
-        // attached the WebViewClient and WebChromeClient
+        // attach the WebViewClient, WebChromeClient and DownloadListener
         if (mMainView != null) {
             mMainView.setWebViewClient(mWebViewClient);
             mMainView.setWebChromeClient(mWebChromeClient);
+            // Attach DownloadManager so that downloads can start in an active
+            // or 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.
+            mMainView.setDownloadListener(mDownloadListener);
+            mMainView.setWebBackForwardListClient(mWebBackForwardListClient);
         }
     }
 
@@ -1100,7 +1423,21 @@ class Tab {
             mSubView.setWebViewClient(new SubWindowClient(mWebViewClient));
             mSubView.setWebChromeClient(new SubWindowChromeClient(
                     mWebChromeClient));
-            mSubView.setDownloadListener(mActivity);
+            // Set a different DownloadListener for the mSubView, since it will
+            // just need to dismiss the mSubView, rather than close the Tab
+            mSubView.setDownloadListener(new DownloadListener() {
+                public void onDownloadStart(String url, String userAgent,
+                        String contentDisposition, String mimetype,
+                        long contentLength) {
+                    mActivity.onDownloadStart(url, userAgent,
+                            contentDisposition, mimetype, contentLength);
+                    if (mSubView.copyBackForwardList().getSize() == 0) {
+                        // This subwindow was opened for the sole purpose of
+                        // downloading a file. Remove it.
+                        dismissSubWindow();
+                    }
+                }
+            });
             mSubView.setOnCreateContextMenuListener(mActivity);
             final BrowserSettings s = BrowserSettings.getInstance();
             s.addObserver(mSubView.getSettings()).update(s, null);
@@ -1511,6 +1848,10 @@ class Tab {
                     mMainView.hashCode() + "_pic.save");
             if (mMainView.savePicture(mSavedState, f)) {
                 mSavedState.putString(CURRPICTURE, f.getPath());
+            } else {
+                // if savePicture returned false, we can't trust the contents,
+                // and it may be large, so we delete it right away
+                f.delete();
             }
         }