Implemented voice search hint heuristics.

Voice search hints are now displayed at intervals as defined by Config. They are shown for a while, then hidden again.

Bug: 2812974
Change-Id: I983250ab1296031fa9b38f17fa105e5b88fd976b
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 94c9140..442b282 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -99,12 +99,6 @@
     <string name="google_show_web_suggestions_summary_enabled">Show suggestions from Google as you type</string>
     <string name="google_show_web_suggestions_summary_disabled">Don\'t show suggestions from Google as you type</string>
 
-    <!-- Title for 'Search Widget' category of search settings -->
-    <string name="search_widget_category_title">Search Widget</string>
-    <!-- Title and summary for 'Show hints' check box setting -->
-    <string name="search_widget_hints_enabled_title">Show hints</string>
-    <string name="search_widget_hints_enabled_summary">Show hints in the search widget</string>
-
     <!-- Title for Voice Search hints bubble -->
     <string name="voice_search_hint_title">Try saying:</string>
     <!-- Starting quotation marks of the voice search hint -->
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 00fe2b0..dbcdc35 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -42,17 +42,4 @@
                 
     </PreferenceCategory>
 
-    <PreferenceCategory
-            android:key="search_widget_settings_category"
-            android:title="@string/search_widget_category_title">
-
-        <CheckBoxPreference
-                android:key="search_widget_hints_enabled"
-                android:title="@string/search_widget_hints_enabled_title"
-                android:summary="@string/search_widget_hints_enabled_summary"
-                android:defaultValue="true"
-                />
-
-    </PreferenceCategory>
-
 </PreferenceScreen>
diff --git a/src/com/android/quicksearchbox/Config.java b/src/com/android/quicksearchbox/Config.java
index 2e0290a..c56c054 100644
--- a/src/com/android/quicksearchbox/Config.java
+++ b/src/com/android/quicksearchbox/Config.java
@@ -16,6 +16,7 @@
 
 package com.android.quicksearchbox;
 
+import android.app.AlarmManager;
 import android.content.Context;
 import android.content.res.Resources;
 import android.os.Process;
@@ -34,7 +35,9 @@
 
     private static final String TAG = "QSB.Config";
 
-    private static final long DAY_MILLIS = 86400000L;
+    protected static final long SECOND_MILLIS = 1000L;
+    protected static final long MINUTE_MILLIS = 60L * SECOND_MILLIS;
+    protected static final long DAY_MILLIS = 86400000L;
 
     private static final int NUM_SUGGESTIONS_ABOVE_KEYBOARD = 4;
     private static final int NUM_PROMOTED_SOURCES = 3;
@@ -58,6 +61,18 @@
     private static final long TYPING_SUGGESTIONS_UPDATE_DELAY_MILLIS = 100;
     private static final long PUBLISH_RESULT_DELAY_MILLIS = 200;
 
+    private static final long VOICE_SEARCH_HINT_ACTIVE_PERIOD = 7L * DAY_MILLIS;
+
+    private static final long VOICE_SEARCH_HINT_UPDATE_INTERVAL
+            = AlarmManager.INTERVAL_FIFTEEN_MINUTES;
+
+    private static final long VOICE_SEARCH_HINT_SHOW_PERIOD_MILLIS
+            = AlarmManager.INTERVAL_HOUR * 2;
+
+    private static final long VOICE_SEARCH_HINT_CHANGE_PERIOD = 2L * MINUTE_MILLIS;
+
+    private static final long VOICE_SEARCH_HINT_VISIBLE_PERIOD = 6L * MINUTE_MILLIS;
+
     private final Context mContext;
     private HashSet<String> mDefaultCorpora;
     private HashSet<String> mHiddenCorpora;
@@ -233,4 +248,44 @@
     public boolean allowVoiceSearchHints() {
         return true;
     }
+
+    /**
+     * The period of time for which after installing voice search we should consider showing voice
+     * search hints.
+     * @return The period in milliseconds.
+     */
+    public long getVoiceSearchHintActivePeriod() {
+        return VOICE_SEARCH_HINT_ACTIVE_PERIOD;
+    }
+
+    /**
+     * The time interval at which we should consider whether or not to show some voice search hints.
+     * @return The period in milliseconds.
+     */
+    public long getVoiceSearchHintUpdatePeriod() {
+        return VOICE_SEARCH_HINT_UPDATE_INTERVAL;
+    }
+
+    /**
+     * The time interval at which, on average, voice search hints are displayed.
+     */
+    public long getVoiceSearchHintShowPeriod() {
+        return VOICE_SEARCH_HINT_SHOW_PERIOD_MILLIS;
+    }
+
+    /**
+     * The amount of time for which voice search hints are displayed in one go.
+     * @return The period in milliseconds.
+     */
+    public long getVoiceSearchHintVisibleTime() {
+        return VOICE_SEARCH_HINT_VISIBLE_PERIOD;
+    }
+
+    /**
+     * The period that we change voice search hints at while they're being displayed.
+     * @return The period in milliseconds.
+     */
+    public long getVoiceSearchHintChangePeriod() {
+        return VOICE_SEARCH_HINT_CHANGE_PERIOD;
+    }
 }
diff --git a/src/com/android/quicksearchbox/SearchSettings.java b/src/com/android/quicksearchbox/SearchSettings.java
index 3adbcef..c3d2252 100644
--- a/src/com/android/quicksearchbox/SearchSettings.java
+++ b/src/com/android/quicksearchbox/SearchSettings.java
@@ -27,7 +27,6 @@
 import android.content.pm.ResolveInfo;
 import android.database.ContentObserver;
 import android.os.Bundle;
-import android.preference.CheckBoxPreference;
 import android.preference.Preference;
 import android.preference.PreferenceActivity;
 import android.preference.PreferenceScreen;
@@ -59,16 +58,13 @@
     private static final String CLEAR_SHORTCUTS_PREF = "clear_shortcuts";
     private static final String SEARCH_ENGINE_SETTINGS_PREF = "search_engine_settings";
     private static final String SEARCH_CORPORA_PREF = "search_corpora";
-    private static final String SEARCH_WIDGET_CATEGORY = "search_widget_settings_category";
 
     // Prefix of per-corpus enable preference
     private static final String CORPUS_ENABLED_PREF_PREFIX = "enable_corpus_";
-    private static final String SEARCH_WIDGET_HINTS_ENABLED_PREF = "search_widget_hints_enabled";
 
     // References to the top-level preference objects
     private Preference mClearShortcutsPreference;
     private PreferenceScreen mSearchEngineSettingsPreference;
-    private CheckBoxPreference mVoiceSearchHintsPreference;
 
     // Dialog ids
     private static final int CLEAR_SHORTCUTS_CONFIRM_DIALOG = 0;
@@ -85,21 +81,11 @@
         mClearShortcutsPreference = preferenceScreen.findPreference(CLEAR_SHORTCUTS_PREF);
         mSearchEngineSettingsPreference = (PreferenceScreen) preferenceScreen.findPreference(
                 SEARCH_ENGINE_SETTINGS_PREF);
-        mVoiceSearchHintsPreference = (CheckBoxPreference)
-                preferenceScreen.findPreference(SEARCH_WIDGET_HINTS_ENABLED_PREF);
         Preference corporaPreference = preferenceScreen.findPreference(SEARCH_CORPORA_PREF);
         corporaPreference.setIntent(getSearchableItemsIntent(this));
 
         mClearShortcutsPreference.setOnPreferenceClickListener(this);
 
-        if (getConfig().allowVoiceSearchHints()) {
-            mVoiceSearchHintsPreference.setOnPreferenceClickListener(this);
-        } else {
-            preferenceScreen.removePreference(
-                    preferenceScreen.findPreference(SEARCH_WIDGET_CATEGORY));
-            mVoiceSearchHintsPreference = null;
-        }
-
         updateClearShortcutsPreference();
         populateSearchEnginePreference();
     }
@@ -119,16 +105,6 @@
         return CORPUS_ENABLED_PREF_PREFIX + corpus.getName();
     }
 
-    public static boolean areVoiceSearchHintsEnabled(Context context) {
-        return getSearchPreferences(context).getBoolean(SEARCH_WIDGET_HINTS_ENABLED_PREF, true);
-    }
-
-    public static void setVoiceSearchHintsEnabled(Context context, boolean enabled) {
-        getSearchPreferences(context)
-                .edit().putBoolean(SEARCH_WIDGET_HINTS_ENABLED_PREF, enabled).commit();
-        SearchWidgetProvider.updateSearchWidgets(context);
-    }
-
     public static SharedPreferences getSearchPreferences(Context context) {
         return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
     }
@@ -182,9 +158,6 @@
         if (preference == mClearShortcutsPreference) {
             showDialog(CLEAR_SHORTCUTS_CONFIRM_DIALOG);
             return true;
-        } else if (preference == mVoiceSearchHintsPreference) {
-            SearchWidgetProvider.updateSearchWidgets(this);
-            return true;
         }
         return false;
     }
diff --git a/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java b/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java
index 935cda3..dc03fb4 100644
--- a/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java
+++ b/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java
@@ -24,6 +24,7 @@
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.View;
 import android.widget.AdapterView;
 import android.widget.ListAdapter;
@@ -32,10 +33,12 @@
  * The configuration screen for search widgets.
  */
 public class SearchWidgetConfigActivity extends ChoiceActivity {
-    static final String TAG = "QSB.SearchWidgetConfigActivity";
+    private static final boolean DBG = false;
+    private static final String TAG = "QSB.SearchWidgetConfigActivity";
 
     private static final String PREFS_NAME = "SearchWidgetConfig";
-    private static final String WIDGET_CORPUS_PREF_PREFIX = "widget_corpus_";
+    private static final String WIDGET_CORPUS_NAME_PREFIX = "widget_corpus_";
+    private static final String WIDGET_CORPUS_SHOWING_HINT_PREFIX = "widget_showing_hint_";
 
     private CorporaAdapter mAdapter;
 
@@ -83,7 +86,7 @@
     }
 
     protected void selectCorpus(Corpus corpus) {
-        writeWidgetCorpusPref(mAppWidgetId, corpus);
+        setWidgetCorpusName(mAppWidgetId, corpus);
         SearchWidgetProvider.updateSearchWidgets(this);
 
         Intent result = new Intent();
@@ -96,20 +99,38 @@
         return context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
     }
 
-    private static String getCorpusPrefKey(int appWidgetId) {
-        return WIDGET_CORPUS_PREF_PREFIX + appWidgetId;
+    private static String getCorpusNameKey(int appWidgetId) {
+        return WIDGET_CORPUS_NAME_PREFIX + appWidgetId;
     }
 
-    private void writeWidgetCorpusPref(int appWidgetId, Corpus corpus) {
+    private static String getShowingHintKey(int appWidgetId) {
+        return WIDGET_CORPUS_SHOWING_HINT_PREFIX + appWidgetId;
+    }
+
+    private void setWidgetCorpusName(int appWidgetId, Corpus corpus) {
         String corpusName = corpus == null ? null : corpus.getName();
         SharedPreferences.Editor prefs = getWidgetPreferences(this).edit();
-        prefs.putString(getCorpusPrefKey(appWidgetId), corpusName);
+        prefs.putString(getCorpusNameKey(appWidgetId), corpusName);
         prefs.commit();
     }
 
-    public static String readWidgetCorpusPref(Context context, int appWidgetId) {
+    public static String getWidgetCorpusName(Context context, int appWidgetId) {
         SharedPreferences prefs = getWidgetPreferences(context);
-        return prefs.getString(getCorpusPrefKey(appWidgetId), null);
+        return prefs.getString(getCorpusNameKey(appWidgetId), null);
+    }
+
+    public static void setWidgetShowingHint(Context context, int appWidgetId, boolean showing) {
+        SharedPreferences.Editor prefs = getWidgetPreferences(context).edit();
+        prefs.putBoolean(getShowingHintKey(appWidgetId), showing);
+        boolean c = prefs.commit();
+        if (DBG) Log.d(TAG, "Widget " + appWidgetId + " set showing hint " + showing + "("+c+")");
+    }
+
+    public static boolean getWidgetShowingHint(Context context, int appWidgetId) {
+        SharedPreferences prefs = getWidgetPreferences(context);
+        boolean r = prefs.getBoolean(getShowingHintKey(appWidgetId), false);
+        if (DBG) Log.d(TAG, "Widget " + appWidgetId + " showing hint: " + r);
+        return r;
     }
 
     private CorpusRanker getCorpusRanker() {
diff --git a/src/com/android/quicksearchbox/SearchWidgetProvider.java b/src/com/android/quicksearchbox/SearchWidgetProvider.java
index 1b59658..1321054 100644
--- a/src/com/android/quicksearchbox/SearchWidgetProvider.java
+++ b/src/com/android/quicksearchbox/SearchWidgetProvider.java
@@ -30,6 +30,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
 import android.graphics.Typeface;
 import android.net.Uri;
 import android.os.Bundle;
@@ -44,6 +45,7 @@
 import android.widget.RemoteViews;
 
 import java.util.ArrayList;
+import java.util.Random;
 
 /**
  * Search widget provider.
@@ -62,60 +64,185 @@
             "com.android.quicksearchbox.action.NEXT_VOICE_SEARCH_HINT";
 
     /**
-     * Broadcast intent action for disabling voice search hints.
+     * Broadcast intent action for hiding voice search hints.
      */
-    private static final String ACTION_CLOSE_VOICE_SEARCH_HINT =
-            "com.android.quicksearchbox.action.CLOSE_VOICE_SEARCH_HINT";
+    private static final String ACTION_HIDE_VOICE_SEARCH_HINT =
+        "com.android.quicksearchbox.action.HIDE_VOICE_SEARCH_HINT";
 
     /**
-     * Voice search hint update interval in milliseconds.
+     * Broadcast intent action for updating voice search hint display. Voice search hints will
+     * only be displayed with some probability.
      */
-    private static final long VOICE_SEARCH_HINT_UPDATE_INTERVAL
-            = AlarmManager.INTERVAL_FIFTEEN_MINUTES;
+    private static final String ACTION_CONSIDER_VOICE_SEARCH_HINT =
+            "com.android.quicksearchbox.action.CONSIDER_VOICE_SEARCH_HINT";
 
     /**
-     * Preference key used for storing the index of the next vocie search hint to show.
+     * Preference key used for storing the index of the next voice search hint to show.
      */
     private static final String NEXT_VOICE_SEARCH_HINT_INDEX_PREF = "next_voice_search_hint";
 
     /**
+     * Preference key used to store the time at which the first voice search hint was displayed.
+     */
+    private static final String FIRST_VOICE_HINT_DISPLAY_TIME = "first_voice_search_hint_time";
+
+    /**
+     * Preference key for the version of voice search we last got hints from.
+     */
+    private static final String LAST_SEEN_VOICE_SEARCH_VERSION = "voice_search_version";
+
+    /**
      * The {@link Search#SOURCE} value used when starting searches from the search widget.
      */
     private static final String WIDGET_SEARCH_SOURCE = "launcher-widget";
 
+    private static Random sRandom;
+
     @Override
     public void onReceive(Context context, Intent intent) {
         if (DBG) Log.d(TAG, "onReceive(" + intent.toUri(0) + ")");
         String action = intent.getAction();
-        if (ACTION_NEXT_VOICE_SEARCH_HINT.equals(action)) {
-            getHintsFromVoiceSearch(context);
-        } else if (ACTION_CLOSE_VOICE_SEARCH_HINT.equals(action)) {
-            SearchSettings.setVoiceSearchHintsEnabled(context, false);
+        if (AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)) {
+            scheduleVoiceHintUpdates(context);
         } else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
             updateSearchWidgets(context);
+        } else if (ACTION_CONSIDER_VOICE_SEARCH_HINT.equals(action)) {
+            considerShowingVoiceSearchHints(context);
+        } else if (ACTION_NEXT_VOICE_SEARCH_HINT.equals(action)) {
+            getHintsFromVoiceSearch(context);
+        } else if (ACTION_HIDE_VOICE_SEARCH_HINT.equals(action)) {
+            hideVoiceSearchHint(context);
         }
     }
 
+    private static Random getRandom() {
+        if (sRandom == null) {
+            sRandom = new Random();
+        }
+        return sRandom;
+    }
+
+    private static boolean haveVoiceSearchHintsExpired(Context context) {
+        SharedPreferences prefs = SearchSettings.getSearchPreferences(context);
+        QsbApplication app = QsbApplication.get(context);
+        int currentVoiceSearchVersion = app.getVoiceSearch().getVersion();
+
+        if (currentVoiceSearchVersion != 0) {
+            long currentTime = System.currentTimeMillis();
+            int lastVoiceSearchVersion = prefs.getInt(LAST_SEEN_VOICE_SEARCH_VERSION, 0);
+            long firstHintTime = prefs.getLong(FIRST_VOICE_HINT_DISPLAY_TIME, 0);
+            if (firstHintTime == 0 || currentVoiceSearchVersion != lastVoiceSearchVersion) {
+                Editor e = prefs.edit();
+                e.putInt(LAST_SEEN_VOICE_SEARCH_VERSION, currentVoiceSearchVersion);
+                e.putLong(FIRST_VOICE_HINT_DISPLAY_TIME, currentTime);
+                e.commit();
+                firstHintTime = currentTime;
+            }
+            if (currentTime - firstHintTime > getConfig(context).getVoiceSearchHintActivePeriod()) {
+                if (DBG) Log.d(TAG, "Voice seach hint period expired; not showing hints.");
+                return true;
+            } else {
+                return false;
+            }
+        } else {
+            if (DBG) Log.d(TAG, "Could not determine voice search version; not showing hints.");
+            return true;
+        }
+    }
+
+    private static boolean shouldShowVoiceSearchHints(Context context) {
+        return (getConfig(context).allowVoiceSearchHints()
+                && !haveVoiceSearchHintsExpired(context));
+    }
+
+    private static SearchWidgetState[] getSearchWidgetStates
+            (Context context, boolean enableVoiceSearchHints) {
+
+        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+        int[] appWidgetIds = appWidgetManager.getAppWidgetIds(myComponentName(context));
+        SearchWidgetState[] states = new SearchWidgetState[appWidgetIds.length];
+        for (int i = 0; i<appWidgetIds.length; ++i) {
+            states[i] = getSearchWidgetState(context, appWidgetIds[i], enableVoiceSearchHints);
+        }
+        return states;
+    }
+
+    private static void considerShowingVoiceSearchHints(Context context) {
+        if (DBG) Log.d(TAG, "considerShowingVoiceSearchHints");
+        if (!shouldShowVoiceSearchHints(context)) return;
+        SearchWidgetState[] states = getSearchWidgetStates(context, true);
+        boolean needHint = false;
+        boolean changed = false;
+        for (SearchWidgetState state : states) {
+            changed |= state.considerShowingHint(context);
+            needHint |= state.isShowingHint();
+        }
+        if (changed) {
+            getHintsFromVoiceSearch(context);
+            sceduleNextVoiceSearchHint(context, true);
+        }
+    }
+
+    private void hideVoiceSearchHint(Context context) {
+        if (DBG) Log.d(TAG, "hideVoiceSearchHint");
+        SearchWidgetState[] states = getSearchWidgetStates(context, true);
+        boolean needHint = false;
+        for (SearchWidgetState state : states) {
+            if (state.isShowingHint()) {
+                state.hideVoiceSearchHint(context);
+                state.updateWidget(context, AppWidgetManager.getInstance(context));
+            }
+            needHint |= state.isShowingHint();
+        }
+        sceduleNextVoiceSearchHint(context, false);
+    }
+
+    private static void voiceSearchHintReceived(Context context, CharSequence hint) {
+        if (DBG) Log.d(TAG, "voiceSearchHintReceived('" + hint + "')");
+        CharSequence formatted = formatVoiceSearchHint(context, hint);
+        SearchWidgetState[] states = getSearchWidgetStates(context, true);
+        boolean needHint = false;
+        for (SearchWidgetState state : states) {
+            if (state.isShowingHint()) {
+                state.setVoiceSearchHint(formatted);
+                state.updateWidget(context, AppWidgetManager.getInstance(context));
+                needHint = true;
+            }
+        }
+        if (!needHint) {
+            sceduleNextVoiceSearchHint(context, false);
+        }
+    }
+
+    private static void scheduleVoiceHintUpdates(Context context) {
+        if (DBG) Log.d(TAG, "scheduleVoiceHintUpdates");
+        if (!shouldShowVoiceSearchHints(context)) return;
+        scheduleVoiceSearchHintUpdates(context, true);
+    }
+
     /**
      * Updates all search widgets.
      */
     public static void updateSearchWidgets(Context context) {
-        updateSearchWidgets(context, true, null);
-    }
+        if (DBG) Log.d(TAG, "updateSearchWidgets");
+        boolean showVoiceSearchHints = shouldShowVoiceSearchHints(context);
+        SearchWidgetState[] states = getSearchWidgetStates(context, showVoiceSearchHints);
 
-    private static void updateSearchWidgets(Context context, boolean updateVoiceSearchHint,
-            CharSequence voiceSearchHint) {
-        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
-        int[] appWidgetIds = appWidgetManager.getAppWidgetIds(myComponentName(context));
-
-        boolean needsVoiceSearchHint = false;
-        for (int appWidgetId : appWidgetIds) {
-            SearchWidgetState state = getSearchWidgetState(context, appWidgetId, voiceSearchHint);
-            state.updateWidget(context, appWidgetManager);
-            needsVoiceSearchHint |= state.shouldShowVoiceSearchHint();
+        boolean needVoiceSearchHint = false;
+        for (SearchWidgetState state : states) {
+            if (state.isShowingHint()) {
+                needVoiceSearchHint = true;
+                // widget update will occur when voice search hint received
+            } else {
+                state.updateWidget(context, AppWidgetManager.getInstance(context));
+            }
         }
-        if (updateVoiceSearchHint) {
-            scheduleVoiceSearchHintUpdates(context, needsVoiceSearchHint);
+        if (DBG) Log.d(TAG, "Need voice search hints=" + needVoiceSearchHint);
+        if (needVoiceSearchHint) {
+            getHintsFromVoiceSearch(context);
+        }
+        if (!showVoiceSearchHints) {
+            scheduleVoiceSearchHintUpdates(context, false);
         }
     }
 
@@ -129,14 +256,11 @@
     }
 
     private static SearchWidgetState getSearchWidgetState(Context context, 
-            int appWidgetId, CharSequence voiceSearchHint) {
+            int appWidgetId, boolean enableVoiceSearchHints) {
         String corpusName =
-                SearchWidgetConfigActivity.readWidgetCorpusPref(context, appWidgetId);
+                SearchWidgetConfigActivity.getWidgetCorpusName(context, appWidgetId);
         Corpus corpus = corpusName == null ? null : getCorpora(context).getCorpus(corpusName);
-        if (DBG) {
-            Log.d(TAG, "Updating appwidget " + appWidgetId + ", corpus=" + corpus
-                    + ",VS hint=" + voiceSearchHint);
-        }
+        if (DBG) Log.d(TAG, "Creating appwidget state " + appWidgetId + ", corpus=" + corpus);
         SearchWidgetState state = new SearchWidgetState(appWidgetId);
 
         Bundle widgetAppData = new Bundle();
@@ -173,12 +297,19 @@
         state.setQueryTextViewIntent(qsbIntent);
 
         // Voice search button
-        Intent voiceSearchIntent = getVoiceSearchIntent(context, corpus, widgetAppData);
-        state.setVoiceSearchIntent(voiceSearchIntent);
-        if (voiceSearchIntent != null
-                && RecognizerIntent.ACTION_WEB_SEARCH.equals(voiceSearchIntent.getAction())) {
-            state.setShouldShowVoiceSearchHint(true);
-            state.setVoiceSearchHint(formatVoiceSearchHint(context, voiceSearchHint));
+        if (enableVoiceSearchHints) {
+            Intent voiceSearchIntent = getVoiceSearchIntent(context, corpus, widgetAppData);
+            state.setVoiceSearchIntent(voiceSearchIntent);
+            if (voiceSearchIntent != null
+                    && RecognizerIntent.ACTION_WEB_SEARCH.equals(voiceSearchIntent.getAction())) {
+                state.setVoiceSearchHintsEnabled(true);
+
+                boolean showingHint =
+                        SearchWidgetConfigActivity.getWidgetShowingHint(context, appWidgetId);
+                if (DBG) Log.d(TAG, "Widget " + appWidgetId + " showing hint: " + showingHint);
+                state.setShowingHint(showingHint);
+
+            }
         }
 
         return state;
@@ -224,33 +355,36 @@
         return spannedHint;
     }
 
-    private static boolean areVoiceSearchHintsEnabled(Context context) {
-        return getConfig(context).allowVoiceSearchHints()
-                && SearchSettings.areVoiceSearchHintsEnabled(context);
+    private static void rescheduleAction(Context context, boolean reshedule, String action, long period) {
+        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+        Intent intent = new Intent(action);
+        intent.setComponent(myComponentName(context));
+        PendingIntent pending = PendingIntent.getBroadcast(context, 0, intent, 0);
+        alarmManager.cancel(pending);
+        if (reshedule) {
+            if (DBG) Log.d(TAG, "Scheduling action " + action + " after period " + period);
+            alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
+                    SystemClock.elapsedRealtime() + period, period, pending);
+        } else {
+            if (DBG) Log.d(TAG, "Cancelled action " + action);
+        }
     }
 
     public static void scheduleVoiceSearchHintUpdates(Context context, boolean enabled) {
-        AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
-        Intent intent = new Intent(ACTION_NEXT_VOICE_SEARCH_HINT);
-        intent.setComponent(myComponentName(context));
-        PendingIntent updateHint = PendingIntent.getBroadcast(context, 0, intent, 0);
-        alarmManager.cancel(updateHint);
-        if (enabled && areVoiceSearchHintsEnabled(context)) {
-            // Do one update immediately, and then at VOICE_SEARCH_HINT_UPDATE_INTERVAL intervals
-            getHintsFromVoiceSearch(context);
-            long period = VOICE_SEARCH_HINT_UPDATE_INTERVAL;
-            alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
-                    SystemClock.elapsedRealtime() + period, period, updateHint);
-        }
+        rescheduleAction(context, enabled, ACTION_CONSIDER_VOICE_SEARCH_HINT,
+                getConfig(context).getVoiceSearchHintUpdatePeriod());
+    }
+
+    private static void sceduleNextVoiceSearchHint(Context context, boolean needUpdates) {
+        rescheduleAction(context, needUpdates, ACTION_NEXT_VOICE_SEARCH_HINT,
+                getConfig(context).getVoiceSearchHintChangePeriod());
     }
 
     /**
      * Requests an asynchronous update of the voice search hints.
      */
     private static void getHintsFromVoiceSearch(Context context) {
-        if (!areVoiceSearchHintsEnabled(context)) return;
         Intent intent = new Intent(RecognizerIntent.ACTION_GET_LANGUAGE_DETAILS);
-        intent.putExtra(Recognition.EXTRA_HINT_CONTEXT, Recognition.HINT_CONTEXT_LAUNCHER);
         if (DBG) Log.d(TAG, "Broadcasting " + intent);
         context.sendOrderedBroadcast(intent, null,
                 new HintReceiver(), null, Activity.RESULT_OK, null, null);
@@ -265,7 +399,7 @@
             ArrayList<CharSequence> hints = getResultExtras(true)
                     .getCharSequenceArrayList(Recognition.EXTRA_HINT_STRINGS);
             CharSequence hint = getNextHint(context, hints);
-            updateSearchWidgets(context, false, hint);
+            voiceSearchHintReceived(context, hint);
         }
     }
 
@@ -315,19 +449,28 @@
         private int mQueryTextViewBackgroundResource;
         private Intent mQueryTextViewIntent;
         private Intent mVoiceSearchIntent;
-        private boolean mShouldShowVoiceSearchHint;
+        private boolean mVoiceSearchHintsEnabled;
         private CharSequence mVoiceSearchHint;
+        private boolean mShowHint;
 
         public SearchWidgetState(int appWidgetId) {
             mAppWidgetId = appWidgetId;
         }
 
-        public boolean shouldShowVoiceSearchHint() {
-            return mShouldShowVoiceSearchHint;
+        public int getId() {
+            return mAppWidgetId;
         }
 
-        public void setShouldShowVoiceSearchHint(boolean shouldShowVoiceSearchHint) {
-            mShouldShowVoiceSearchHint = shouldShowVoiceSearchHint;
+        public void setVoiceSearchHintsEnabled(boolean enabled) {
+            mVoiceSearchHintsEnabled = enabled;
+        }
+
+        public void setShowingHint(boolean show) {
+            mShowHint = show;
+        }
+
+        public boolean isShowingHint() {
+            return mShowHint;
         }
 
         public void setCorpusIconUri(Uri corpusIconUri) {
@@ -358,7 +501,64 @@
             mVoiceSearchHint = voiceSearchHint;
         }
 
-        public void updateWidget(Context context, AppWidgetManager appWidgetManager) {
+        private boolean chooseToShowHint(Context context) {
+            // this is called every getConfig().getVoiceSearchHintUpdatePeriod() milliseconds
+            // we want to return true every getConfig().getVoiceSearchHintShowPeriod() milliseconds
+            // so:
+            Config cfg = getConfig(context);
+            float p = (float) cfg.getVoiceSearchHintUpdatePeriod()
+                    / (float) cfg.getVoiceSearchHintShowPeriod();
+            float f = getRandom().nextFloat();
+            // if p > 1 we won't return true as often as we should (we can't return more times than
+            // we're called!) but we will always return true.
+            boolean r = (f < p);
+            if (DBG) Log.d(TAG, "chooseToShowHint p=" + p +"; f=" + f + "; r=" + r);
+            return r;
+        }
+
+        private Intent createIntent(Context context, String action) {
+            Intent intent = new Intent(action);
+            intent.setComponent(myComponentName(context));
+            return intent;
+        }
+
+        private void sheduleHintHiding(Context context) {
+            Intent hideIntent = createIntent(context, ACTION_HIDE_VOICE_SEARCH_HINT);
+
+            AlarmManager alarmManager =
+                    (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+            PendingIntent hideHint = PendingIntent.getBroadcast(context, 0, hideIntent, 0);
+
+            long period = getConfig(context).getVoiceSearchHintVisibleTime();
+            if (DBG) {
+                Log.d(TAG, "Scheduling action " + ACTION_HIDE_VOICE_SEARCH_HINT +
+                        " after period " + period);
+            }
+            alarmManager.set(AlarmManager.ELAPSED_REALTIME,
+                    SystemClock.elapsedRealtime() + period, hideHint);
+
+        }
+
+        private void updateShowingHint(Context context) {
+            SearchWidgetConfigActivity.setWidgetShowingHint(context, mAppWidgetId, mShowHint);
+        }
+
+        public boolean considerShowingHint(Context context) {
+            if (!mVoiceSearchHintsEnabled || mShowHint) return false;
+            if (!chooseToShowHint(context)) return false;
+            sheduleHintHiding(context);
+            mShowHint = true;
+            updateShowingHint(context);
+            return true;
+        }
+
+        public void hideVoiceSearchHint(Context context) {
+            mShowHint = false;
+            updateShowingHint(context);
+        }
+
+        public void updateWidget(Context context,AppWidgetManager appWidgetMgr) {
+            if (DBG) Log.d(TAG, "Updating appwidget " + mAppWidgetId);
             RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.search_widget);
             // Corpus indicator
             // Before Froyo, android.resource URI could not be used in ImageViews.
@@ -384,25 +584,23 @@
             } else {
                 views.setViewVisibility(R.id.search_widget_voice_btn, View.GONE);
             }
+
             // Voice Search hints
-            if (mShouldShowVoiceSearchHint && !TextUtils.isEmpty(mVoiceSearchHint)) {
+            if (mShowHint && !TextUtils.isEmpty(mVoiceSearchHint)) {
                 views.setTextViewText(R.id.voice_search_hint_text, mVoiceSearchHint);
 
-                Intent nextHintIntent = new Intent(ACTION_NEXT_VOICE_SEARCH_HINT);
-                nextHintIntent.setComponent(myComponentName(context));
-                setOnClickBroadcastIntent(context, views, R.id.voice_search_hint_text,
-                        nextHintIntent);
-
-                Intent closeHintIntent = new Intent(ACTION_CLOSE_VOICE_SEARCH_HINT);
-                closeHintIntent.setComponent(myComponentName(context));
+                Intent closeHintIntent = createIntent(context, ACTION_HIDE_VOICE_SEARCH_HINT);
                 setOnClickBroadcastIntent(context, views, R.id.voice_search_hint_close,
                         closeHintIntent);
 
+                setOnClickActivityIntent(context, views, R.id.voice_search_hint_text,
+                        mVoiceSearchIntent);
+
                 views.setViewVisibility(R.id.voice_search_hint, View.VISIBLE);
             } else {
                 views.setViewVisibility(R.id.voice_search_hint, View.GONE);
             }
-            appWidgetManager.updateAppWidget(mAppWidgetId, views);
+            appWidgetMgr.updateAppWidget(mAppWidgetId, views);
         }
 
         private void setOnClickBroadcastIntent(Context context, RemoteViews views, int viewId,
diff --git a/src/com/android/quicksearchbox/VoiceSearch.java b/src/com/android/quicksearchbox/VoiceSearch.java
index 69d3a12..e826784 100644
--- a/src/com/android/quicksearchbox/VoiceSearch.java
+++ b/src/com/android/quicksearchbox/VoiceSearch.java
@@ -18,16 +18,21 @@
 import android.app.SearchManager;
 import android.content.Context;
 import android.content.Intent;
+import android.content.pm.ComponentInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
 import android.os.Bundle;
 import android.speech.RecognizerIntent;
+import android.util.Log;
 
 /**
  * Voice Search integration.
  */
 public class VoiceSearch {
 
+    private static final String TAG = "QSB.VoiceSearch";
+
     private final Context mContext;
 
     public VoiceSearch(Context context) {
@@ -50,11 +55,15 @@
         return new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
     }
 
-    public boolean isVoiceSearchAvailable() {
+    private ResolveInfo getResolveInfo() {
         Intent intent = createVoiceSearchIntent();
         ResolveInfo ri = mContext.getPackageManager().
                 resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
-        return ri != null;
+        return ri;
+    }
+
+    public boolean isVoiceSearchAvailable() {
+        return getResolveInfo() != null;
     }
 
     public Intent createVoiceWebSearchIntent(Bundle appData) {
@@ -69,4 +78,20 @@
         return intent;
     }
 
+    /**
+     * Gets the {@code versionCode} of the currently installed voice search package.
+     * @return The {@code versionCode} of voiceSearch, or 0 if none is installed.
+     */
+    public int getVersion() {
+        ResolveInfo ri = getResolveInfo();
+        if (ri == null) return 0;
+        ComponentInfo ci = ri.activityInfo != null ? ri.activityInfo : ri.serviceInfo;
+        try {
+            return getContext().getPackageManager().getPackageInfo(ci.packageName, 0).versionCode;
+        } catch (NameNotFoundException e) {
+            Log.e(TAG, "Cannot find voice search package " + ci.packageName, e);
+            return 0;
+        }
+    }
+
 }