Merge remote branch 'goog/froyo' into merge_froyo_to_market-0
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 21a8fc4..18edda0 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -62,8 +62,13 @@
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:scheme="qsb.corpus" />
             </intent-filter>
+            <meta-data android:name="android.app.search.shortcut.provider" android:value="content://com.android.quicksearchbox.shortcuts/shortcuts" />
         </activity>
 
+        <provider android:name=".ShortcutsProvider"
+                android:authorities="com.android.quicksearchbox.shortcuts">
+        </provider>
+
         <activity android:name=".SearchSettings"
                 android:label="@string/search_settings"
                 android:excludeFromRecents="true">
@@ -76,6 +81,24 @@
             </intent-filter>
         </activity>
 
+        <activity android:name=".SearchableItemsSettings"
+                android:label="@string/search_sources"
+                android:excludeFromRecents="true">
+            <intent-filter>
+                <action android:name="com.android.quicksearchbox.action.SEARCHABLE_ITEMS" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+
+        <receiver android:name=".CorporaUpdateReceiver">
+            <intent-filter>
+                <action android:name="android.search.action.SEARCHABLES_CHANGED" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.search.action.SETTINGS_CHANGED" />
+            </intent-filter>
+        </receiver>
+
         <receiver android:name=".SearchWidgetProvider"
                   android:label="@string/app_name">
             <intent-filter>
diff --git a/res/drawable-hdpi/refine_query_default.png b/res/drawable-hdpi/refine_query_default.png
new file mode 100644
index 0000000..10ae447
--- /dev/null
+++ b/res/drawable-hdpi/refine_query_default.png
Binary files differ
diff --git a/res/drawable-hdpi/refine_query_focused.png b/res/drawable-hdpi/refine_query_focused.png
new file mode 100644
index 0000000..5337bfa
--- /dev/null
+++ b/res/drawable-hdpi/refine_query_focused.png
Binary files differ
diff --git a/res/drawable-hdpi/refine_query_pressed.png b/res/drawable-hdpi/refine_query_pressed.png
new file mode 100644
index 0000000..883becb
--- /dev/null
+++ b/res/drawable-hdpi/refine_query_pressed.png
Binary files differ
diff --git a/res/drawable-hdpi/voice_search_hint_bg.9.png b/res/drawable-hdpi/voice_search_hint_bg.9.png
new file mode 100755
index 0000000..438c172
--- /dev/null
+++ b/res/drawable-hdpi/voice_search_hint_bg.9.png
Binary files differ
diff --git a/res/drawable-mdpi/refine_query_default.png b/res/drawable-mdpi/refine_query_default.png
new file mode 100644
index 0000000..ab98e2c
--- /dev/null
+++ b/res/drawable-mdpi/refine_query_default.png
Binary files differ
diff --git a/res/drawable-mdpi/refine_query_focused.png b/res/drawable-mdpi/refine_query_focused.png
new file mode 100644
index 0000000..4a4bfc6
--- /dev/null
+++ b/res/drawable-mdpi/refine_query_focused.png
Binary files differ
diff --git a/res/drawable-mdpi/refine_query_pressed.png b/res/drawable-mdpi/refine_query_pressed.png
new file mode 100644
index 0000000..bd641da
--- /dev/null
+++ b/res/drawable-mdpi/refine_query_pressed.png
Binary files differ
diff --git a/res/drawable-mdpi/voice_search_hint_bg.9.png b/res/drawable-mdpi/voice_search_hint_bg.9.png
new file mode 100644
index 0000000..eb09cbd
--- /dev/null
+++ b/res/drawable-mdpi/voice_search_hint_bg.9.png
Binary files differ
diff --git a/res/drawable/refine_query.xml b/res/drawable/refine_query.xml
new file mode 100644
index 0000000..e80edca
--- /dev/null
+++ b/res/drawable/refine_query.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <item android:state_window_focused="false" android:state_enabled="true"
+        android:drawable="@drawable/refine_query_default" />
+
+    <item android:state_pressed="true"
+        android:drawable="@drawable/refine_query_pressed" />
+
+    <item android:state_enabled="true" android:state_focused="true"
+        android:drawable="@drawable/refine_query_focused" />
+
+    <item android:drawable="@drawable/refine_query_default" />
+
+</selector>
+
diff --git a/res/layout/choice_activity.xml b/res/layout/choice_activity.xml
index b0414b3..f34673e 100644
--- a/res/layout/choice_activity.xml
+++ b/res/layout/choice_activity.xml
@@ -50,7 +50,8 @@
                 android:src="@drawable/ic_dialog_menu_generic" />
             <TextView android:id="@+id/alertTitle"
                 style="?android:attr/textAppearanceLarge"
-                android:singleLine="true"
+                android:singleLine="false"
+                android:maxLines="3"
                 android:ellipsize="end"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content" />
diff --git a/res/layout/corpus_selection_dialog.xml b/res/layout/corpus_selection_dialog.xml
index 8a5a2f2..c826f86 100644
--- a/res/layout/corpus_selection_dialog.xml
+++ b/res/layout/corpus_selection_dialog.xml
@@ -52,9 +52,22 @@
             android:layout_gravity="top|left"
             android:horizontalSpacing="2dip"
             android:verticalSpacing="2dip"
+            android:numColumns="@integer/corpus_selection_dialog_columns"
             android:listSelector="@drawable/corpus_grid_item_bg"
             />
 
+        <TextView
+            android:id="@+id/corpus_edit_items"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="4dip"
+            android:layout_gravity="bottom|right"
+            android:singleLine="true"
+            android:textColor="@android:color/tertiary_text_light"
+            android:textSize="16dip"
+            android:text="@string/corpus_selection_edit_items"
+        />
+
     </LinearLayout>
 
     <ImageView
diff --git a/res/layout/search_widget.xml b/res/layout/search_widget.xml
index 99a8a02..bc0a8db 100644
--- a/res/layout/search_widget.xml
+++ b/res/layout/search_widget.xml
@@ -14,48 +14,66 @@
      limitations under the License.
 -->
 
-<LinearLayout
+<RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/search_plate"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:paddingRight="14dip"
-    android:orientation="horizontal"
-    android:background="@drawable/search_floater" >
+    android:layout_height="match_parent"
+    >
 
-    <include layout="@layout/corpus_indicator" />
-
-    <TextView
-        android:id="@+id/search_widget_text"
-        android:layout_width="0dip"
+    <LinearLayout
+        android:id="@+id/search_plate"
+        android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:layout_weight="1.0"
-        android:layout_marginTop="6dip"
-        android:layout_marginBottom="6dip"
-        android:paddingLeft="10dip"
-        android:paddingRight="10dip"
-        android:paddingTop="5dip"
-        android:paddingBottom="5dip"
-        android:gravity="center_vertical|left"
-        android:singleLine="true"
-        android:ellipsize="end"
-        android:editable="false"
-        android:focusable="true"
-        android:inputType="none"
-        android:background="@drawable/textfield_search_empty_google"
-        android:textSize="18sp"
-        android:textStyle="normal"
-        android:textColor="@android:color/primary_text_light"
-        android:textColorHint="@color/search_hint"
-    />
+        android:layout_alignParentTop="true"
+        android:layout_alignParentLeft="true"
+        android:paddingRight="14dip"
+        android:orientation="horizontal"
+        android:background="@drawable/search_floater" >
 
-    <ImageButton
-        android:id="@+id/search_widget_voice_btn"
+        <include layout="@layout/corpus_indicator" />
+
+        <TextView
+            android:id="@+id/search_widget_text"
+            android:layout_width="0dip"
+            android:layout_height="wrap_content"
+            android:layout_weight="1.0"
+            android:layout_marginTop="6dip"
+            android:layout_marginBottom="6dip"
+            android:paddingLeft="10dip"
+            android:paddingRight="10dip"
+            android:paddingTop="5dip"
+            android:paddingBottom="5dip"
+            android:gravity="center_vertical|left"
+            android:singleLine="true"
+            android:ellipsize="end"
+            android:editable="false"
+            android:focusable="true"
+            android:inputType="none"
+            android:background="@drawable/textfield_search_empty_google"
+            android:textSize="18sp"
+            android:textStyle="normal"
+            android:textColor="@android:color/primary_text_light"
+            android:textColorHint="@color/search_hint"
+        />
+
+        <ImageButton
+            android:id="@+id/search_widget_voice_btn"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
+            android:background="@drawable/btn_search_dialog_voice"
+            android:src="@drawable/ic_btn_speak_now"
+            android:layout_marginRight="-4dip"
+        />
+
+    </LinearLayout>
+
+    <include
+        android:id="@+id/voice_search_hint"
+        layout="@layout/voice_search_hint"
         android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:background="@drawable/btn_search_dialog_voice"
-        android:src="@drawable/ic_btn_speak_now"
-        android:layout_marginRight="-4dip"
+        android:layout_height="wrap_content"
+        android:layout_alignParentBottom="true"
+        android:layout_alignParentRight="true"
     />
 
-</LinearLayout>
+</RelativeLayout>
diff --git a/res/layout/suggestion.xml b/res/layout/suggestion.xml
index 1a99fe3..c1864c7 100644
--- a/res/layout/suggestion.xml
+++ b/res/layout/suggestion.xml
@@ -68,6 +68,7 @@
         android:textColor="@android:color/primary_text_light"
         android:textSize="16sp"
         android:singleLine="true"
+        android:ellipsize="start"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:layout_centerVertical="true"
diff --git a/res/layout/voice_search_hint.xml b/res/layout/voice_search_hint.xml
new file mode 100644
index 0000000..d43e999
--- /dev/null
+++ b/res/layout/voice_search_hint.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:background="@drawable/voice_search_hint_bg"
+        android:orientation="horizontal"
+        android:gravity="center_vertical|left"
+        android:visibility="gone"
+        >
+
+    <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.0"
+            android:textColor="#88FFFFFF"
+            android:textSize="14dip"
+            android:gravity="left"
+            android:singleLine="true"
+            android:paddingRight="4dip"
+            android:text="@string/voice_search_hint_title"
+            />
+
+    <TextView
+            android:id="@+id/voice_search_hint_text"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_weight="1.0"
+            android:textColor="@android:color/white"
+            android:textSize="14dip"
+            android:gravity="left"
+            android:singleLine="true"
+            />
+
+    <ImageView
+            android:id="@+id/voice_search_hint_close"
+            android:layout_width="24dip"
+            android:layout_height="24dip"
+            android:layout_weight="0.0"
+            android:paddingLeft="4dip"
+            android:src="@android:drawable/btn_dialog"
+            android:scaleType="centerInside"
+            />
+
+</LinearLayout>
diff --git a/res/values-land/styles.xml b/res/values-land/styles.xml
new file mode 100644
index 0000000..502f746
--- /dev/null
+++ b/res/values-land/styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+
+<resources>
+    <!-- The number of columns in the business listing launcher GridView -->
+    <integer name="corpus_selection_dialog_columns">6</integer>
+
+</resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9aada1c..c90ee5d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -25,6 +25,7 @@
 
     <!-- Source selector -->
     <string name="corpus_selection_heading">Search</string>
+    <string name="corpus_selection_edit_items">Add or remove searchable items...</string>
     <string name="corpus_label_global">All</string>
 
     <!-- Name of the combined Web source shown in QSB -->
@@ -95,6 +96,19 @@
     <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 'Voice Search' category of search settings -->
+    <string name="voice_search_category_title">Voice Search</string>
+    <!-- Title and summary for 'show voice search hints' check box setting -->
+    <string name="voice_search_hints_enabled_title">Show Voice Search hints</string>
+    <string name="voice_search_hints_enabled_summary">Shows hints in the search widget for how to use Voice Search</string>
+
+    <!-- Title for Voice Search hints bubble -->
+    <string name="voice_search_hint_title">Try saying:</string>
+    <!-- Starting quotation marks of the voice search hint -->
+    <string name="voice_search_hint_quotation_start">“</string>
+    <!-- Ending quotation marks of the voice search hint -->
+    <string name="voice_search_hint_quotation_end">”</string>
+
     <!-- Note that this is the standard search url.  It uses the current locale for language -->
     <!-- (%1$s) and country (%2$s) and shouldn't need to be replaced by locale or mcc selected -->
     <!-- resources. -->
diff --git a/res/values/styles.xml b/res/values/styles.xml
index 4cec2ac..1dd2adf 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -41,4 +41,7 @@
         <item name="android:windowExitAnimation">@anim/corpus_selector_close</item>
     </style>
 
+    <!-- The number of columns in the business listing launcher GridView -->
+    <integer name="corpus_selection_dialog_columns">4</integer>
+
 </resources>
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 8f67eae..901f069 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -27,15 +27,11 @@
     
     <PreferenceCategory
             android:title="@string/system_search_category_title">
-            
+
         <PreferenceScreen
-                android:title="@string/search_sources"
                 android:key="search_corpora"
-                android:summary="@string/search_sources_summary">
-
-            <!-- CheckBoxPreferences added here dynamically -->
-
-        </PreferenceScreen>
+                android:title="@string/search_sources"
+                android:summary="@string/search_sources_summary" />
 
         <!-- TODO: Use DialogPreference instead. -->
         <Preference
@@ -46,4 +42,16 @@
                 
     </PreferenceCategory>
 
+    <PreferenceCategory
+            android:title="@string/voice_search_category_title">
+
+        <CheckBoxPreference
+                android:key="voice_search_hints_enabled"
+                android:title="@string/voice_search_hints_enabled_title"
+                android:summary="@string/voice_search_hints_enabled_summary"
+                android:defaultValue="true"
+                />
+
+    </PreferenceCategory>
+
 </PreferenceScreen>
diff --git a/res/xml/preferences_searchable_items.xml b/res/xml/preferences_searchable_items.xml
new file mode 100644
index 0000000..4a20243
--- /dev/null
+++ b/res/xml/preferences_searchable_items.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2010 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.
+-->
+
+<PreferenceScreen
+        xmlns:android="http://schemas.android.com/apk/res/android"
+        android:title="@string/search_sources"
+        android:key="search_corpora"
+        android:summary="@string/search_sources_summary">
+
+    <!-- CheckBoxPreferences added here dynamically -->
+
+</PreferenceScreen>
diff --git a/src/com/android/quicksearchbox/AbstractSuggestionCursor.java b/src/com/android/quicksearchbox/AbstractSuggestionCursor.java
index c00a3c8..7e66aef 100644
--- a/src/com/android/quicksearchbox/AbstractSuggestionCursor.java
+++ b/src/com/android/quicksearchbox/AbstractSuggestionCursor.java
@@ -16,6 +16,11 @@
 
 package com.android.quicksearchbox;
 
+import android.app.SearchManager;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+
 
 /**
  * Base class for suggestion cursors.
@@ -32,6 +37,46 @@
         return mUserQuery;
     }
 
+    public Intent getSuggestionIntent(Bundle appSearchData) {
+        Source source = getSuggestionSource();
+        String action = getSuggestionIntentAction();
+        // use specific action if supplied, or default action if supplied, or fixed default
+        if (action == null) {
+            action = source.getDefaultIntentAction();
+            if (action == null) {
+                action = Intent.ACTION_SEARCH;
+            }
+        }
+
+        String data = getSuggestionIntentDataString();
+        String query = getSuggestionQuery();
+        String userQuery = getUserQuery();
+        String extraData = getSuggestionIntentExtraData();
+
+        // Now build the Intent
+        Intent intent = new Intent(action);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        // We need CLEAR_TOP to avoid reusing an old task that has other activities
+        // on top of the one we want.
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        if (data != null) {
+            intent.setData(Uri.parse(data));
+        }
+        intent.putExtra(SearchManager.USER_QUERY, userQuery);
+        if (query != null) {
+            intent.putExtra(SearchManager.QUERY, query);
+        }
+        if (extraData != null) {
+            intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+        }
+        if (appSearchData != null) {
+            intent.putExtra(SearchManager.APP_DATA, appSearchData);
+        }
+
+        intent.setComponent(source.getIntentComponent());
+        return intent;
+    }
+
     public String getSuggestionDisplayQuery() {
         String query = getSuggestionQuery();
         if (query != null) {
diff --git a/src/com/android/quicksearchbox/Corpora.java b/src/com/android/quicksearchbox/Corpora.java
index d92603f..c5eae29 100644
--- a/src/com/android/quicksearchbox/Corpora.java
+++ b/src/com/android/quicksearchbox/Corpora.java
@@ -74,9 +74,9 @@
     Corpus getCorpusForSource(Source source);
 
     /**
-     * Frees any resources used by the corpus set.
+     * Updates the corpora.
      */
-    void close();
+    void update();
 
     /**
      * Registers an observer that is called when corpus set changes.
diff --git a/src/com/android/quicksearchbox/CorporaUpdateReceiver.java b/src/com/android/quicksearchbox/CorporaUpdateReceiver.java
new file mode 100644
index 0000000..11163c7
--- /dev/null
+++ b/src/com/android/quicksearchbox/CorporaUpdateReceiver.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2010 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.quicksearchbox;
+
+import android.app.SearchManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+/**
+ * Listens for broadcasts that require updates to the corpus set.
+ */
+public class CorporaUpdateReceiver extends BroadcastReceiver {
+
+    private static final boolean DBG = false;
+    private static final String TAG = "QSB.CorporaUpdateReceiver";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        String action = intent.getAction();
+        if (SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action)
+                || SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED.equals(action)) {
+            if (DBG) Log.d(TAG, "onReceive(" + intent + ")");
+            updateCorpora(context);
+            SearchWidgetProvider.updateSearchWidgets(context);
+        }
+    }
+
+    private void updateCorpora(Context context) {
+        getQsbApplication(context).updateCorpora();
+    }
+
+    private QsbApplication getQsbApplication(Context context) {
+        return (QsbApplication) context.getApplicationContext();
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/CorpusSelectionDialog.java b/src/com/android/quicksearchbox/CorpusSelectionDialog.java
index e15837c..eece6a3 100644
--- a/src/com/android/quicksearchbox/CorpusSelectionDialog.java
+++ b/src/com/android/quicksearchbox/CorpusSelectionDialog.java
@@ -21,6 +21,7 @@
 
 import android.app.Dialog;
 import android.content.Context;
+import android.content.Intent;
 import android.os.Bundle;
 import android.util.Log;
 import android.view.KeyEvent;
@@ -31,6 +32,7 @@
 import android.view.WindowManager;
 import android.widget.AdapterView;
 import android.widget.GridView;
+import android.widget.TextView;
 
 /**
  * Corpus selection dialog.
@@ -40,10 +42,10 @@
     private static final boolean DBG = false;
     private static final String TAG = "QSB.SelectSearchSourceDialog";
 
-    private static final int NUM_COLUMNS = 4;
-
     private GridView mCorpusGrid;
 
+    private final int mGridColumns;
+
     private OnCorpusSelectedListener mListener;
 
     private Corpus mCorpus;
@@ -52,6 +54,7 @@
 
     public CorpusSelectionDialog(Context context) {
         super(context, R.style.Theme_SelectSearchSource);
+        mGridColumns = context.getResources().getInteger(R.integer.corpus_selection_dialog_columns);
     }
 
     /**
@@ -72,12 +75,14 @@
     protected void onCreate(Bundle savedInstanceState) {
         setContentView(R.layout.corpus_selection_dialog);
         mCorpusGrid = (GridView) findViewById(R.id.corpus_grid);
-        mCorpusGrid.setNumColumns(NUM_COLUMNS);
         mCorpusGrid.setOnItemClickListener(new CorpusClickListener());
         // TODO: for some reason, putting this in the XML layout instead makes
         // the list items unclickable.
         mCorpusGrid.setFocusable(true);
 
+        TextView editItems = (TextView) findViewById(R.id.corpus_edit_items);
+        editItems.setOnClickListener(new CorpusEditListener());
+
         Window window = getWindow();
         WindowManager.LayoutParams lp = window.getAttributes();
         lp.width = WindowManager.LayoutParams.MATCH_PARENT;
@@ -128,7 +133,7 @@
         }
         // Dismiss dialog on up move when nothing, or an item on the top row, is selected.
         if (keyCode == KeyEvent.KEYCODE_DPAD_UP) {
-            int selectedRow = mCorpusGrid.getSelectedItemPosition() / NUM_COLUMNS;
+            int selectedRow = mCorpusGrid.getSelectedItemPosition() / mGridColumns;
             if (selectedRow <= 0) {
                 cancel();
                 return true;
@@ -178,7 +183,8 @@
     protected void selectCorpus(Corpus corpus) {
         dismiss();
         if (mListener != null) {
-            mListener.onCorpusSelected(corpus);
+            String corpusName = corpus == null ? null : corpus.getName();
+            mListener.onCorpusSelected(corpusName);
         }
     }
 
@@ -189,7 +195,14 @@
         }
     }
 
+    private class CorpusEditListener implements View.OnClickListener {
+        public void onClick(View v) {
+            Intent intent = SearchSettings.getSearchableItemsIntent(getContext());
+            getContext().startActivity(intent);
+        }
+    }
+
     public interface OnCorpusSelectedListener {
-        void onCorpusSelected(Corpus corpus);
+        void onCorpusSelected(String corpusName);
     }
 }
diff --git a/src/com/android/quicksearchbox/Launcher.java b/src/com/android/quicksearchbox/Launcher.java
deleted file mode 100644
index 0f90da8..0000000
--- a/src/com/android/quicksearchbox/Launcher.java
+++ /dev/null
@@ -1,130 +0,0 @@
-/*
- * Copyright (C) 2009 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.quicksearchbox;
-
-import android.app.SearchManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
-import android.net.Uri;
-import android.os.Bundle;
-import android.speech.RecognizerIntent;
-import android.util.Log;
-
-/**
- * Launches suggestions and searches.
- *
- */
-public class Launcher {
-
-    private static final String TAG = "Launcher";
-
-    private final Context mContext;
-
-    /**
-     * Data sent by the app that launched QSB.
-     */
-    public Launcher(Context context) {
-        mContext = context;
-    }
-
-    /**
-     * Gets the corpus to use for any searches. This is the web corpus in "All" mode,
-     * and the selected corpus otherwise.
-     */
-    public Corpus getSearchCorpus(Corpora corpora, Corpus selectedCorpus) {
-        if (selectedCorpus != null) {
-            return selectedCorpus;
-        } else {
-            Corpus webCorpus = corpora.getWebCorpus();
-            if (webCorpus == null) {
-                Log.e(TAG, "No web corpus");
-            }
-            return webCorpus;
-        }
-    }
-
-    public boolean shouldShowVoiceSearch(Corpus corpus) {
-        if (corpus != null && !corpus.voiceSearchEnabled()) {
-            return false;
-        }
-        return isVoiceSearchAvailable();
-    }
-
-    private boolean isVoiceSearchAvailable() {
-        Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
-        ResolveInfo ri = mContext.getPackageManager().
-                resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
-        return ri != null;
-    }
-
-    public Intent getSuggestionIntent(SuggestionCursor cursor, int position,
-            Bundle appSearchData) {
-        cursor.moveTo(position);
-        Source source = cursor.getSuggestionSource();
-        String action = cursor.getSuggestionIntentAction();
-        // use specific action if supplied, or default action if supplied, or fixed default
-        if (action == null) {
-            action = source.getDefaultIntentAction();
-            if (action == null) {
-                action = Intent.ACTION_SEARCH;
-            }
-        }
-
-        String data = cursor.getSuggestionIntentDataString();
-        String query = cursor.getSuggestionQuery();
-        String userQuery = cursor.getUserQuery();
-        String extraData = cursor.getSuggestionIntentExtraData();
-
-        // Now build the Intent
-        Intent intent = new Intent(action);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        // We need CLEAR_TOP to avoid reusing an old task that has other activities
-        // on top of the one we want.
-        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        if (data != null) {
-            intent.setData(Uri.parse(data));
-        }
-        intent.putExtra(SearchManager.USER_QUERY, userQuery);
-        if (query != null) {
-            intent.putExtra(SearchManager.QUERY, query);
-        }
-        if (extraData != null) {
-            intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
-        }
-        if (appSearchData != null) {
-            intent.putExtra(SearchManager.APP_DATA, appSearchData);
-        }
-
-        intent.setComponent(cursor.getSuggestionSource().getComponentName());
-        return intent;
-    }
-
-    public void launchIntent(Intent intent) {
-        if (intent == null) {
-            return;
-        }
-        try {
-            mContext.startActivity(intent);
-        } catch (RuntimeException ex) {
-            // Since the intents for suggestions specified by suggestion providers,
-            // guard against them not being handled, not allowed, etc.
-            Log.e(TAG, "Failed to start " + intent.toUri(0), ex);
-        }
-    }
-
-}
diff --git a/src/com/android/quicksearchbox/QsbApplication.java b/src/com/android/quicksearchbox/QsbApplication.java
index d565d14..3de143e 100644
--- a/src/com/android/quicksearchbox/QsbApplication.java
+++ b/src/com/android/quicksearchbox/QsbApplication.java
@@ -74,10 +74,6 @@
             mConfig.close();
             mConfig = null;
         }
-        if (mCorpora != null) {
-            mCorpora.close();
-            mCorpora = null;
-        }
         if (mShortcutRepository != null) {
             mShortcutRepository.close();
             mShortcutRepository = null;
@@ -99,6 +95,10 @@
         return mUiThreadHandler;
     }
 
+    public void runOnUiThread(Runnable action) {
+        getMainThreadHandler().post(action);
+    }
+
     /**
      * Gets the QSB configuration object.
      * May be called from any thread.
@@ -129,12 +129,23 @@
     protected Corpora createCorpora() {
         SearchableCorpora corpora = new SearchableCorpora(this, getConfig(), createSources(),
                 createCorpusFactory());
-        corpora.load();
+        corpora.update();
         return corpora;
     }
 
+    /**
+     * Updates the corpora, if they are loaded.
+     * May only be called from the main thread.
+     */
+    public void updateCorpora() {
+        checkThread();
+        if (mCorpora != null) {
+            mCorpora.update();
+        }
+    }
+
     protected Sources createSources() {
-        return new SearchableSources(this, getMainThreadHandler());
+        return new SearchableSources(this);
     }
 
     protected CorpusFactory createCorpusFactory() {
@@ -262,6 +273,7 @@
                 promoter,
                 getShortcutRepository(),
                 getCorpora(),
+                getCorpusRanker(),
                 getLogger());
         return provider;
     }
diff --git a/src/com/android/quicksearchbox/SearchActivity.java b/src/com/android/quicksearchbox/SearchActivity.java
index 1065098..afe7451 100644
--- a/src/com/android/quicksearchbox/SearchActivity.java
+++ b/src/com/android/quicksearchbox/SearchActivity.java
@@ -28,6 +28,7 @@
 import android.app.SearchManager;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.database.DataSetObserver;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
@@ -43,12 +44,12 @@
 import android.view.ViewGroup;
 import android.view.View.OnFocusChangeListener;
 import android.view.inputmethod.InputMethodManager;
+import android.widget.AbsListView;
 import android.widget.EditText;
 import android.widget.ImageButton;
 
 import java.io.File;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 
 /**
@@ -87,6 +88,8 @@
 
     protected SuggestionsAdapter mSuggestionsAdapter;
 
+    private CorporaObserver mCorporaObserver;
+
     protected EditText mQueryTextView;
     // True if the query was empty on the previous call to updateQuery()
     protected boolean mQueryWasEmpty = true;
@@ -98,7 +101,7 @@
     protected ImageButton mVoiceSearchButton;
     protected ImageButton mCorpusIndicator;
 
-    private Launcher mLauncher;
+    private VoiceSearch mVoiceSearch;
 
     private Corpus mCorpus;
     private Bundle mAppSearchData;
@@ -106,14 +109,14 @@
     private String mUserQuery;
     private boolean mSelectAll;
 
-    private Handler mHandler = new Handler();
-    private Runnable mUpdateSuggestionsTask = new Runnable() {
+    private final Handler mHandler = new Handler();
+    private final Runnable mUpdateSuggestionsTask = new Runnable() {
         public void run() {
             updateSuggestions(getQuery());
         }
     };
 
-    private Runnable mShowInputMethodTask = new Runnable() {
+    private final Runnable mShowInputMethodTask = new Runnable() {
         public void run() {
             showInputMethodForQuery();
         }
@@ -135,7 +138,7 @@
         mSuggestionsView = (SuggestionsView) findViewById(R.id.suggestions);
         mSuggestionsView.setSuggestionClickListener(new ClickHandler());
         mSuggestionsView.setSuggestionSelectionListener(new SelectionHandler());
-        mSuggestionsView.setInteractionListener(new InputMethodCloser());
+        mSuggestionsView.setOnScrollListener(new InputMethodCloser());
         mSuggestionsView.setOnKeyListener(new SuggestionsViewKeyListener());
         mSuggestionsView.setOnFocusChangeListener(new SuggestListFocusListener());
 
@@ -147,7 +150,7 @@
         mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn);
         mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator);
 
-        mLauncher = new Launcher(this);
+        mVoiceSearch = new VoiceSearch(this);
 
         mQueryTextView.addTextChangedListener(new SearchTextWatcher());
         mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener());
@@ -176,6 +179,9 @@
         // is called.
         mSuggestionsView.setAdapter(mSuggestionsAdapter);
         mSuggestionsFooter.setAdapter(mSuggestionsAdapter);
+
+        mCorporaObserver = new CorporaObserver();
+        getCorpora().registerDataSetObserver(mCorporaObserver);
     }
 
     private void startMethodTracing() {
@@ -201,7 +207,7 @@
         if (savedInstanceState == null) return;
         String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS);
         String query = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
-        setCorpus(getCorpus(corpusName));
+        setCorpus(corpusName);
         setUserQuery(query);
     }
 
@@ -211,18 +217,17 @@
         // We don't save appSearchData, since we always get the value
         // from the intent and the user can't change it.
 
-        String corpusName = mCorpus == null ? null : mCorpus.getName();
-        outState.putString(INSTANCE_KEY_CORPUS, corpusName);
+        outState.putString(INSTANCE_KEY_CORPUS, getCorpusName());
         outState.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
     }
 
     private void setupFromIntent(Intent intent) {
         if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")");
-        Corpus corpus = getCorpusFromUri(intent.getData());
+        String corpusName = getCorpusNameFromUri(intent.getData());
         String query = intent.getStringExtra(SearchManager.QUERY);
         Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA);
 
-        setCorpus(corpus);
+        setCorpus(corpusName);
         setUserQuery(query);
         mSelectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false);
         mAppSearchData = appSearchData;
@@ -257,11 +262,10 @@
                 .build();
     }
 
-    private Corpus getCorpusFromUri(Uri uri) {
+    private String getCorpusNameFromUri(Uri uri) {
         if (uri == null) return null;
         if (!SCHEME_CORPUS.equals(uri.getScheme())) return null;
-        String name = uri.getAuthority();
-        return getCorpus(name);
+        return uri.getAuthority();
     }
 
     private Corpus getCorpus(String sourceName) {
@@ -274,21 +278,25 @@
         return corpus;
     }
 
-    private void setCorpus(Corpus corpus) {
-        if (DBG) Log.d(TAG, "setCorpus(" + corpus + ")");
-        mCorpus = corpus;
+    private void setCorpus(String corpusName) {
+        if (DBG) Log.d(TAG, "setCorpus(" + corpusName + ")");
+        mCorpus = getCorpus(corpusName);
         Drawable sourceIcon;
-        if (corpus == null) {
+        if (mCorpus == null) {
             sourceIcon = getCorpusViewFactory().getGlobalSearchIcon();
         } else {
-            sourceIcon = corpus.getCorpusIcon();
+            sourceIcon = mCorpus.getCorpusIcon();
         }
-        mSuggestionsAdapter.setCorpus(corpus);
+        mSuggestionsAdapter.setCorpus(mCorpus);
         mCorpusIndicator.setImageDrawable(sourceIcon);
 
         updateUi(getQuery().length() == 0);
     }
 
+    private String getCorpusName() {
+        return mCorpus == null ? null : mCorpus.getName();
+    }
+
     private QsbApplication getQsbApplication() {
         return (QsbApplication) getApplication();
     }
@@ -325,6 +333,7 @@
     protected void onDestroy() {
         if (DBG) Log.d(TAG, "onDestroy()");
         super.onDestroy();
+        getCorpora().unregisterDataSetObserver(mCorporaObserver);
         mSuggestionsFooter.setAdapter(null);
         mSuggestionsView.setAdapter(null);  // closes mSuggestionsAdapter
     }
@@ -454,7 +463,7 @@
     }
 
     protected void updateVoiceSearchButton(boolean queryEmpty) {
-        if (queryEmpty && mLauncher.shouldShowVoiceSearch(mCorpus)) {
+        if (queryEmpty && mVoiceSearch.shouldShowVoiceSearch(mCorpus)) {
             mVoiceSearchButton.setVisibility(View.VISIBLE);
             mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
         } else {
@@ -483,15 +492,18 @@
         }
     }
 
-    protected void onSearchClicked(int method) {
+    /**
+     * @return true if a search was performed as a result of this click, false otherwise.
+     */
+    protected boolean onSearchClicked(int method) {
         String query = getQuery();
         if (DBG) Log.d(TAG, "Search clicked, query=" + query);
 
         // Don't do empty queries
-        if (TextUtils.getTrimmedLength(query) == 0) return;
+        if (TextUtils.getTrimmedLength(query) == 0) return false;
 
-        Corpus searchCorpus = mLauncher.getSearchCorpus(getCorpora(), mCorpus);
-        if (searchCorpus == null) return;
+        Corpus searchCorpus = getSearchCorpus();
+        if (searchCorpus == null) return false;
 
         mTookAction = true;
 
@@ -508,12 +520,13 @@
 
         // Start search
         Intent intent = searchCorpus.createSearchIntent(query, mAppSearchData);
-        mLauncher.launchIntent(intent);
+        launchIntent(intent);
+        return true;
     }
 
     protected void onVoiceSearchClicked() {
         if (DBG) Log.d(TAG, "Voice Search clicked");
-        Corpus searchCorpus = mLauncher.getSearchCorpus(getCorpora(), mCorpus);
+        Corpus searchCorpus = getSearchCorpus();
         if (searchCorpus == null) return;
 
         mTookAction = true;
@@ -523,13 +536,42 @@
 
         // Start voice search
         Intent intent = searchCorpus.createVoiceSearchIntent(mAppSearchData);
-        mLauncher.launchIntent(intent);
+        launchIntent(intent);
+    }
+
+    /**
+     * Gets the corpus to use for any searches. This is the web corpus in "All" mode,
+     * and the selected corpus otherwise.
+     */
+    private Corpus getSearchCorpus() {
+        if (mCorpus != null) {
+            return mCorpus;
+        } else {
+            Corpus webCorpus = getCorpora().getWebCorpus();
+            if (webCorpus == null) {
+                Log.e(TAG, "No web corpus");
+            }
+            return webCorpus;
+        }
     }
 
     protected SuggestionCursor getCurrentSuggestions() {
         return mSuggestionsAdapter.getCurrentSuggestions();
     }
 
+    protected void launchIntent(Intent intent) {
+        if (intent == null) {
+            return;
+        }
+        try {
+            startActivity(intent);
+        } catch (RuntimeException ex) {
+            // Since the intents for suggestions specified by suggestion providers,
+            // guard against them not being handled, not allowed, etc.
+            Log.e(TAG, "Failed to start " + intent.toUri(0), ex);
+        }
+    }
+
     protected boolean launchSuggestion(int position) {
         SuggestionCursor suggestions = getCurrentSuggestions();
         if (position < 0 || position >= suggestions.getCount()) {
@@ -548,8 +590,9 @@
         getShortcutRepository().reportClick(suggestions, position);
 
         // Launch intent
-        Intent intent = mLauncher.getSuggestionIntent(suggestions, position, mAppSearchData);
-        mLauncher.launchIntent(intent);
+        suggestions.moveTo(position);
+        Intent intent = suggestions.getSuggestionIntent(mAppSearchData);
+        launchIntent(intent);
 
         return true;
     }
@@ -637,14 +680,6 @@
         }
     }
 
-    private List<Corpus> getCorporaToQuery() {
-        if (mCorpus == null) {
-            return getCorpusRanker().getRankedCorpora();
-        } else {
-            return Collections.singletonList(mCorpus);
-        }
-    }
-
     private int getMaxSuggestions() {
         Config config = getConfig();
         return mCorpus == null
@@ -669,9 +704,8 @@
         }
 
         query = ltrim(query);
-        List<Corpus> corporaToQuery = getCorporaToQuery();
         Suggestions suggestions = getSuggestionsProvider().getSuggestions(
-                query, corporaToQuery, getMaxSuggestions());
+                query, mCorpus, getMaxSuggestions());
         mSuggestionsAdapter.setSuggestions(suggestions);
     }
 
@@ -729,7 +763,9 @@
         public boolean onKey(View view, int keyCode, KeyEvent event) {
             // Handle IME search action key
             if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() == KeyEvent.ACTION_UP) {
-                onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
+                // if no action was taken, consume the key event so that the keyboard
+                // remains on screen.
+                return !onSearchClicked(Logger.SEARCH_METHOD_KEYBOARD);
             }
             return false;
         }
@@ -760,8 +796,13 @@
         }
     }
 
-    private class InputMethodCloser implements SuggestionsView.InteractionListener {
-        public void onInteraction() {
+    private class InputMethodCloser implements SuggestionsView.OnScrollListener {
+
+        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+                int totalItemCount) {
+        }
+
+        public void onScrollStateChanged(AbsListView view, int scrollState) {
             hideInputMethod();
         }
     }
@@ -774,6 +815,21 @@
        public boolean onSuggestionLongClicked(int position) {
            return SearchActivity.this.onSuggestionLongClicked(position);
        }
+
+       public void onSuggestionQueryRefineClicked(int position) {
+           if (DBG) Log.d(TAG, "query refine clicked, pos " + position);
+           SuggestionCursor suggestions = getCurrentSuggestions();
+           if (suggestions != null) {
+               suggestions.moveTo(position);
+               String query = suggestions.getSuggestionQuery();
+               if (!TextUtils.isEmpty(query)) {
+                   query += " ";
+                   setUserQuery(query);
+                   setQuery(query, false);
+                   updateSuggestions(query);
+               }
+           }
+       }
     }
 
     private class SelectionHandler implements SuggestionSelectionListener {
@@ -823,8 +879,8 @@
 
     private class CorpusSelectionListener
             implements CorpusSelectionDialog.OnCorpusSelectedListener {
-        public void onCorpusSelected(Corpus corpus) {
-            setCorpus(corpus);
+        public void onCorpusSelected(String corpusName) {
+            setCorpus(corpusName);
             updateSuggestions(getQuery());
             mQueryTextView.requestFocus();
             showInputMethodForQuery();
@@ -840,6 +896,14 @@
         }
     }
 
+    private class CorporaObserver extends DataSetObserver {
+        @Override
+        public void onChanged() {
+            setCorpus(getCorpusName());
+            updateSuggestions(getQuery());
+        }
+    }
+
     private static String ltrim(String text) {
         int start = 0;
         int length = text.length();
diff --git a/src/com/android/quicksearchbox/SearchSettings.java b/src/com/android/quicksearchbox/SearchSettings.java
index 8c63696..73dfb2d 100644
--- a/src/com/android/quicksearchbox/SearchSettings.java
+++ b/src/com/android/quicksearchbox/SearchSettings.java
@@ -30,9 +30,7 @@
 import android.preference.CheckBoxPreference;
 import android.preference.Preference;
 import android.preference.PreferenceActivity;
-import android.preference.PreferenceGroup;
 import android.preference.PreferenceScreen;
-import android.preference.Preference.OnPreferenceChangeListener;
 import android.preference.Preference.OnPreferenceClickListener;
 import android.provider.Settings;
 import android.provider.Settings.System;
@@ -42,11 +40,10 @@
 import java.util.List;
 
 /**
- * Activity for setting global search preferences. Changes to search preferences trigger a broadcast
- * intent that causes all SuggestionSources objects to be updated.
+ * Activity for setting global search preferences.
  */
 public class SearchSettings extends PreferenceActivity
-        implements OnPreferenceClickListener, OnPreferenceChangeListener {
+        implements OnPreferenceClickListener {
 
     private static final boolean DBG = false;
     private static final String TAG = "SearchSettings";
@@ -54,6 +51,10 @@
     // Name of the preferences file used to store search preference
     public static final String PREFERENCES_NAME = "SearchSettings";
 
+    // Intent action that opens the "Searchable Items" preference
+    public static final String ACTION_SEARCHABLE_ITEMS =
+            "com.android.quicksearchbox.action.SEARCHABLE_ITEMS";
+
     // Only used to find the preferences after inflating
     private static final String CLEAR_SHORTCUTS_PREF = "clear_shortcuts";
     private static final String SEARCH_ENGINE_SETTINGS_PREF = "search_engine_settings";
@@ -61,11 +62,12 @@
 
     // Preifx of per-corpus enable preference
     private static final String CORPUS_ENABLED_PREF_PREFIX = "enable_corpus_";
+    private static final String VOICE_SEARCH_HINTS_ENABLED_PREF = "voice_search_hints_enabled";
 
     // References to the top-level preference objects
     private Preference mClearShortcutsPreference;
     private PreferenceScreen mSearchEngineSettingsPreference;
-    private PreferenceGroup mSourcePreferences;
+    private CheckBoxPreference mVoiceSearchHintsPreference;
 
     // Dialog ids
     private static final int CLEAR_SHORTCUTS_CONFIRM_DIALOG = 0;
@@ -82,16 +84,24 @@
         mClearShortcutsPreference = preferenceScreen.findPreference(CLEAR_SHORTCUTS_PREF);
         mSearchEngineSettingsPreference = (PreferenceScreen) preferenceScreen.findPreference(
                 SEARCH_ENGINE_SETTINGS_PREF);
-        mSourcePreferences = (PreferenceGroup) getPreferenceScreen().findPreference(
-                SEARCH_CORPORA_PREF);
+        mVoiceSearchHintsPreference = (CheckBoxPreference)
+                preferenceScreen.findPreference(VOICE_SEARCH_HINTS_ENABLED_PREF);
+        Preference corporaPreference = preferenceScreen.findPreference(SEARCH_CORPORA_PREF);
+        corporaPreference.setIntent(getSearchableItemsIntent(this));
 
         mClearShortcutsPreference.setOnPreferenceClickListener(this);
+        mVoiceSearchHintsPreference.setOnPreferenceClickListener(this);
 
         updateClearShortcutsPreference();
-        populateSourcePreference();
         populateSearchEnginePreference();
     }
 
+    public static Intent getSearchableItemsIntent(Context context) {
+        Intent intent = new Intent(SearchSettings.ACTION_SEARCHABLE_ITEMS);
+        intent.setPackage(context.getPackageName());
+        return intent;
+    }
+
     /**
      * Gets the preference key of the preference for whether the given corpus
      * is enabled. The preference is stored in the {@link #PREFERENCES_NAME}
@@ -101,6 +111,16 @@
         return CORPUS_ENABLED_PREF_PREFIX + corpus.getName();
     }
 
+    public static boolean areVoiceSearchHintsEnabled(Context context) {
+        return getSearchPreferences(context).getBoolean(VOICE_SEARCH_HINTS_ENABLED_PREF, true);
+    }
+
+    public static void setVoiceSearchHintsEnabled(Context context, boolean enabled) {
+        getSearchPreferences(context)
+                .edit().putBoolean(VOICE_SEARCH_HINTS_ENABLED_PREF, enabled).commit();
+        SearchWidgetProvider.updateSearchWidgets(context);
+    }
+
     public static SharedPreferences getSearchPreferences(Context context) {
         return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
     }
@@ -109,10 +129,6 @@
         return (QsbApplication) getApplication();
     }
 
-    private Corpora getCorpora() {
-        return getQsbApplication().getCorpora();
-    }
-
     private ShortcutRepository getShortcuts() {
         return getQsbApplication().getShortcutRepository();
     }
@@ -154,47 +170,13 @@
         return resolveInfos.get(0).activityInfo.loadLabel(pm);
     }
 
-    /**
-     * Fills the suggestion source list.
-     */
-    private void populateSourcePreference() {
-        mSourcePreferences.setOrderingAsAdded(false);
-        for (Corpus corpus : getCorpora().getAllCorpora()) {
-            Preference pref = createCorpusPreference(corpus);
-            if (pref != null) {
-                if (DBG) Log.d(TAG, "Adding corpus: " + corpus);
-                mSourcePreferences.addPreference(pref);
-            }
-        }
-    }
-
-    /**
-     * Adds a suggestion source to the list of suggestion source checkbox preferences.
-     */
-    private Preference createCorpusPreference(Corpus corpus) {
-        CheckBoxPreference sourcePref = new CheckBoxPreference(this);
-        sourcePref.setKey(getCorpusEnabledPreference(corpus));
-        // Put web corpus first. The rest are alphabetical.
-        if (corpus.isWebCorpus()) {
-            sourcePref.setOrder(0);
-        }
-        sourcePref.setDefaultValue(getCorpora().isCorpusDefaultEnabled(corpus));
-        sourcePref.setOnPreferenceChangeListener(this);
-        CharSequence label = corpus.getLabel();
-        sourcePref.setTitle(label);
-        CharSequence description = corpus.getSettingsDescription();
-        sourcePref.setSummaryOn(description);
-        sourcePref.setSummaryOff(description);
-        return sourcePref;
-    }
-
-    /**
-     * Handles clicks on the "Clear search shortcuts" preference.
-     */
     public synchronized boolean onPreferenceClick(Preference preference) {
         if (preference == mClearShortcutsPreference) {
             showDialog(CLEAR_SHORTCUTS_CONFIRM_DIALOG);
             return true;
+        } else if (preference == mVoiceSearchHintsPreference) {
+            SearchWidgetProvider.updateSearchWidgets(this);
+            return true;
         }
         return false;
     }
@@ -223,16 +205,11 @@
     /**
      * Informs our listeners about the updated settings data.
      */
-    private void broadcastSettingsChanged() {
+    public static void broadcastSettingsChanged(Context context) {
         // We use a message broadcast since the listeners could be in multiple processes.
         Intent intent = new Intent(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED);
         Log.i(TAG, "Broadcasting: " + intent);
-        sendBroadcast(intent);
-    }
-
-    public synchronized boolean onPreferenceChange(Preference preference, Object newValue) {
-        broadcastSettingsChanged();
-        return true;
+        context.sendBroadcast(intent);
     }
 
     public static boolean getShowWebSuggestions(Context context) {
diff --git a/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java b/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java
index 17d7d7c..b07b21f 100644
--- a/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java
+++ b/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java
@@ -78,7 +78,7 @@
 
     protected void selectCorpus(Corpus corpus) {
         writeWidgetCorpusPref(mAppWidgetId, corpus);
-        updateWidget(corpus);
+        SearchWidgetProvider.updateSearchWidgets(this);
 
         Intent result = new Intent();
         result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
@@ -86,12 +86,6 @@
         finish();
     }
 
-    private void updateWidget(Corpus corpus) {
-        AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
-        SearchWidgetProvider.setupSearchWidget(this, appWidgetManager,
-                mAppWidgetId, corpus);
-    }
-
     private static SharedPreferences getWidgetPreferences(Context context) {
         return context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
     }
diff --git a/src/com/android/quicksearchbox/SearchWidgetProvider.java b/src/com/android/quicksearchbox/SearchWidgetProvider.java
index 2dc6b7f..9a0e62c 100644
--- a/src/com/android/quicksearchbox/SearchWidgetProvider.java
+++ b/src/com/android/quicksearchbox/SearchWidgetProvider.java
@@ -17,69 +17,150 @@
 package com.android.quicksearchbox;
 
 import com.android.common.Search;
+import com.android.common.speech.Recognition;
 import com.android.quicksearchbox.ui.CorpusViewFactory;
 
+import android.app.Activity;
+import android.app.AlarmManager;
 import android.app.PendingIntent;
 import android.app.SearchManager;
 import android.appwidget.AppWidgetManager;
-import android.appwidget.AppWidgetProvider;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
+import android.graphics.Typeface;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.SystemClock;
+import android.speech.RecognizerIntent;
+import android.text.Annotation;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.style.StyleSpan;
 import android.util.Log;
 import android.view.View;
 import android.widget.RemoteViews;
 
+import java.util.ArrayList;
+
 /**
  * Search widget provider.
  *
  */
-public class SearchWidgetProvider extends AppWidgetProvider {
+public class SearchWidgetProvider extends BroadcastReceiver {
 
     private static final boolean DBG = false;
     private static final String TAG = "QSB.SearchWidgetProvider";
 
+    /**
+     * Broadcast intent action for showing the next voice search hint
+     * (if voice search hints are enabled).
+     */
+    private static final String ACTION_NEXT_VOICE_SEARCH_HINT =
+            "com.android.quicksearchbox.action.NEXT_VOICE_SEARCH_HINT";
+
+    /**
+     * Broadcast intent action for disabling voice search hints.
+     */
+    private static final String ACTION_CLOSE_VOICE_SEARCH_HINT =
+            "com.android.quicksearchbox.action.CLOSE_VOICE_SEARCH_HINT";
+
+    /**
+     * Voice search hint update interval in milliseconds.
+     */
+    private static final long VOICE_SEARCH_HINT_UPDATE_INTERVAL
+            = AlarmManager.INTERVAL_FIFTEEN_MINUTES;
+
+    /**
+     * Preference key used for storing the index of the next vocie search hint to show.
+     */
+    private static final String NEXT_VOICE_SEARCH_HINT_INDEX_PREF = "next_voice_search_hint";
+
+    /**
+     * The {@link Search#SOURCE} value used when starting searches from the search widget.
+     */
     private static final String WIDGET_SEARCH_SOURCE = "launcher-widget";
 
     @Override
-    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
-        final int count = appWidgetIds.length;
-        for (int i = 0; i < count; i++) {
-            updateSearchWidget(context, appWidgetManager, appWidgetIds[i]);
+    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);
+        } else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
+            updateSearchWidgets(context);
         }
     }
 
-    private void updateSearchWidget(Context context, AppWidgetManager appWidgetManager,
-            int appWidgetId) {
-        String corpusName = SearchWidgetConfigActivity.readWidgetCorpusPref(context, appWidgetId);
-        Corpus corpus = corpusName == null ? null : getCorpora(context).getCorpus(corpusName);
-        setupSearchWidget(context, appWidgetManager, appWidgetId, corpus);
+    /**
+     * Updates all search widgets.
+     */
+    public static void updateSearchWidgets(Context context) {
+        updateSearchWidgets(context, true, null);
     }
 
-    public static void setupSearchWidget(Context context, AppWidgetManager appWidgetManager,
-            int appWidgetId, Corpus corpus) {
-        if (DBG) Log.d(TAG, "setupSearchWidget()");
-        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.search_widget);
+    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();
+        }
+        if (updateVoiceSearchHint) {
+            scheduleVoiceSearchHintUpdates(context, needsVoiceSearchHint);
+        }
+    }
+
+    /**
+     * Gets the component name of this search widget provider.
+     */
+    private static ComponentName myComponentName(Context context) {
+        String pkg = context.getPackageName();
+        String cls = pkg + ".SearchWidgetProvider";
+        return new ComponentName(pkg, cls);
+    }
+
+    private static SearchWidgetState getSearchWidgetState(Context context, 
+            int appWidgetId, CharSequence voiceSearchHint) {
+        String corpusName =
+                SearchWidgetConfigActivity.readWidgetCorpusPref(context, appWidgetId);
+        Corpus corpus = corpusName == null ? null : getCorpora(context).getCorpus(corpusName);
+        if (DBG) {
+            Log.d(TAG, "Updating appwidget " + appWidgetId + ", corpus=" + corpus
+                    + ",VS hint=" + voiceSearchHint);
+        }
+        SearchWidgetState state = new SearchWidgetState(appWidgetId);
 
         Bundle widgetAppData = new Bundle();
         widgetAppData.putString(Search.SOURCE, WIDGET_SEARCH_SOURCE);
 
         // Corpus indicator
-        bindCorpusIndicator(context, views, widgetAppData, corpus);
+        state.setCorpusIconUri(getCorpusIconUri(context, corpus));
 
-        // Hint
-        CharSequence hint;
-        int backgroundId;
+        Intent corpusIconIntent = new Intent(SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS);
+        corpusIconIntent.setPackage(context.getPackageName());
+        corpusIconIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+                | Intent.FLAG_ACTIVITY_CLEAR_TOP
+                | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+        corpusIconIntent.putExtra(SearchManager.APP_DATA, widgetAppData);
+        corpusIconIntent.setData(SearchActivity.getCorpusUri(corpus));
+        state.setCorpusIndicatorIntent(corpusIconIntent);
+
+        // Query text view hint
         if (corpus == null || corpus.isWebCorpus()) {
-            hint = null;
-            backgroundId = R.drawable.textfield_search_empty_google;
+            state.setQueryTextViewBackgroundResource(R.drawable.textfield_search_empty_google);
         } else {
-            hint = corpus.getHint();
-            backgroundId = R.drawable.textfield_search_empty;
+            state.setQueryTextViewHint(corpus.getHint());
+            state.setQueryTextViewBackgroundResource(R.drawable.textfield_search_empty);
         }
-        views.setCharSequence(R.id.search_widget_text, "setHint", hint);
-        views.setInt(R.id.search_widget_text, "setBackgroundResource", backgroundId);
 
         // Text field click
         Intent qsbIntent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
@@ -89,51 +170,31 @@
                 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
         qsbIntent.putExtra(SearchManager.APP_DATA, widgetAppData);
         qsbIntent.setData(SearchActivity.getCorpusUri(corpus));
-        setOnClickIntent(context, views, R.id.search_widget_text, qsbIntent);
+        state.setQueryTextViewIntent(qsbIntent);
 
+        // Voice search button
         Intent voiceSearchIntent = getVoiceSearchIntent(context, corpus, widgetAppData);
-        if (voiceSearchIntent != null) {
-            setOnClickIntent(context, views, R.id.search_widget_voice_btn, voiceSearchIntent);
-            views.setViewVisibility(R.id.search_widget_voice_btn, View.VISIBLE);
-        } else {
-            views.setViewVisibility(R.id.search_widget_voice_btn, View.GONE);
+        state.setVoiceSearchIntent(voiceSearchIntent);
+        if (voiceSearchIntent != null
+                && RecognizerIntent.ACTION_WEB_SEARCH.equals(voiceSearchIntent.getAction())) {
+            state.setShouldShowVoiceSearchHint(true);
+            state.setVoiceSearchHint(formatVoiceSearchHint(context, voiceSearchHint));
         }
 
-        appWidgetManager.updateAppWidget(appWidgetId, views);
-    }
-
-    private static void bindCorpusIndicator(Context context, RemoteViews views,
-            Bundle widgetAppData, Corpus corpus) {
-        Uri sourceIconUri = getCorpusIconUri(context, corpus);
-        views.setImageViewUri(R.id.corpus_indicator, sourceIconUri);
-
-        Intent intent = new Intent(SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS);
-        intent.setPackage(context.getPackageName());
-        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
-                | Intent.FLAG_ACTIVITY_CLEAR_TOP
-                | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
-        intent.putExtra(SearchManager.APP_DATA, widgetAppData);
-        intent.setData(SearchActivity.getCorpusUri(corpus));
-        setOnClickIntent(context, views, R.id.corpus_indicator, intent);
+        return state;
     }
 
     private static Intent getVoiceSearchIntent(Context context, Corpus corpus,
             Bundle widgetAppData) {
-        Launcher launcher = new Launcher(context);
-        if (!launcher.shouldShowVoiceSearch(corpus)) return null;
+        VoiceSearch voiceSearch = new VoiceSearch(context);
+        if (!voiceSearch.shouldShowVoiceSearch(corpus)) return null;
         if (corpus == null) {
-            return WebCorpus.createVoiceWebSearchIntent(widgetAppData);
+            return voiceSearch.createVoiceWebSearchIntent(widgetAppData);
         } else {
             return corpus.createVoiceSearchIntent(widgetAppData);
         }
     }
 
-    private static void setOnClickIntent(Context context, RemoteViews views,
-            int viewId, Intent intent) {
-        PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
-        views.setOnClickPendingIntent(viewId, pendingIntent);
-    }
-
     private static Uri getCorpusIconUri(Context context, Corpus corpus) {
         if (corpus == null) {
             return getCorpusViewFactory(context).getGlobalSearchIconUri();
@@ -141,6 +202,94 @@
         return corpus.getCorpusIconUri();
     }
 
+    private static CharSequence formatVoiceSearchHint(Context context, CharSequence hint) {
+        if (TextUtils.isEmpty(hint)) return null;
+        SpannableStringBuilder spannedHint = new SpannableStringBuilder(
+                context.getString(R.string.voice_search_hint_quotation_start));
+        spannedHint.append(hint);
+        Object[] items = spannedHint.getSpans(0, spannedHint.length(), Object.class);
+        for (Object item : items) {
+            if (item instanceof Annotation) {
+                Annotation annotation = (Annotation) item;
+                if (annotation.getKey().equals("action")
+                        && annotation.getValue().equals("true")) {
+                    final int start = spannedHint.getSpanStart(annotation);
+                    final int end = spannedHint.getSpanEnd(annotation);
+                    spannedHint.removeSpan(item);
+                    spannedHint.setSpan(new StyleSpan(Typeface.BOLD), start, end, 0);
+                }
+            }
+        }
+        spannedHint.append(context.getString(R.string.voice_search_hint_quotation_end));
+        return spannedHint;
+    }
+
+    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 && SearchSettings.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);
+        }
+    }
+
+    /**
+     * Requests an asynchronous update of the voice search hints.
+     */
+    private static void getHintsFromVoiceSearch(Context context) {
+        if (!SearchSettings.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);
+    }
+
+    private static class HintReceiver extends BroadcastReceiver {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (getResultCode() != Activity.RESULT_OK) {
+                return;
+            }
+            ArrayList<CharSequence> hints = getResultExtras(true)
+                    .getCharSequenceArrayList(Recognition.EXTRA_HINT_STRINGS);
+            CharSequence hint = getNextHint(context, hints);
+            updateSearchWidgets(context, false, hint);
+        }
+    }
+
+    /**
+     * Gets the next formatted hint, if there are any hints.
+     * Must be called on the application main thread.
+     *
+     * @return A hint, or {@code null} if no hints are available.
+     */
+    private static CharSequence getNextHint(Context context, ArrayList<CharSequence> hints) {
+        if (hints == null || hints.isEmpty()) return null;
+        int i = getNextVoiceSearchHintIndex(context, hints.size());
+        return hints.get(i);
+    }
+
+    private static int getNextVoiceSearchHintIndex(Context context, int size) {
+        int i = getAndIncrementIntPreference(
+                SearchSettings.getSearchPreferences(context),
+                NEXT_VOICE_SEARCH_HINT_INDEX_PREF);
+        return i % size;
+    }
+
+    // TODO: Could this be made atomic to avoid races?
+    private static int getAndIncrementIntPreference(SharedPreferences prefs, String name) {
+        int i = prefs.getInt(name, 0);
+        prefs.edit().putInt(name, i + 1).commit();
+        return i;
+    }
+
     private static QsbApplication getQsbApplication(Context context) {
         return (QsbApplication) context.getApplicationContext();
     }
@@ -153,4 +302,109 @@
         return getQsbApplication(context).getCorpusViewFactory();
     }
 
+    private static class SearchWidgetState {
+        private final int mAppWidgetId;
+        private Uri mCorpusIconUri;
+        private Intent mCorpusIndicatorIntent;
+        private CharSequence mQueryTextViewHint;
+        private int mQueryTextViewBackgroundResource;
+        private Intent mQueryTextViewIntent;
+        private Intent mVoiceSearchIntent;
+        private boolean mShouldShowVoiceSearchHint;
+        private CharSequence mVoiceSearchHint;
+
+        public SearchWidgetState(int appWidgetId) {
+            mAppWidgetId = appWidgetId;
+        }
+
+        public boolean shouldShowVoiceSearchHint() {
+            return mShouldShowVoiceSearchHint;
+        }
+
+        public void setShouldShowVoiceSearchHint(boolean shouldShowVoiceSearchHint) {
+            mShouldShowVoiceSearchHint = shouldShowVoiceSearchHint;
+        }
+
+        public void setCorpusIconUri(Uri corpusIconUri) {
+            mCorpusIconUri = corpusIconUri;
+        }
+
+        public void setCorpusIndicatorIntent(Intent corpusIndicatorIntent) {
+            mCorpusIndicatorIntent = corpusIndicatorIntent;
+        }
+
+        public void setQueryTextViewHint(CharSequence queryTextViewHint) {
+            mQueryTextViewHint = queryTextViewHint;
+        }
+
+        public void setQueryTextViewBackgroundResource(int queryTextViewBackgroundResource) {
+            mQueryTextViewBackgroundResource = queryTextViewBackgroundResource;
+        }
+
+        public void setQueryTextViewIntent(Intent queryTextViewIntent) {
+            mQueryTextViewIntent = queryTextViewIntent;
+        }
+
+        public void setVoiceSearchIntent(Intent voiceSearchIntent) {
+            mVoiceSearchIntent = voiceSearchIntent;
+        }
+
+        public void setVoiceSearchHint(CharSequence voiceSearchHint) {
+            mVoiceSearchHint = voiceSearchHint;
+        }
+
+        public void updateWidget(Context context, AppWidgetManager appWidgetManager) {
+            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.search_widget);
+            // Corpus indicator
+            views.setImageViewUri(R.id.corpus_indicator, mCorpusIconUri);
+            setOnClickActivityIntent(context, views, R.id.corpus_indicator,
+                    mCorpusIndicatorIntent);
+            // Query TextView
+            views.setCharSequence(R.id.search_widget_text, "setHint", mQueryTextViewHint);
+            views.setInt(R.id.search_widget_text, "setBackgroundResource",
+                    mQueryTextViewBackgroundResource);
+            setOnClickActivityIntent(context, views, R.id.search_widget_text,
+                    mQueryTextViewIntent);
+            // Voice Search button
+            if (mVoiceSearchIntent != null) {
+                setOnClickActivityIntent(context, views, R.id.search_widget_voice_btn,
+                        mVoiceSearchIntent);
+                views.setViewVisibility(R.id.search_widget_voice_btn, View.VISIBLE);
+            } else {
+                views.setViewVisibility(R.id.search_widget_voice_btn, View.GONE);
+            }
+            // Voice Search hints
+            if (mShouldShowVoiceSearchHint && !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));
+                setOnClickBroadcastIntent(context, views, R.id.voice_search_hint_close,
+                        closeHintIntent);
+
+                views.setViewVisibility(R.id.voice_search_hint, View.VISIBLE);
+            } else {
+                views.setViewVisibility(R.id.voice_search_hint, View.GONE);
+            }
+            appWidgetManager.updateAppWidget(mAppWidgetId, views);
+        }
+
+        private void setOnClickBroadcastIntent(Context context, RemoteViews views, int viewId,
+                Intent intent) {
+            PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0);
+            views.setOnClickPendingIntent(viewId, pendingIntent);
+        }
+
+        private void setOnClickActivityIntent(Context context, RemoteViews views, int viewId,
+                Intent intent) {
+            PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
+            views.setOnClickPendingIntent(viewId, pendingIntent);
+        }
+    }
+
 }
diff --git a/src/com/android/quicksearchbox/SearchableCorpora.java b/src/com/android/quicksearchbox/SearchableCorpora.java
index 4a38d81..b3d38f0 100644
--- a/src/com/android/quicksearchbox/SearchableCorpora.java
+++ b/src/com/android/quicksearchbox/SearchableCorpora.java
@@ -20,6 +20,7 @@
 import android.content.SharedPreferences;
 import android.database.DataSetObservable;
 import android.database.DataSetObserver;
+import android.text.TextUtils;
 import android.util.Log;
 
 import java.util.ArrayList;
@@ -29,7 +30,7 @@
 import java.util.List;
 
 /**
- * Maintains the list of all suggestion sources.
+ * Maintains the list of all corpora.
  */
 public class SearchableCorpora implements Corpora {
 
@@ -44,8 +45,6 @@
     private final CorpusFactory mCorpusFactory;
     private final SharedPreferences mPreferences;
 
-    private boolean mLoaded = false;
-
     private Sources mSources;
     // Maps corpus names to corpora
     private HashMap<String,Corpus> mCorporaByName;
@@ -73,24 +72,15 @@
         return mContext;
     }
 
-    private void checkLoaded() {
-        if (!mLoaded) {
-            throw new IllegalStateException("corpora not loaded.");
-        }
-    }
-
     public Collection<Corpus> getAllCorpora() {
-        checkLoaded();
         return Collections.unmodifiableCollection(mCorporaByName.values());
     }
 
     public Collection<Corpus> getEnabledCorpora() {
-        checkLoaded();
         return mEnabledCorpora;
     }
 
     public Corpus getCorpus(String name) {
-        checkLoaded();
         return mCorporaByName.get(name);
     }
 
@@ -99,48 +89,20 @@
     }
 
     public Corpus getCorpusForSource(Source source) {
-        checkLoaded();
         return mCorporaBySource.get(source);
     }
 
     public Source getSource(String name) {
-        checkLoaded();
+        if (TextUtils.isEmpty(name)) {
+            Log.w(TAG, "Empty source name");
+            return null;
+        }
         return mSources.getSource(name);
     }
 
-    /**
-     * After calling, clients must call {@link #close()} when done with this object.
-     */
-    public void load() {
-        if (mLoaded) {
-            throw new IllegalStateException("load(): Already loaded.");
-        }
+    public void update() {
+        mSources.update();
 
-        mSources.registerDataSetObserver(new DataSetObserver() {
-            @Override
-            public void onChanged() {
-                updateCorpora();
-            }
-        });
-
-        // will cause a callback to updateCorpora()
-        mSources.load();
-        mLoaded = true;
-    }
-
-    /**
-     * Releases all resources used by this object. It is possible to call
-     * {@link #load()} again after calling this method.
-     */
-    public void close() {
-        checkLoaded();
-
-        mSources.close();
-        mSources = null;
-        mLoaded = false;
-    }
-
-    private void updateCorpora() {
         Collection<Corpus> corpora = mCorpusFactory.createCorpora(mSources);
 
         mCorporaByName = new HashMap<String,Corpus>(corpora.size());
diff --git a/src/com/android/quicksearchbox/SearchableCorpusFactory.java b/src/com/android/quicksearchbox/SearchableCorpusFactory.java
index 45a1831..4d34fc5 100644
--- a/src/com/android/quicksearchbox/SearchableCorpusFactory.java
+++ b/src/com/android/quicksearchbox/SearchableCorpusFactory.java
@@ -19,6 +19,7 @@
 import com.android.quicksearchbox.util.Factory;
 
 import android.content.Context;
+import android.util.Log;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -30,6 +31,8 @@
  */
 public class SearchableCorpusFactory implements CorpusFactory {
 
+    private static final String TAG = "QSB.SearchableCorpusFactory";
+
     private final Context mContext;
 
     private final Factory<Executor> mWebCorpusExecutorFactory;
@@ -61,8 +64,8 @@
      * @param sources All available sources.
      */
     protected void addSpecialCorpora(ArrayList<Corpus> corpora, Sources sources) {
-        corpora.add(createWebCorpus(sources));
-        corpora.add(createAppsCorpus(sources));
+        addCorpus(corpora, createWebCorpus(sources));
+        addCorpus(corpora, createAppsCorpus(sources));
     }
 
     /**
@@ -82,14 +85,26 @@
         // Creates corpora for all unclaimed sources
         for (Source source : sources.getSources()) {
             if (!claimedSources.contains(source)) {
-                corpora.add(createSingleSourceCorpus(source));
+                addCorpus(corpora, createSingleSourceCorpus(source));
             }
         }
     }
 
+    private void addCorpus(ArrayList<Corpus> corpora, Corpus corpus) {
+        if (corpus != null) corpora.add(corpus);
+    }
+
     protected Corpus createWebCorpus(Sources sources) {
         Source webSource = sources.getWebSearchSource();
+        if (webSource != null && !webSource.canRead()) {
+            Log.w(TAG, "Can't read web source " + webSource.getName());
+            webSource = null;
+        }
         Source browserSource = getBrowserSource(sources);
+        if (browserSource != null && !browserSource.canRead()) {
+            Log.w(TAG, "Can't read browser source " + browserSource.getName());
+            browserSource = null;
+        }
         Executor executor = createWebCorpusExecutor();
         return new WebCorpus(mContext, executor, webSource, browserSource);
     }
@@ -100,6 +115,7 @@
     }
 
     protected Corpus createSingleSourceCorpus(Source source) {
+        if (!source.canRead()) return null;
         return new SingleSourceCorpus(mContext, source);
     }
 
diff --git a/src/com/android/quicksearchbox/SearchableItemsSettings.java b/src/com/android/quicksearchbox/SearchableItemsSettings.java
new file mode 100644
index 0000000..5dc4cf8
--- /dev/null
+++ b/src/com/android/quicksearchbox/SearchableItemsSettings.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2010 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.quicksearchbox;
+
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceGroup;
+import android.preference.Preference.OnPreferenceChangeListener;
+import android.util.Log;
+
+/**
+ * Activity for selecting searchable items.
+ */
+public class SearchableItemsSettings extends PreferenceActivity
+        implements OnPreferenceChangeListener {
+
+    private static final boolean DBG = false;
+    private static final String TAG = "QSB.SearchableItemsSettings";
+
+    // Only used to find the preferences after inflating
+    private static final String SEARCH_CORPORA_PREF = "search_corpora";
+
+    // References to the top-level preference objects
+    private PreferenceGroup mCorporaPreferences;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        getPreferenceManager().setSharedPreferencesName(SearchSettings.PREFERENCES_NAME);
+
+        addPreferencesFromResource(R.xml.preferences_searchable_items);
+
+        mCorporaPreferences = (PreferenceGroup) getPreferenceScreen().findPreference(
+                SEARCH_CORPORA_PREF);
+
+        populateSourcePreference();
+    }
+
+    private QsbApplication getQsbApplication() {
+        return (QsbApplication) getApplication();
+    }
+
+    private Corpora getCorpora() {
+        return getQsbApplication().getCorpora();
+    }
+
+    /**
+     * Fills the suggestion source list.
+     */
+    private void populateSourcePreference() {
+        mCorporaPreferences.setOrderingAsAdded(false);
+        for (Corpus corpus : getCorpora().getAllCorpora()) {
+            Preference pref = createCorpusPreference(corpus);
+            if (pref != null) {
+                if (DBG) Log.d(TAG, "Adding corpus: " + corpus);
+                mCorporaPreferences.addPreference(pref);
+            }
+        }
+    }
+
+    /**
+     * Adds a suggestion source to the list of suggestion source checkbox preferences.
+     */
+    private Preference createCorpusPreference(Corpus corpus) {
+        CheckBoxPreference sourcePref = new CheckBoxPreference(this);
+        sourcePref.setKey(SearchSettings.getCorpusEnabledPreference(corpus));
+        // Put web corpus first. The rest are alphabetical.
+        if (corpus.isWebCorpus()) {
+            sourcePref.setOrder(0);
+        }
+        sourcePref.setDefaultValue(getCorpora().isCorpusDefaultEnabled(corpus));
+        sourcePref.setOnPreferenceChangeListener(this);
+        CharSequence label = corpus.getLabel();
+        sourcePref.setTitle(label);
+        CharSequence description = corpus.getSettingsDescription();
+        sourcePref.setSummaryOn(description);
+        sourcePref.setSummaryOff(description);
+        return sourcePref;
+    }
+
+    public boolean onPreferenceChange(Preference preference, Object newValue) {
+        SearchSettings.broadcastSettingsChanged(this);
+        return true;
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/SearchableSource.java b/src/com/android/quicksearchbox/SearchableSource.java
index a5c8e96..aa00c21 100644
--- a/src/com/android/quicksearchbox/SearchableSource.java
+++ b/src/com/android/quicksearchbox/SearchableSource.java
@@ -69,7 +69,7 @@
     // Cached icon for the activity
     private Drawable.ConstantState mSourceIcon = null;
 
-    private final IconLoader mIconLoader;
+    private IconLoader mIconLoader;
 
     public SearchableSource(Context context, SearchableInfo searchable)
             throws NameNotFoundException {
@@ -81,7 +81,6 @@
         mActivityInfo = pm.getActivityInfo(componentName, 0);
         PackageInfo pkgInfo = pm.getPackageInfo(componentName.getPackageName(), 0);
         mVersionCode = pkgInfo.versionCode;
-        mIconLoader = createIconLoader(context, searchable.getSuggestPackage());
     }
 
     protected Context getContext() {
@@ -98,8 +97,9 @@
     public boolean canRead() {
         String authority = mSearchable.getSuggestAuthority();
         if (authority == null) {
-            Log.w(TAG, getName() + " has no searchSuggestAuthority");
-            return false;
+            // TODO: maybe we should have a way to distinguish between having suggestions
+            // and being readable.
+            return true;
         }
 
         Uri.Builder uriBuilder = new Uri.Builder()
@@ -161,12 +161,20 @@
         return false;
     }
 
-    private IconLoader createIconLoader(Context context, String providerPackage) {
-        if (providerPackage == null) return null;
-        return new CachingIconLoader(new PackageIconLoader(context, providerPackage));
+    private IconLoader getIconLoader() {
+        if (mIconLoader == null) {
+            // Get icons from the package containing the suggestion provider, if any
+            String iconPackage = mSearchable.getSuggestPackage();
+            if (iconPackage == null) {
+                // Fall back to the package containing the searchable activity
+                iconPackage = mSearchable.getSearchActivity().getPackageName();
+            }
+            mIconLoader = new CachingIconLoader(new PackageIconLoader(mContext, iconPackage));
+        }
+        return mIconLoader;
     }
 
-    public ComponentName getComponentName() {
+    public ComponentName getIntentComponent() {
         return mSearchable.getSearchActivity();
     }
 
@@ -179,11 +187,11 @@
     }
 
     public Drawable getIcon(String drawableId) {
-        return mIconLoader == null ? null : mIconLoader.getIcon(drawableId);
+        return getIconLoader().getIcon(drawableId);
     }
 
     public Uri getIconUri(String drawableId) {
-        return mIconLoader == null ? null : mIconLoader.getIconUri(drawableId);
+        return getIconLoader().getIconUri(drawableId);
     }
 
     public CharSequence getLabel() {
@@ -236,7 +244,7 @@
     }
 
     public Intent createSearchIntent(String query, Bundle appData) {
-        return createSourceSearchIntent(getComponentName(), query, appData);
+        return createSourceSearchIntent(getIntentComponent(), query, appData);
     }
 
     public static Intent createSourceSearchIntent(ComponentName activity, String query,
@@ -261,7 +269,7 @@
 
     public Intent createVoiceSearchIntent(Bundle appData) {
         if (mSearchable.getVoiceSearchLaunchWebSearch()) {
-            return WebCorpus.createVoiceWebSearchIntent(appData);
+            return new VoiceSearch(mContext).createVoiceWebSearchIntent(appData);
         } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
             return createVoiceAppSearchIntent(appData);
         }
diff --git a/src/com/android/quicksearchbox/SearchableSources.java b/src/com/android/quicksearchbox/SearchableSources.java
index a8f0e62..315b382 100644
--- a/src/com/android/quicksearchbox/SearchableSources.java
+++ b/src/com/android/quicksearchbox/SearchableSources.java
@@ -18,16 +18,11 @@
 
 import android.app.SearchManager;
 import android.app.SearchableInfo;
-import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
-import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
-import android.database.DataSetObservable;
-import android.database.DataSetObserver;
-import android.os.Handler;
 import android.util.Log;
 
 import java.util.Collection;
@@ -43,15 +38,8 @@
     private static final boolean DBG = false;
     private static final String TAG = "QSB.SearchableSources";
 
-    // The number of milliseconds that source update requests are delayed to
-    // allow grouping multiple requests.
-    private static final long UPDATE_SOURCES_DELAY_MILLIS = 200;
-
-    private final DataSetObservable mDataSetObservable = new DataSetObservable();
-
     private final Context mContext;
     private final SearchManager mSearchManager;
-    private boolean mLoaded;
 
     // All suggestion sources, by name.
     private HashMap<String, Source> mSources;
@@ -59,31 +47,16 @@
     // The web search source to use.
     private Source mWebSearchSource;
 
-    private final Handler mUiThread;
-
-    private Runnable mUpdateSources = new Runnable() {
-        public void run() {
-            mUiThread.removeCallbacks(this);
-            updateSources();
-            notifyDataSetChanged();
-        }
-    };
-
     /**
      *
      * @param context Used for looking up source information etc.
      */
-    public SearchableSources(Context context, Handler uiThread) {
+    public SearchableSources(Context context) {
         mContext = context;
-        mUiThread = uiThread;
         mSearchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
-        mLoaded = false;
     }
 
     public Collection<Source> getSources() {
-        if (!mLoaded) {
-            throw new IllegalStateException("getSources(): sources not loaded.");
-        }
         return mSources.values();
     }
 
@@ -92,62 +65,14 @@
     }
 
     public Source getWebSearchSource() {
-        if (!mLoaded) {
-            throw new IllegalStateException("getWebSearchSource(): sources not loaded.");
-        }
         return mWebSearchSource;
     }
 
-    // Broadcast receiver for package change notifications
-    private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
-        @Override
-        public void onReceive(Context context, Intent intent) {
-            String action = intent.getAction();
-            if (SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action)
-                    || SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED.equals(action)) {
-                if (DBG) Log.d(TAG, "onReceive(" + intent + ")");
-                // TODO: Instead of rebuilding the whole list on every change,
-                // just add, remove or update the application that has changed.
-                // Adding and updating seem tricky, since I can't see an easy way to list the
-                // launchable activities in a given package.
-                mUiThread.postDelayed(mUpdateSources, UPDATE_SOURCES_DELAY_MILLIS);
-            }
-        }
-    };
-
-    public void load() {
-        if (mLoaded) {
-            throw new IllegalStateException("load(): Already loaded.");
-        }
-
-        // Listen for searchables changes.
-        IntentFilter intentFilter = new IntentFilter();
-        intentFilter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED);
-        intentFilter.addAction(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED);
-        mContext.registerReceiver(mBroadcastReceiver, intentFilter);
-
-        // update list of sources
-        updateSources();
-
-        mLoaded = true;
-
-        notifyDataSetChanged();
-    }
-
-    public void close() {
-        mContext.unregisterReceiver(mBroadcastReceiver);
-
-        mDataSetObservable.unregisterAll();
-
-        mSources = null;
-        mLoaded = false;
-    }
-
     /**
-     * Loads the list of suggestion sources.
+     * Updates the list of suggestion sources.
      */
-    private void updateSources() {
-        if (DBG) Log.d(TAG, "updateSources()");
+    public void update() {
+        if (DBG) Log.d(TAG, "update()");
         mSources = new HashMap<String,Source>();
 
         addSearchableSources();
@@ -164,7 +89,7 @@
         }
         for (SearchableInfo searchable : searchables) {
             SearchableSource source = createSearchableSource(searchable);
-            if (source != null && source.canRead()) {
+            if (source != null) {
                 if (DBG) Log.d(TAG, "Created source " + source);
                 addSource(source);
             }
@@ -204,16 +129,4 @@
             return null;
         }
     }
-
-    public void registerDataSetObserver(DataSetObserver observer) {
-        mDataSetObservable.registerObserver(observer);
-    }
-
-    public void unregisterDataSetObserver(DataSetObserver observer) {
-        mDataSetObservable.unregisterObserver(observer);
-    }
-
-    protected void notifyDataSetChanged() {
-        mDataSetObservable.notifyChanged();
-    }
 }
diff --git a/src/com/android/quicksearchbox/ShortcutRepository.java b/src/com/android/quicksearchbox/ShortcutRepository.java
index fbbe155..bfc24ef 100644
--- a/src/com/android/quicksearchbox/ShortcutRepository.java
+++ b/src/com/android/quicksearchbox/ShortcutRepository.java
@@ -16,7 +16,7 @@
 
 package com.android.quicksearchbox;
 
-import java.util.List;
+import java.util.Collection;
 import java.util.Map;
 
 /**
@@ -54,7 +54,7 @@
      * @param maxShortcuts The maximum number of shortcuts to return.
      * @return A cursor containing shortcutted results for the query.
      */
-    SuggestionCursor getShortcutsForQuery(String query, List<Corpus> allowedCorpora,
+    SuggestionCursor getShortcutsForQuery(String query, Collection<Corpus> allowedCorpora,
             int maxShortcuts);
 
     /**
diff --git a/src/com/android/quicksearchbox/ShortcutRepositoryImplLog.java b/src/com/android/quicksearchbox/ShortcutRepositoryImplLog.java
index ea8150c..1331afb 100644
--- a/src/com/android/quicksearchbox/ShortcutRepositoryImplLog.java
+++ b/src/com/android/quicksearchbox/ShortcutRepositoryImplLog.java
@@ -34,8 +34,8 @@
 import android.util.Log;
 
 import java.io.File;
+import java.util.Collection;
 import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Executor;
 
@@ -223,7 +223,7 @@
         reportClickAtTime(suggestions, position, now);
     }
 
-    public SuggestionCursor getShortcutsForQuery(String query, List<Corpus> allowedCorpora,
+    public SuggestionCursor getShortcutsForQuery(String query, Collection<Corpus> allowedCorpora,
             int maxShortcuts) {
         ShortcutCursor shortcuts = getShortcutsForQuery(query, allowedCorpora, maxShortcuts,
                         System.currentTimeMillis());
@@ -245,7 +245,7 @@
     }
 
     /* package for testing */ ShortcutCursor getShortcutsForQuery(String query,
-            List<Corpus> allowedCorpora, int maxShortcuts, long now) {
+            Collection<Corpus> allowedCorpora, int maxShortcuts, long now) {
         if (DBG) Log.d(TAG, "getShortcutsForQuery(" + query + "," + allowedCorpora + ")");
         String sql = query.length() == 0 ? mEmptyQueryShortcutQuery : mShortcutQuery;
         String[] params = buildShortcutQueryParams(query, now);
diff --git a/src/com/android/quicksearchbox/ShortcutsProvider.java b/src/com/android/quicksearchbox/ShortcutsProvider.java
new file mode 100644
index 0000000..acc1822
--- /dev/null
+++ b/src/com/android/quicksearchbox/ShortcutsProvider.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2010 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.quicksearchbox;
+
+import android.app.SearchManager;
+import android.content.ComponentName;
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Binder;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * Handles broadcast intents for adding shortcuts to QSB.
+ */
+public class ShortcutsProvider extends ContentProvider {
+
+    private static final boolean DBG = true;
+    private static final String TAG = "QSB.ExternalShortcutReceiver";
+
+    public static final String EXTRA_SHORTCUT_SOURCE = "shortcut_source";
+
+    private static final int URI_CODE_SHORTCUTS = 0;
+
+    private UriMatcher mUriMatcher;
+
+    @Override
+    public boolean onCreate() {
+        mUriMatcher = buildUriMatcher();
+        return true;
+    }
+
+    private UriMatcher buildUriMatcher() {
+        String authority = getAuthority();
+        UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
+        matcher.addURI(authority, "shortcuts", URI_CODE_SHORTCUTS);
+        return matcher;
+    }
+
+    private String getAuthority() {
+        return getContext().getPackageName() + ".shortcuts";
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        switch (mUriMatcher.match(uri)) {
+            case URI_CODE_SHORTCUTS:
+                return SearchManager.SUGGEST_MIME_TYPE;
+            default:
+                throw new IllegalArgumentException("Unknown URI: " + uri);
+        }
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        switch (mUriMatcher.match(uri)) {
+            case URI_CODE_SHORTCUTS:
+                addShortcut(values);
+                return null;
+            default:
+                throw new IllegalArgumentException("Unknown URI: " + uri);
+        }
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        throw new UnsupportedOperationException();
+    }
+
+    private void addShortcut(final ContentValues shortcut) {
+        String sourceName = shortcut.getAsString(EXTRA_SHORTCUT_SOURCE);
+        if (TextUtils.isEmpty(sourceName)) {
+            Log.e(TAG, "Missing " + EXTRA_SHORTCUT_SOURCE);
+            return;
+        }
+        final ComponentName sourceComponent = ComponentName.unflattenFromString(sourceName);
+        if (!checkCallingPackage(sourceComponent.getPackageName())) {
+            Log.w(TAG, "Got shortcut for " + sourceComponent + " from a different process");
+            return;
+        }
+
+        getQsbApplication().runOnUiThread(new Runnable() {
+            public void run() {
+                storeShortcut(sourceComponent, shortcut);
+            }
+        });
+    }
+
+    // Called on the main thread
+    private void storeShortcut(ComponentName sourceComponent, ContentValues shortcut) {
+        if (DBG) Log.d(TAG, "Adding (PID: " + Binder.getCallingPid() + "): " + shortcut);
+
+        Source source = getCorpora().getSource(sourceComponent.flattenToShortString());
+        if (source == null) {
+            Log.w(TAG, "Unknown shortcut source " + sourceComponent);
+            return;
+        }
+
+        String userQuery = shortcut.getAsString(SearchManager.USER_QUERY);
+        if (userQuery == null) userQuery = "";
+
+        DataSuggestionCursor cursor = new DataSuggestionCursor(userQuery);
+        cursor.add(makeSuggestion(source, shortcut));
+        getShortcutRepository().reportClick(cursor, 0);
+    }
+
+    private boolean checkCallingPackage(String packageName) {
+        int callingUid = Binder.getCallingUid();
+        PackageManager pm = getContext().getPackageManager();
+        String[] uidPkgs = pm.getPackagesForUid(callingUid);
+        if (uidPkgs == null) return false;
+        for (String uidPkg : uidPkgs) {
+            if (packageName.equals(uidPkg)) return true;
+        }
+        return false;
+    }
+
+    private SuggestionData makeSuggestion(Source source, ContentValues shortcut) {
+        String format = shortcut.getAsString(SearchManager.SUGGEST_COLUMN_FORMAT);
+        String text1 = shortcut.getAsString(SearchManager.SUGGEST_COLUMN_TEXT_1);
+        String text2 = shortcut.getAsString(SearchManager.SUGGEST_COLUMN_TEXT_2);
+        String text2Url = shortcut.getAsString(SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
+        String icon1 = shortcut.getAsString(SearchManager.SUGGEST_COLUMN_ICON_1);
+        String icon2 = shortcut.getAsString(SearchManager.SUGGEST_COLUMN_ICON_2);
+        String shortcutId = shortcut.getAsString(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
+        boolean spinnerWhileRefreshing = unboxBoolean(
+                shortcut.getAsBoolean(SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING),
+                false);
+        String intentAction = shortcut.getAsString(SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
+        String intentData = shortcut.getAsString(SearchManager.SUGGEST_COLUMN_INTENT_DATA);
+        String intentExtraData =
+                shortcut.getAsString(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
+        String query = shortcut.getAsString(SearchManager.SUGGEST_COLUMN_QUERY);
+
+        SuggestionData suggestion = new SuggestionData(source);
+        suggestion.setFormat(format);
+        suggestion.setText1(text1);
+        suggestion.setText2(text2);
+        suggestion.setText2Url(text2Url);
+        suggestion.setIcon1(icon1);
+        suggestion.setIcon2(icon2);
+        suggestion.setShortcutId(shortcutId);
+        suggestion.setSpinnerWhileRefreshing(spinnerWhileRefreshing);
+        suggestion.setIntentAction(intentAction);
+        suggestion.setIntentData(intentData);
+        suggestion.setIntentExtraData(intentExtraData);
+        suggestion.setSuggestionQuery(query);
+        return suggestion;
+    }
+
+    private static boolean unboxBoolean(Boolean value, boolean defValue) {
+        return value == null ? defValue : value;
+    }
+
+    private QsbApplication getQsbApplication() {
+        return (QsbApplication) getContext().getApplicationContext();
+    }
+
+    private ShortcutRepository getShortcutRepository() {
+        return getQsbApplication().getShortcutRepository();
+    }
+
+    private Corpora getCorpora() {
+        return getQsbApplication().getCorpora();
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/Source.java b/src/com/android/quicksearchbox/Source.java
index 04cab39..f32ca15 100644
--- a/src/com/android/quicksearchbox/Source.java
+++ b/src/com/android/quicksearchbox/Source.java
@@ -29,10 +29,9 @@
 public interface Source extends SuggestionCursorProvider<SourceResult> {
 
     /**
-     * Gets the name of the activity that this source is for. When a suggestion is
-     * clicked, the resulting intent will be sent to this activity.
+     * Gets the name activity that intents from this source are sent to.
      */
-    ComponentName getComponentName();
+    ComponentName getIntentComponent();
 
     /**
      * Gets the version code of the source. This is expected to change when the app that
@@ -107,6 +106,11 @@
     Intent createVoiceSearchIntent(Bundle appData);
 
     /**
+     * Checks if the current process can read the suggestions from this source.
+     */
+    boolean canRead();
+
+    /**
      * Gets suggestions from this source.
      *
      * @param query The user query.
@@ -126,11 +130,6 @@
     SuggestionCursor refreshShortcut(String shortcutId, String extraData);
 
     /**
-     * Checks whether this is a web suggestion source.
-     */
-    boolean isWebSuggestionSource();
-
-    /**
      * Checks whether the text in the query field should come from the suggestion intent data.
      */
     boolean shouldRewriteQueryFromData();
diff --git a/src/com/android/quicksearchbox/Sources.java b/src/com/android/quicksearchbox/Sources.java
index bc8641f..76a7cce 100644
--- a/src/com/android/quicksearchbox/Sources.java
+++ b/src/com/android/quicksearchbox/Sources.java
@@ -1,8 +1,6 @@
 
 package com.android.quicksearchbox;
 
-import android.database.DataSetObserver;
-
 import java.util.Collection;
 
 /**
@@ -28,29 +26,8 @@
     Source getWebSearchSource();
 
     /**
-     * After calling, clients must call {@link #close()} when done with this object.
+     * Updates the list of sources.
      */
-    void load();
-
-    /**
-     * Releases all resources used by this object. It is possible to call
-     * {@link #load()} again after calling this method.
-     */
-    void close();
-
-    /**
-     * Register an observer that is called when changes happen to this data set.
-     *
-     * @param observer gets notified when the data set changes.
-     */
-    void registerDataSetObserver(DataSetObserver observer);
-
-    /**
-     * Unregister an observer that has previously been registered with
-     * {@link #registerDataSetObserver(DataSetObserver)}
-     *
-     * @param observer the observer to unregister.
-     */
-    void unregisterDataSetObserver(DataSetObserver observer);
+    void update();
 
 }
diff --git a/src/com/android/quicksearchbox/SuggestionCursor.java b/src/com/android/quicksearchbox/SuggestionCursor.java
index 93effe4..23ab5b2 100644
--- a/src/com/android/quicksearchbox/SuggestionCursor.java
+++ b/src/com/android/quicksearchbox/SuggestionCursor.java
@@ -16,7 +16,9 @@
 
 package com.android.quicksearchbox;
 
+import android.content.Intent;
 import android.database.DataSetObserver;
+import android.os.Bundle;
 
 
 /**
@@ -144,10 +146,15 @@
     String getSuggestionIntentDataString();
 
     /**
-     * Gets the data associated with this suggestion's intent.
+     * Gets the query associated with this suggestion's intent.
      */
     String getSuggestionQuery();
 
+    /**
+     * Gets the intent launched by this suggestion.
+     */
+    Intent getSuggestionIntent(Bundle appSearchData);
+
     String getSuggestionDisplayQuery();
 
     /**
diff --git a/src/com/android/quicksearchbox/SuggestionsProvider.java b/src/com/android/quicksearchbox/SuggestionsProvider.java
index b7ac4b9..3070a10 100644
--- a/src/com/android/quicksearchbox/SuggestionsProvider.java
+++ b/src/com/android/quicksearchbox/SuggestionsProvider.java
@@ -16,7 +16,6 @@
 
 package com.android.quicksearchbox;
 
-import java.util.List;
 
 /**
  * Provides a set of suggestion results for a query..
@@ -27,10 +26,10 @@
      * Gets suggestions for a query.
      *
      * @param query The query.
-     * @param corpora The corpora to query.
+     * @param singleCorpus The corpora to query, {@code null} for all enabled corpora.
      * @param maxSuggestions The maximum number of suggestions to return.
      */
-    Suggestions getSuggestions(String query, List<Corpus> corpora, int maxSuggestions);
+    Suggestions getSuggestions(String query, Corpus singleCorpus, int maxSuggestions);
 
     void close();
 }
diff --git a/src/com/android/quicksearchbox/SuggestionsProviderImpl.java b/src/com/android/quicksearchbox/SuggestionsProviderImpl.java
index 36edb19..2c7b493 100644
--- a/src/com/android/quicksearchbox/SuggestionsProviderImpl.java
+++ b/src/com/android/quicksearchbox/SuggestionsProviderImpl.java
@@ -19,14 +19,14 @@
 import com.android.quicksearchbox.util.BatchingNamedTaskExecutor;
 import com.android.quicksearchbox.util.Consumer;
 import com.android.quicksearchbox.util.NamedTaskExecutor;
-import com.android.quicksearchbox.util.Util;
 
 import android.os.Handler;
 import android.util.Log;
 
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
 /**
  * Suggestions provider implementation.
@@ -49,12 +49,14 @@
 
     private final ShortcutRepository mShortcutRepo;
 
-    private final Logger mLogger;
-
     private final ShouldQueryStrategy mShouldQueryStrategy = new ShouldQueryStrategy();
 
     private final Corpora mCorpora;
 
+    private final CorpusRanker mCorpusRanker;
+
+    private final Logger mLogger;
+
     private BatchingNamedTaskExecutor mBatchingExecutor;
 
     public SuggestionsProviderImpl(Config config,
@@ -63,14 +65,16 @@
             Promoter promoter,
             ShortcutRepository shortcutRepo,
             Corpora corpora,
+            CorpusRanker corpusRanker,
             Logger logger) {
         mConfig = config;
         mQueryExecutor = queryExecutor;
         mPublishThread = publishThread;
         mPromoter = promoter;
         mShortcutRepo = shortcutRepo;
-        mLogger = logger;
         mCorpora = corpora;
+        mCorpusRanker = corpusRanker;
+        mLogger = logger;
     }
 
     public void close() {
@@ -87,16 +91,19 @@
         }
     }
 
-    protected SuggestionCursor getShortcutsForQuery(String query, List<Corpus> corpora,
+    protected SuggestionCursor getShortcutsForQuery(String query, Corpus singleCorpus,
             int maxShortcuts) {
         if (mShortcutRepo == null) return null;
-        return mShortcutRepo.getShortcutsForQuery(query, corpora, maxShortcuts);
+        Collection<Corpus> allowedCorpora = mCorpora.getEnabledCorpora();
+        return mShortcutRepo.getShortcutsForQuery(query, allowedCorpora, maxShortcuts);
     }
 
     /**
      * Gets the sources that should be queried for the given query.
      */
-    private List<Corpus> getCorporaToQuery(String query, List<Corpus> orderedCorpora) {
+    private List<Corpus> getCorporaToQuery(String query, Corpus singleCorpus) {
+        if (singleCorpus != null) return Collections.singletonList(singleCorpus);
+        List<Corpus> orderedCorpora = mCorpusRanker.getRankedCorpora();
         ArrayList<Corpus> corporaToQuery = new ArrayList<Corpus>(orderedCorpora.size());
         for (Corpus corpus : orderedCorpora) {
             if (shouldQueryCorpus(corpus, query)) {
@@ -121,16 +128,16 @@
         }
     }
 
-    public Suggestions getSuggestions(String query, List<Corpus> corpora, int maxSuggestions) {
+    public Suggestions getSuggestions(String query, Corpus singleCorpus, int maxSuggestions) {
         if (DBG) Log.d(TAG, "getSuggestions(" + query + ")");
         cancelPendingTasks();
-        List<Corpus> corporaToQuery = getCorporaToQuery(query, corpora);
+        List<Corpus> corporaToQuery = getCorporaToQuery(query, singleCorpus);
         final Suggestions suggestions = new Suggestions(mPromoter,
                 maxSuggestions,
                 query,
                 corporaToQuery.size());
         int maxShortcuts = mConfig.getMaxShortcutsReturned();
-        SuggestionCursor shortcuts = getShortcutsForQuery(query, corpora, maxShortcuts);
+        SuggestionCursor shortcuts = getShortcutsForQuery(query, singleCorpus, maxShortcuts);
         if (shortcuts != null) {
             suggestions.setShortcuts(shortcuts);
         }
diff --git a/src/com/android/quicksearchbox/VoiceSearch.java b/src/com/android/quicksearchbox/VoiceSearch.java
new file mode 100644
index 0000000..acc5860
--- /dev/null
+++ b/src/com/android/quicksearchbox/VoiceSearch.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 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.quicksearchbox;
+
+import android.app.SearchManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.speech.RecognizerIntent;
+
+/**
+ * Voice Search integration.
+ */
+public class VoiceSearch {
+
+    private final Context mContext;
+
+    public VoiceSearch(Context context) {
+        mContext = context;
+    }
+
+    public boolean shouldShowVoiceSearch(Corpus corpus) {
+        if (corpus != null && !corpus.voiceSearchEnabled()) {
+            return false;
+        }
+        return isVoiceSearchAvailable();
+    }
+
+    private boolean isVoiceSearchAvailable() {
+        Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
+        ResolveInfo ri = mContext.getPackageManager().
+                resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
+        return ri != null;
+    }
+
+    public Intent createVoiceWebSearchIntent(Bundle appData) {
+        Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
+                RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
+        if (appData != null) {
+            intent.putExtra(SearchManager.APP_DATA, appData);
+        }
+        return intent;
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/WebCorpus.java b/src/com/android/quicksearchbox/WebCorpus.java
index 84306bb..26fe504 100644
--- a/src/com/android/quicksearchbox/WebCorpus.java
+++ b/src/com/android/quicksearchbox/WebCorpus.java
@@ -25,7 +25,6 @@
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
-import android.speech.RecognizerIntent;
 import android.util.Patterns;
 import android.webkit.URLUtil;
 
@@ -65,27 +64,16 @@
     }
 
     public Intent createSearchIntent(String query, Bundle appData) {
-        return isUrl(query)? createBrowseIntent(query) : createWebSearchIntent(query, appData);
-    }
-
-    private static Intent createWebSearchIntent(String query, Bundle appData) {
-        Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        // We need CLEAR_TOP to avoid reusing an old task that has other activities
-        // on top of the one we want.
-        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        intent.putExtra(SearchManager.USER_QUERY, query);
-        intent.putExtra(SearchManager.QUERY, query);
-        if (appData != null) {
-            intent.putExtra(SearchManager.APP_DATA, appData);
+        if (isUrl(query)) {
+            return createBrowseIntent(query);
+        } else if (mWebSearchSource != null){
+            return mWebSearchSource.createSearchIntent(query, appData);
+        } else {
+            return null;
         }
-        // TODO: Include something like this, to let the web search activity
-        // know how this query was started.
-        //intent.putExtra(SearchManager.SEARCH_MODE, SearchManager.MODE_GLOBAL_SEARCH_TYPED_QUERY);
-        return intent;
     }
 
-    private static Intent createBrowseIntent(String query) {
+    private Intent createBrowseIntent(String query) {
         Intent intent = new Intent(Intent.ACTION_VIEW);
         intent.addCategory(Intent.CATEGORY_BROWSABLE);
         intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
@@ -114,18 +102,7 @@
     }
 
     public Intent createVoiceSearchIntent(Bundle appData) {
-        return createVoiceWebSearchIntent(appData);
-    }
-
-    public static Intent createVoiceWebSearchIntent(Bundle appData) {
-        Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
-                RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
-        if (appData != null) {
-            intent.putExtra(SearchManager.APP_DATA, appData);
-        }
-        return intent;
+        return new VoiceSearch(getContext()).createVoiceWebSearchIntent(appData);
     }
 
     private int getCorpusIconResource() {
diff --git a/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java b/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java
index 3a29ae9..a4b007f 100644
--- a/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java
+++ b/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java
@@ -86,10 +86,44 @@
             Log.d(TAG, "bindAsSuggestion(), text1=" + text1 + ",text2=" + text2
                     + ",icon1=" + icon1 + ",icon2=" + icon2);
         }
+        // If there is no text for the second line, allow the first line to be up to two lines
+        if (TextUtils.isEmpty(text2)) {
+            mText1.setSingleLine(false);
+            mText1.setMaxLines(2);
+            mText1.setEllipsize(TextUtils.TruncateAt.START);
+        } else {
+            mText1.setSingleLine(true);
+            mText1.setMaxLines(1);
+            mText1.setEllipsize(TextUtils.TruncateAt.MIDDLE);
+        }
         setText1(text1);
         setText2(text2);
         setIcon1(icon1);
         setIcon2(icon2);
+        updateRefinable(suggestion);
+    }
+
+    protected void updateRefinable(SuggestionCursor suggestion) {
+        boolean refinable = mIcon2.getDrawable() == null
+                && !TextUtils.isEmpty(suggestion.getSuggestionQuery());
+        setRefinable(suggestion, refinable);
+    }
+
+    protected void setRefinable(SuggestionCursor suggestion, boolean refinable) {
+        if (refinable) {
+            final int position = suggestion.getPosition();
+            mIcon2.setOnClickListener(new View.OnClickListener() {
+                public void onClick(View v) {
+                    Log.d(TAG, "Clicked query refine");
+                    SuggestionsView suggestions = (SuggestionsView) getParent();
+                    suggestions.onIcon2Clicked(position);
+                }
+            });
+            Drawable icon2 = getContext().getResources().getDrawable(R.drawable.refine_query);
+            setIcon2(icon2);
+        } else {
+            mIcon2.setOnClickListener(null);
+        }
     }
 
     private CharSequence formatUrl(CharSequence url) {
diff --git a/src/com/android/quicksearchbox/ui/SuggestionClickListener.java b/src/com/android/quicksearchbox/ui/SuggestionClickListener.java
index 9cc3b10..10fc714 100644
--- a/src/com/android/quicksearchbox/ui/SuggestionClickListener.java
+++ b/src/com/android/quicksearchbox/ui/SuggestionClickListener.java
@@ -20,16 +20,25 @@
  * Listener interface for clicks on suggestions.
  */
 public interface SuggestionClickListener {
+
     /**
      * Called when a suggestion is clicked.
      *
      * @param position Position of the clicked suggestion.
      */
     void onSuggestionClicked(int position);
+
     /**
      * Called when a suggestion is long clicked.
      *
      * @param position Position of the long clicked suggestion.
      */
     boolean onSuggestionLongClicked(int position);
+
+    /**
+     * Called when the "query refine" button of a suggestion is clicked.
+     *
+     * @param position Position of the suggestion.
+     */
+    void onSuggestionQueryRefineClicked(int position);
 }
diff --git a/src/com/android/quicksearchbox/ui/SuggestionsView.java b/src/com/android/quicksearchbox/ui/SuggestionsView.java
index 6fa7ac7..a73f1fa 100644
--- a/src/com/android/quicksearchbox/ui/SuggestionsView.java
+++ b/src/com/android/quicksearchbox/ui/SuggestionsView.java
@@ -22,7 +22,6 @@
 import android.graphics.Rect;
 import android.util.AttributeSet;
 import android.util.Log;
-import android.view.MotionEvent;
 import android.view.View;
 import android.widget.AdapterView;
 import android.widget.ListView;
@@ -39,8 +38,6 @@
 
     private SuggestionSelectionListener mSuggestionSelectionListener;
 
-    private InteractionListener mInteractionListener;
-
     public SuggestionsView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
@@ -61,10 +58,6 @@
         mSuggestionSelectionListener = listener;
     }
 
-    public void setInteractionListener(InteractionListener listener) {
-        mInteractionListener = listener;
-    }
-
     /**
      * Gets the position of the selected suggestion.
      *
@@ -84,14 +77,6 @@
     }
 
     @Override
-    public boolean onInterceptTouchEvent(MotionEvent event) {
-        if (event.getAction() == MotionEvent.ACTION_DOWN && mInteractionListener != null) {
-            mInteractionListener.onInteraction();
-        }
-        return super.onInterceptTouchEvent(event);
-    }
-
-    @Override
     protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
         if (DBG) {
@@ -130,13 +115,6 @@
         }
     }
 
-    public interface InteractionListener {
-        /**
-         * Called when the user interacts with this view.
-         */
-        void onInteraction();
-    }
-
     private class ItemClickListener implements AdapterView.OnItemClickListener {
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
             if (DBG) Log.d(TAG, "onItemClick(" + position + ")");
@@ -174,4 +152,11 @@
             fireNothingSelected();
         }
     }
+
+    public void onIcon2Clicked(int position) {
+        if (mSuggestionClickListener != null) {
+            mSuggestionClickListener.onSuggestionQueryRefineClicked(position);
+        }
+    }
+
 }
diff --git a/tests/src/com/android/quicksearchbox/MockCorpora.java b/tests/src/com/android/quicksearchbox/MockCorpora.java
index cd0eb14..7a6adb0 100644
--- a/tests/src/com/android/quicksearchbox/MockCorpora.java
+++ b/tests/src/com/android/quicksearchbox/MockCorpora.java
@@ -102,8 +102,7 @@
         return true;
     }
 
-    public void close() {
-        // Nothing to release
+    public void update() {
     }
 
     public void registerDataSetObserver(DataSetObserver observer) {
diff --git a/tests/src/com/android/quicksearchbox/MockShortcutRepository.java b/tests/src/com/android/quicksearchbox/MockShortcutRepository.java
index ab7ea38..73c2223 100644
--- a/tests/src/com/android/quicksearchbox/MockShortcutRepository.java
+++ b/tests/src/com/android/quicksearchbox/MockShortcutRepository.java
@@ -16,7 +16,7 @@
 
 package com.android.quicksearchbox;
 
-import java.util.List;
+import java.util.Collection;
 import java.util.Map;
 
 /**
@@ -31,7 +31,7 @@
     public void close() {
     }
 
-    public SuggestionCursor getShortcutsForQuery(String query, List<Corpus> corporaToQuery,
+    public SuggestionCursor getShortcutsForQuery(String query, Collection<Corpus> corporaToQuery,
             int maxShortcuts) {
         // TODO: should look at corporaToQuery
         DataSuggestionCursor cursor = new DataSuggestionCursor(query);
diff --git a/tests/src/com/android/quicksearchbox/MockSource.java b/tests/src/com/android/quicksearchbox/MockSource.java
index f07471c..2b5ff46 100644
--- a/tests/src/com/android/quicksearchbox/MockSource.java
+++ b/tests/src/com/android/quicksearchbox/MockSource.java
@@ -47,7 +47,7 @@
         mVersionCode = versionCode;
     }
 
-    public ComponentName getComponentName() {
+    public ComponentName getIntentComponent() {
         // Not an activity, but no code should treat it as one.
         return new ComponentName("com.android.quicksearchbox",
                 getClass().getName() + "." + mName);
@@ -58,7 +58,7 @@
     }
 
     public String getName() {
-        return getComponentName().flattenToShortString();
+        return getIntentComponent().flattenToShortString();
     }
 
     public String getDefaultIntentAction() {
@@ -101,6 +101,10 @@
         return null;
     }
 
+    public boolean canRead() {
+        return true;
+    }
+
     public SourceResult getSuggestions(String query, int queryLimit) {
         if (query.length() == 0) {
             return null;
@@ -155,6 +159,10 @@
         return null;
     }
 
+    public boolean isExternal() {
+        return false;
+    }
+
     public boolean isWebSuggestionSource() {
         return false;
     }
diff --git a/tests/src/com/android/quicksearchbox/MockSources.java b/tests/src/com/android/quicksearchbox/MockSources.java
index 1bf39cc..2817007 100644
--- a/tests/src/com/android/quicksearchbox/MockSources.java
+++ b/tests/src/com/android/quicksearchbox/MockSources.java
@@ -27,8 +27,6 @@
  */
 public class MockSources implements Sources {
 
-    private final DataSetObservable mDataSetObservable = new DataSetObservable();
-
     private final HashMap<String, Source> mSources = new HashMap<String, Source>();
 
     public void addSource(Source source) {
@@ -47,21 +45,7 @@
         return null;
     }
 
-    public void load() {
-        notifyDataSetChanged();
+    public void update() {
     }
 
-    public void close() {
-    }
-
-    public void registerDataSetObserver(DataSetObserver observer) {
-        mDataSetObservable.registerObserver(observer);
-    }
-
-    public void unregisterDataSetObserver(DataSetObserver observer) {
-        mDataSetObservable.unregisterObserver(observer);
-    }
-
-    protected void notifyDataSetChanged() {
-        mDataSetObservable.notifyChanged();
-    }}
+}
diff --git a/tests/src/com/android/quicksearchbox/SearchableCorporaTest.java b/tests/src/com/android/quicksearchbox/SearchableCorporaTest.java
index 164823e..6173163 100644
--- a/tests/src/com/android/quicksearchbox/SearchableCorporaTest.java
+++ b/tests/src/com/android/quicksearchbox/SearchableCorporaTest.java
@@ -39,7 +39,7 @@
         sources.addSource(MockSource.SOURCE_1);
         sources.addSource(MockSource.SOURCE_2);
         mCorpora = new SearchableCorpora(mContext, config, sources, new MockCorpusFactory());
-        mCorpora.load();
+        mCorpora.update();
     }
 
     public void testGetAllCorpora() {
diff --git a/tests/src/com/android/quicksearchbox/ShortcutPromoterTest.java b/tests/src/com/android/quicksearchbox/ShortcutPromoterTest.java
index 2107e8b..3ae4efa 100644
--- a/tests/src/com/android/quicksearchbox/ShortcutPromoterTest.java
+++ b/tests/src/com/android/quicksearchbox/ShortcutPromoterTest.java
@@ -42,7 +42,7 @@
     protected void setUp() throws Exception {
         mQuery = "foo";
         List<Corpus> corpora = Arrays.asList(MockCorpus.CORPUS_1, MockCorpus.CORPUS_2);
-        mShortcuts = new MockShortcutRepository().getShortcutsForQuery(mQuery, corpora, 8);
+        mShortcuts = new MockShortcutRepository().getShortcutsForQuery(mQuery, null, 8);
         mSuggestions = new ArrayList<CorpusResult>();
         for (Corpus corpus : corpora) {
             mSuggestions.add(corpus.getSuggestions(mQuery, 10));
diff --git a/tests/src/com/android/quicksearchbox/ShortcutRepositoryTest.java b/tests/src/com/android/quicksearchbox/ShortcutRepositoryTest.java
index b41997c..7874552 100644
--- a/tests/src/com/android/quicksearchbox/ShortcutRepositoryTest.java
+++ b/tests/src/com/android/quicksearchbox/ShortcutRepositoryTest.java
@@ -27,6 +27,7 @@
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
@@ -803,7 +804,7 @@
         }
     }
 
-    void assertShortcuts(String message, String query, List<Corpus> allowedCorpora,
+    void assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora,
             SuggestionCursor expected) {
         SuggestionCursor cursor = mRepo.getShortcutsForQuery(query, allowedCorpora,
                 mConfig.getMaxShortcutsReturned(), NOW);
@@ -814,7 +815,7 @@
         }
     }
 
-    void assertShortcuts(String message, String query, List<Corpus> allowedCorpora,
+    void assertShortcuts(String message, String query, Collection<Corpus> allowedCorpora,
             SuggestionData... expected) {
         assertShortcuts(message, query, allowedCorpora, new DataSuggestionCursor(query, expected));
     }