Handle the voice search intent.

Once the voice search intent has been handled, the title bar
background changes to green, and touching it displays other voice
search possibilities.

Fixes http://b/issue?id=2390686
diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java
index 90bacad..3f7c9e9 100644
--- a/src/com/android/browser/BrowserActivity.java
+++ b/src/com/android/browser/BrowserActivity.java
@@ -23,6 +23,8 @@
 import android.content.ActivityNotFoundException;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentProviderClient;
 import android.content.ContentResolver;
 import android.content.ContentUris;
 import android.content.ContentValues;
@@ -381,9 +383,11 @@
             // the tab will be close when exit.
             UrlData urlData = getUrlDataFromIntent(intent);
 
+            String action = intent.getAction();
             final Tab t = mTabControl.createNewTab(
-                    Intent.ACTION_VIEW.equals(intent.getAction()) &&
-                    intent.getData() != null,
+                    (Intent.ACTION_VIEW.equals(action) &&
+                    intent.getData() != null)
+                    || Tab.VoiceSearchData.VOICE_SEARCH_RESULTS.equals(action),
                     intent.getStringExtra(Browser.EXTRA_APPLICATION_ID), urlData.mUrl);
             mTabControl.setCurrentTab(t);
             attachTabToContentView(t);
@@ -410,7 +414,7 @@
                     waitForCredentials();
                 }
             } else {
-                urlData.loadIn(webView);
+                urlData.loadIn(t);
             }
         } else {
             // TabControl.restoreState() will create a new tab even if
@@ -425,6 +429,24 @@
         }
     }
 
+    /**
+     * Feed the previously stored results strings to the BrowserProvider so that
+     * the SearchDialog will show them instead of the standard searches.
+     * @param result String to show on the editable line of the SearchDialog.
+     */
+    /* package */ void showVoiceSearchResults(String result) {
+        ContentProviderClient client = mResolver.acquireContentProviderClient(
+                Browser.BOOKMARKS_URI);
+        ContentProvider prov = client.getLocalContentProvider();
+        BrowserProvider bp = (BrowserProvider) prov;
+        bp.setQueryResults(mTabControl.getCurrentTab().getVoiceSearchResults());
+        client.release();
+
+        startSearch(result, false,
+                createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_SEARCHKEY),
+                false);
+    }
+
     @Override
     protected void onNewIntent(Intent intent) {
         Tab current = mTabControl.getCurrentTab();
@@ -448,10 +470,13 @@
             // just resume the browser
             return;
         }
+        boolean activateVoiceSearch = Tab.VoiceSearchData.VOICE_SEARCH_RESULTS
+                .equals(action);
         if (Intent.ACTION_VIEW.equals(action)
                 || Intent.ACTION_SEARCH.equals(action)
                 || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
-                || Intent.ACTION_WEB_SEARCH.equals(action)) {
+                || Intent.ACTION_WEB_SEARCH.equals(action)
+                || activateVoiceSearch) {
             // If this was a search request (e.g. search query directly typed into the address bar),
             // pass it on to the default web search provider.
             if (handleWebSearchIntent(intent)) {
@@ -465,7 +490,7 @@
 
             final String appId = intent
                     .getStringExtra(Browser.EXTRA_APPLICATION_ID);
-            if (Intent.ACTION_VIEW.equals(action)
+            if ((Intent.ACTION_VIEW.equals(action) || activateVoiceSearch)
                     && !getPackageName().equals(appId)
                     && (flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
                 Tab appTab = mTabControl.getTabFromId(appId);
@@ -485,14 +510,14 @@
                     if (current != appTab) {
                         switchToTab(mTabControl.getTabIndex(appTab));
                         if (needsLoad) {
-                            urlData.loadIn(appTab.getWebView());
+                            urlData.loadIn(appTab);
                         }
                     } else {
                         // If the tab was the current tab, we have to attach
                         // it to the view system again.
                         attachTabToContentView(appTab);
                         if (needsLoad) {
-                            urlData.loadIn(appTab.getWebView());
+                            urlData.loadIn(appTab);
                         }
                     }
                     return;
@@ -541,7 +566,7 @@
                 }
                 // Get rid of the subwindow if it exists
                 dismissSubWindow(current);
-                urlData.loadIn(current.getWebView());
+                urlData.loadIn(current);
             }
         }
     }
@@ -571,6 +596,9 @@
 
         String url = null;
         final String action = intent.getAction();
+        if (Tab.VoiceSearchData.VOICE_SEARCH_RESULTS.equals(action)) {
+            return false;
+        }
         if (Intent.ACTION_VIEW.equals(action)) {
             Uri data = intent.getData();
             if (data != null) url = data.toString();
@@ -622,7 +650,7 @@
     }
 
     private UrlData getUrlDataFromIntent(Intent intent) {
-        String url = null;
+        String url = "";
         Map<String, String> headers = null;
         if (intent != null) {
             final String action = intent.getAction();
@@ -673,9 +701,22 @@
                 }
             }
         }
-        return new UrlData(url, headers);
+        return new UrlData(url, headers, intent);
     }
+    /* package */ void showVoiceTitleBar(String title) {
+        mTitleBar.setInVoiceMode(true);
+        mFakeTitleBar.setInVoiceMode(true);
 
+        mTitleBar.setDisplayTitle(title);
+        mFakeTitleBar.setDisplayTitle(title);
+    }
+    /* package */ void revertVoiceTitleBar() {
+        mTitleBar.setInVoiceMode(false);
+        mFakeTitleBar.setInVoiceMode(false);
+
+        mTitleBar.setDisplayTitle(mTitle);
+        mFakeTitleBar.setDisplayTitle(mTitle);
+    }
     /* package */ static String fixUrl(String inUrl) {
         // FIXME: Converting the url to lower case
         // duplicates functionality in smartUrlFilter().
@@ -1742,6 +1783,11 @@
 
         WebView view = t.getWebView();
         view.setEmbeddedTitleBar(mTitleBar);
+        if (t.isInVoiceSearchMode()) {
+            showVoiceTitleBar(t.getVoiceDisplayTitle());
+        } else {
+            revertVoiceTitleBar();
+        }
         // Request focus on the top window.
         t.getTopWindow().requestFocus();
     }
@@ -1803,7 +1849,7 @@
             mTabControl.setCurrentTab(tab);
             attachTabToContentView(tab);
             if (!urlData.isEmpty()) {
-                urlData.loadIn(webview);
+                urlData.loadIn(tab);
             }
             return tab;
         } else {
@@ -1811,10 +1857,10 @@
             dismissSubWindow(currentTab);
             if (!urlData.isEmpty()) {
                 // Load the given url.
-                urlData.loadIn(currentTab.getWebView());
+                urlData.loadIn(currentTab);
             }
+            return currentTab;
         }
-        return currentTab;
     }
 
     private Tab openTab(String url) {
@@ -1991,8 +2037,10 @@
         mUrl = url;
         mTitle = title;
 
-        mTitleBar.setTitleAndUrl(title, url);
-        mFakeTitleBar.setTitleAndUrl(title, url);
+        // If we are in voice search mode, the title has already been set.
+        if (mTabControl.getCurrentTab().isInVoiceSearchMode()) return;
+        mTitleBar.setDisplayTitle(url);
+        mFakeTitleBar.setDisplayTitle(url);
     }
 
     /**
@@ -3891,23 +3939,35 @@
     private static class UrlData {
         final String mUrl;
         final Map<String, String> mHeaders;
+        final Intent mVoiceIntent;
 
         UrlData(String url) {
             this.mUrl = url;
             this.mHeaders = null;
+            this.mVoiceIntent = null;
         }
 
-        UrlData(String url, Map<String, String> headers) {
+        UrlData(String url, Map<String, String> headers, Intent intent) {
             this.mUrl = url;
             this.mHeaders = headers;
+            if (Tab.VoiceSearchData.VOICE_SEARCH_RESULTS.equals(
+                    intent.getAction())) {
+                this.mVoiceIntent = intent;
+            } else {
+                this.mVoiceIntent = null;
+            }
         }
 
         boolean isEmpty() {
-            return mUrl == null || mUrl.length() == 0;
+            return mVoiceIntent == null && (mUrl == null || mUrl.length() == 0);
         }
 
-        public void loadIn(WebView webView) {
-            webView.loadUrl(mUrl, mHeaders);
+        public void loadIn(Tab t) {
+            if (mVoiceIntent != null) {
+                t.activateVoiceSearchMode(mVoiceIntent);
+            } else {
+                t.getWebView().loadUrl(mUrl, mHeaders);
+            }
         }
     };
 
diff --git a/src/com/android/browser/BrowserProvider.java b/src/com/android/browser/BrowserProvider.java
index 4747721..bd6a723 100644
--- a/src/com/android/browser/BrowserProvider.java
+++ b/src/com/android/browser/BrowserProvider.java
@@ -53,6 +53,7 @@
 
 import java.io.File;
 import java.io.FilenameFilter;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -718,16 +719,105 @@
         }
     }
 
+    private static class ResultsCursor extends AbstractCursor {
+        // Array indices for RESULTS_COLUMNS
+        private static final int RESULT_ACTION_ID = 1;
+        private static final int RESULT_DATA_ID = 2;
+        private static final int RESULT_TEXT_ID = 3;
+        private static final int RESULT_ICON_ID = 4;
+        private static final int RESULT_EXTRA_ID = 5;
+
+        private static final String[] RESULTS_COLUMNS = new String[] {
+                "_id",
+                SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+                SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+                SearchManager.SUGGEST_COLUMN_TEXT_1,
+                SearchManager.SUGGEST_COLUMN_ICON_1,
+                SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA
+        };
+        private final ArrayList<String> mResults;
+        public ResultsCursor(ArrayList<String> results) {
+            mResults = results;
+        }
+        public int getCount() { return mResults.size(); }
+
+        public String[] getColumnNames() {
+            return RESULTS_COLUMNS;
+        }
+
+        public String getString(int column) {
+            switch (column) {
+                case RESULT_ACTION_ID:
+                    return Tab.VoiceSearchData.VOICE_SEARCH_RESULTS;
+                case RESULT_TEXT_ID:
+                // The data is used when the phone is in landscape mode.  We
+                // still want to show the result string.
+                case RESULT_DATA_ID:
+                    return mResults.get(mPos);
+                case RESULT_EXTRA_ID:
+                    // The Intent's extra data will store the index into
+                    // mResults so the BrowserActivity will know which result to
+                    // use.
+                    return Integer.toString(mPos);
+                case RESULT_ICON_ID:
+                    return Integer.valueOf(R.drawable.magnifying_glass)
+                            .toString();
+                default:
+                    return null;
+            }
+        }
+        public short getShort(int column) {
+            throw new UnsupportedOperationException();
+        }
+        public int getInt(int column) {
+            throw new UnsupportedOperationException();
+        }
+        public long getLong(int column) {
+            if ((mPos != -1) && column == 0) {
+                return mPos;        // use row# as the _id
+            }
+            throw new UnsupportedOperationException();
+        }
+        public float getFloat(int column) {
+            throw new UnsupportedOperationException();
+        }
+        public double getDouble(int column) {
+            throw new UnsupportedOperationException();
+        }
+        public boolean isNull(int column) {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    private ResultsCursor mResultsCursor;
+
+    /**
+     * Provide a set of results to be returned to query, intended to be used
+     * by the SearchDialog when the BrowserActivity is in voice search mode.
+     * @param results Strings to display in the dropdown from the SearchDialog
+     */
+    /* package */ void setQueryResults(ArrayList<String> results) {
+        if (results == null) {
+            mResultsCursor = null;
+        } else {
+            mResultsCursor = new ResultsCursor(results);
+        }
+    }
+
     @Override
     public Cursor query(Uri url, String[] projectionIn, String selection,
             String[] selectionArgs, String sortOrder)
             throws IllegalStateException {
-        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
-
         int match = URI_MATCHER.match(url);
         if (match == -1) {
             throw new IllegalArgumentException("Unknown URL");
         }
+        if (match == URI_MATCH_SUGGEST && mResultsCursor != null) {
+            Cursor results = mResultsCursor;
+            mResultsCursor = null;
+            return results;
+        }
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
 
         if (match == URI_MATCH_SUGGEST || match == URI_MATCH_BOOKMARKS_SUGGEST) {
             String suggestSelection;
diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java
index 22c8286..0a54eee 100644
--- a/src/com/android/browser/Tab.java
+++ b/src/com/android/browser/Tab.java
@@ -17,14 +17,17 @@
 package com.android.browser;
 
 import java.io.File;
+import java.util.ArrayList;
 import java.util.LinkedList;
 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;
@@ -133,6 +136,117 @@
 
     // -------------------------------------------------------------------------
 
+    /**
+     * 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;
+    }
+    /**
+     * 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) {
+        ArrayList<String> results = intent.getStringArrayListExtra(
+                    "result_strings");
+        ArrayList<String> urls = intent.getStringArrayListExtra(
+                    "result_urls");
+        if (results != null) {
+            // This tab is now entering voice search mode for the first time, or
+            // a new voice search was done.
+            if (urls == null || results.size() != urls.size()) {
+                throw new AssertionError("improper extras passed in Intent");
+            }
+            mVoiceSearchData = new VoiceSearchData(results, urls);
+        } else {
+            String extraData = intent.getStringExtra(
+                    SearchManager.EXTRA_DATA_KEY);
+            if (extraData != null) {
+                mVoiceSearchData.mLastVoiceSearchIndex
+                        = Integer.parseInt(extraData);
+                if (mVoiceSearchData.mLastVoiceSearchIndex
+                        >= mVoiceSearchData.mVoiceSearchResults.size()) {
+                    throw new AssertionError("index must be less than "
+                            + " size of mVoiceSearchResults");
+                }
+            }
+        }
+        mVoiceSearchData.mLastVoiceSearchTitle
+                = mVoiceSearchData.mVoiceSearchResults.get(mVoiceSearchData.
+                mLastVoiceSearchIndex);
+        if (mInForeground) {
+            mActivity.showVoiceTitleBar(mVoiceSearchData.mLastVoiceSearchTitle);
+        }
+        mVoiceSearchData.mLastVoiceSearchUrl
+                = mVoiceSearchData.mVoiceSearchUrls.get(mVoiceSearchData.
+                mLastVoiceSearchIndex);
+        mMainView.loadUrl(mVoiceSearchData.mLastVoiceSearchUrl);
+    }
+    /* package */ static class VoiceSearchData {
+        /**
+         * Intent action for a voice search.  Will be replaced with a global
+         * variable.
+         */
+        public static final String VOICE_SEARCH_RESULTS
+                = "android.speech.action.VOICE_SEARCH_RESULTS";
+
+        public VoiceSearchData(ArrayList<String> results,
+                ArrayList<String> urls) {
+            mVoiceSearchResults = results;
+            mVoiceSearchUrls = urls;
+            mLastVoiceSearchIndex = 0;
+        }
+        /*
+         * 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;
+        /*
+         * 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.  Only
+         * meaningful in voice search mode.
+         */
+        public String mLastVoiceSearchUrl;
+        /**
+         * The last title used for voice search.  Needed to update the title bar
+         * when switching tabs.
+         */
+        public String mLastVoiceSearchTitle;
+        /*
+         * The index into mVoiceSearchResults and mVoiceSearchUrls of the last
+         * voice search performed. Stored so it can be used to index into
+         * mVoiceSearchUrls to determine the url in getUrlDataFromIntent.
+         */
+        public int mLastVoiceSearchIndex;
+    }
+
     // Container class for the next error dialog that needs to be displayed
     private class ErrorDialog {
         public final int mTitle;
@@ -209,6 +323,13 @@
         @Override
         public void onPageStarted(WebView view, String url, Bitmap favicon) {
             mInLoad = true;
+            if (mVoiceSearchData != null
+                    && !url.equals(mVoiceSearchData.mLastVoiceSearchUrl)) {
+                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
diff --git a/src/com/android/browser/TitleBar.java b/src/com/android/browser/TitleBar.java
index a9da7c0..743af9b 100644
--- a/src/com/android/browser/TitleBar.java
+++ b/src/com/android/browser/TitleBar.java
@@ -67,6 +67,8 @@
     private MyHandler       mHandler;
     private Intent          mVoiceSearchIntent;
     private boolean         mInVoiceMode;
+    private Drawable        mVoiceModeBackground;
+    private Drawable        mNormalBackground;
 
     private static int LONG_PRESS = 1;
 
@@ -110,6 +112,9 @@
         }
         mStopDrawable = resources.getDrawable(R.drawable.ic_btn_stop_v2);
         mBookmarkDrawable = mRtButton.getDrawable();
+        mVoiceModeBackground = resources.getDrawable(
+                R.drawable.textfield_voice_search);
+        mNormalBackground = mTitleBg.getBackground();
     }
 
     private class MyHandler extends Handler {
@@ -186,7 +191,12 @@
                     mRtButton.setPressed(false);
                 } else if (mTitleBg.isPressed()) {
                     mHandler.removeMessages(LONG_PRESS);
-                    mBrowserActivity.onSearchRequested();
+                    if (mInVoiceMode) {
+                        mBrowserActivity.showVoiceSearchResults(
+                                mTitle.getText().toString());
+                    } else {
+                        mBrowserActivity.onSearchRequested();
+                    }
                     mTitleBg.setPressed(false);
                 }
                 break;
@@ -222,13 +232,20 @@
     /* package */ void setInVoiceMode(boolean inVoiceMode) {
         if (mInVoiceMode == inVoiceMode) return;
         mInVoiceMode = inVoiceMode && mVoiceSearchIntent != null;
+        Drawable rightButtonDrawable, titleDrawable;
         if (mInVoiceMode) {
-            mRtButton.setImageDrawable(mVoiceDrawable);
-        } else if (mInLoad) {
-            mRtButton.setImageDrawable(mStopDrawable);
+            rightButtonDrawable = mVoiceDrawable;
+            titleDrawable = mVoiceModeBackground;
         } else {
-            mRtButton.setImageDrawable(mBookmarkDrawable);
+            titleDrawable = mNormalBackground;
+            if (mInLoad) {
+                rightButtonDrawable = mStopDrawable;
+            } else {
+                rightButtonDrawable = mBookmarkDrawable;
+            }
         }
+        mTitleBg.setBackgroundDrawable(titleDrawable);
+        mRtButton.setImageDrawable(rightButtonDrawable);
     }
 
     /**
@@ -275,13 +292,15 @@
     }
 
     /**
-     * Update the title and url.
+     * Update the text displayed in the title bar.
+     * @param title String to display.  If null, the loading string will be
+     *      shown.
      */
-    /* package */ void setTitleAndUrl(CharSequence title, CharSequence url) {
-        if (url == null) {
+    /* package */ void setDisplayTitle(String title) {
+        if (title == null) {
             mTitle.setText(R.string.title_bar_loading);
         } else {
-            mTitle.setText(url.toString());
+            mTitle.setText(title);
         }
     }