Refactoring to make QSB more modular

Change-Id: I3bd5444bdcf4ac62a921c8c921306cc17aa440dc
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 049de81..22fc1f3 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -37,7 +37,7 @@
 
     <application android:label="@string/app_name"
                  android:icon="@drawable/search_app_icon"
-                 android:name=".QsbApplication">
+                 android:name=".QsbApplicationWrapper">
         <activity android:name=".SearchActivity"
                   android:label="@string/app_name"
                   android:launchMode="singleTask"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c90ee5d..5a4f1fd 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -88,6 +88,9 @@
          Home/res/values/strings.xml -->
     <string name="google_search_hint">Google Search</string>
 
+    <!-- Search settings description for the Google search source. -->
+    <string name="google_search_description">Google search suggestions</string>
+
     <!-- Settings category title for 'Google search settings' settings activity -->
     <string name="google_search_settings">Google search</string>
 
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 901f069..012fe51 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -43,6 +43,7 @@
     </PreferenceCategory>
 
     <PreferenceCategory
+            android:key="voice_search_settings_category"
             android:title="@string/voice_search_category_title">
 
         <CheckBoxPreference
diff --git a/src/com/android/quicksearchbox/AbstractSource.java b/src/com/android/quicksearchbox/AbstractSource.java
new file mode 100644
index 0000000..7197852
--- /dev/null
+++ b/src/com/android/quicksearchbox/AbstractSource.java
@@ -0,0 +1,113 @@
+/*
+ * 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.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * Abstract suggestion source implementation.
+ */
+public abstract class AbstractSource implements Source {
+
+    private static final String TAG = "QSB.AbstractSource";
+
+    private final Context mContext;
+
+    private IconLoader mIconLoader;
+
+    public AbstractSource(Context context) {
+        mContext = context;
+    }
+
+    protected Context getContext() {
+        return mContext;
+    }
+
+    protected IconLoader getIconLoader() {
+        if (mIconLoader == null) {
+            String iconPackage = getIconPackage();
+            mIconLoader = new CachingIconLoader(new PackageIconLoader(mContext, iconPackage));
+        }
+        return mIconLoader;
+    }
+
+    protected abstract String getIconPackage();
+
+    public Drawable getIcon(String drawableId) {
+        return getIconLoader().getIcon(drawableId);
+    }
+
+    public Uri getIconUri(String drawableId) {
+        return getIconLoader().getIconUri(drawableId);
+    }
+
+    public Intent createSearchIntent(String query, Bundle appData) {
+        return createSourceSearchIntent(getIntentComponent(), query, appData);
+    }
+
+    public static Intent createSourceSearchIntent(ComponentName activity, String query,
+            Bundle appData) {
+        if (activity == null) {
+            Log.w(TAG, "Tried to create search intent with no target activity");
+            return null;
+        }
+        Intent intent = new Intent(Intent.ACTION_SEARCH);
+        intent.setComponent(activity);
+        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);
+        }
+        return intent;
+    }
+
+    protected Intent createVoiceWebSearchIntent(Bundle appData) {
+        return QsbApplication.get(mContext).getVoiceSearch()
+                .createVoiceWebSearchIntent(appData);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o != null && o.getClass().equals(this.getClass())) {
+            AbstractSource s = (AbstractSource) o;
+            return s.getName().equals(getName());
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return getName().hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return "Source{name=" + getName() + "}";
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/AppsCorpus.java b/src/com/android/quicksearchbox/AppsCorpus.java
index ed35825..ca41c7c 100644
--- a/src/com/android/quicksearchbox/AppsCorpus.java
+++ b/src/com/android/quicksearchbox/AppsCorpus.java
@@ -93,7 +93,7 @@
     private Intent createAppSearchIntent(String query, Bundle appData) {
         ComponentName name = getComponentName(getContext(), R.string.apps_search_activity);
         if (name == null) return null;
-        Intent intent = SearchableSource.createSourceSearchIntent(name, query, appData);
+        Intent intent = AbstractSource.createSourceSearchIntent(name, query, appData);
         if (intent == null) return null;
         ActivityInfo ai = intent.resolveActivityInfo(getContext().getPackageManager(), 0);
         if (ai != null) {
diff --git a/src/com/android/quicksearchbox/Config.java b/src/com/android/quicksearchbox/Config.java
index 6e5b5df..987ba9f 100644
--- a/src/com/android/quicksearchbox/Config.java
+++ b/src/com/android/quicksearchbox/Config.java
@@ -214,4 +214,7 @@
         return PUBLISH_RESULT_DELAY_MILLIS;
     }
 
+    public boolean allowVoiceSearchHints() {
+        return false;
+    }
 }
diff --git a/src/com/android/quicksearchbox/CorporaUpdateReceiver.java b/src/com/android/quicksearchbox/CorporaUpdateReceiver.java
index 11163c7..20ac202 100644
--- a/src/com/android/quicksearchbox/CorporaUpdateReceiver.java
+++ b/src/com/android/quicksearchbox/CorporaUpdateReceiver.java
@@ -42,11 +42,7 @@
     }
 
     private void updateCorpora(Context context) {
-        getQsbApplication(context).updateCorpora();
-    }
-
-    private QsbApplication getQsbApplication(Context context) {
-        return (QsbApplication) context.getApplicationContext();
+        QsbApplication.get(context).updateCorpora();
     }
 
 }
diff --git a/src/com/android/quicksearchbox/Corpus.java b/src/com/android/quicksearchbox/Corpus.java
index b4a0720..1affbe0 100644
--- a/src/com/android/quicksearchbox/Corpus.java
+++ b/src/com/android/quicksearchbox/Corpus.java
@@ -93,4 +93,9 @@
      * Checks if this corpus should be hidden from the corpus selector.
      */
     boolean isCorpusHidden();
+
+    /**
+     * Checks if this corpus is location aware.
+     */
+    boolean isLocationAware();
 }
diff --git a/src/com/android/quicksearchbox/CorpusSelectionDialog.java b/src/com/android/quicksearchbox/CorpusSelectionDialog.java
index 68f2187..fcff3a9 100644
--- a/src/com/android/quicksearchbox/CorpusSelectionDialog.java
+++ b/src/com/android/quicksearchbox/CorpusSelectionDialog.java
@@ -169,7 +169,7 @@
     }
 
     private QsbApplication getQsbApplication() {
-        return (QsbApplication) getContext().getApplicationContext();
+        return QsbApplication.get(getContext());
     }
 
     private CorpusRanker getCorpusRanker() {
diff --git a/src/com/android/quicksearchbox/CursorBackedSourceResult.java b/src/com/android/quicksearchbox/CursorBackedSourceResult.java
new file mode 100644
index 0000000..db847e4
--- /dev/null
+++ b/src/com/android/quicksearchbox/CursorBackedSourceResult.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.database.Cursor;
+
+public class CursorBackedSourceResult extends CursorBackedSuggestionCursor
+        implements SourceResult {
+
+    private final Source mSource;
+
+    public CursorBackedSourceResult(Source source, String userQuery) {
+        this(source, userQuery, null);
+    }
+
+    public CursorBackedSourceResult(Source source, String userQuery, Cursor cursor) {
+        super(userQuery, cursor);
+        mSource = source;
+    }
+
+    public Source getSource() {
+        return mSource;
+    }
+
+    @Override
+    public Source getSuggestionSource() {
+        return mSource;
+    }
+
+    public boolean isSuggestionShortcut() {
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return mSource + "[" + getUserQuery() + "]";
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/quicksearchbox/EventLogLogger.java b/src/com/android/quicksearchbox/EventLogLogger.java
index cb86369..4dc6dd7 100644
--- a/src/com/android/quicksearchbox/EventLogLogger.java
+++ b/src/com/android/quicksearchbox/EventLogLogger.java
@@ -17,8 +17,6 @@
 package com.android.quicksearchbox;
 
 import android.content.Context;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
 import android.util.EventLog;
 
 import java.util.Collection;
@@ -38,22 +36,12 @@
 
     private final String mPackageName;
 
-    private final int mVersionCode;
-
     private final Random mRandom;
 
     public EventLogLogger(Context context, Config config) {
         mContext = context;
         mConfig = config;
-        mPackageName= mContext.getPackageName();
-        try {
-            PackageInfo pkgInfo = mContext.getPackageManager().getPackageInfo(mPackageName, 0);
-            mVersionCode = pkgInfo.versionCode;
-        } catch (PackageManager.NameNotFoundException ex) {
-            // The current package should always exist, how else could we
-            // run code from it?
-            throw new RuntimeException(ex);
-        }
+        mPackageName = mContext.getPackageName();
         mRandom = new Random();
     }
 
@@ -62,7 +50,7 @@
     }
 
     protected int getVersionCode() {
-        return mVersionCode;
+        return QsbApplication.get(getContext()).getVersionCode();
     }
 
     protected Config getConfig() {
@@ -75,7 +63,7 @@
         String startMethod = intentSource;
         String currentCorpus = getCorpusLogName(corpus);
         String enabledCorpora = getCorpusLogNames(orderedCorpora);
-        EventLogTags.writeQsbStart(mPackageName, mVersionCode, startMethod,
+        EventLogTags.writeQsbStart(mPackageName, getVersionCode(), startMethod,
                 latency, currentCorpus, enabledCorpora);
     }
 
diff --git a/src/com/android/quicksearchbox/MultiSourceCorpus.java b/src/com/android/quicksearchbox/MultiSourceCorpus.java
index a3af111..8573363 100644
--- a/src/com/android/quicksearchbox/MultiSourceCorpus.java
+++ b/src/com/android/quicksearchbox/MultiSourceCorpus.java
@@ -40,7 +40,7 @@
     private int mQueryThreshold;
     private boolean mQueryAfterZeroResults;
     private boolean mVoiceSearchEnabled;
-
+    private boolean mIsLocationAware;
 
     public MultiSourceCorpus(Context context, Config config,
             Executor executor, Source... sources) {
@@ -96,14 +96,17 @@
         return sources;
     }
 
-    private void calculateSourceProperties() {
+    private void updateSourceProperties() {
+        if (mSourcePropertiesValid) return;
         mQueryThreshold = Integer.MAX_VALUE;
         mQueryAfterZeroResults = false;
         mVoiceSearchEnabled = false;
+        mIsLocationAware = false;
         for (Source s : getSources()) {
             mQueryThreshold = Math.min(mQueryThreshold, s.getQueryThreshold());
             mQueryAfterZeroResults |= s.queryAfterZeroResults();
             mVoiceSearchEnabled |= s.voiceSearchEnabled();
+            mIsLocationAware |= s.isLocationAware();
         }
         if (mQueryThreshold == Integer.MAX_VALUE) {
             mQueryThreshold = 0;
@@ -112,26 +115,25 @@
     }
 
     public int getQueryThreshold() {
-        if (!mSourcePropertiesValid) {
-            calculateSourceProperties();
-        }
+        updateSourceProperties();
         return mQueryThreshold;
     }
 
     public boolean queryAfterZeroResults() {
-        if (!mSourcePropertiesValid) {
-            calculateSourceProperties();
-        }
+        updateSourceProperties();
         return mQueryAfterZeroResults;
     }
 
     public boolean voiceSearchEnabled() {
-        if (!mSourcePropertiesValid) {
-            calculateSourceProperties();
-        }
+        updateSourceProperties();
         return mVoiceSearchEnabled;
     }
 
+    public boolean isLocationAware() {
+        updateSourceProperties();
+        return mIsLocationAware;
+    }
+
     public CorpusResult getSuggestions(String query, int queryLimit, boolean onlyCorpus) {
         LatencyTracker latencyTracker = new LatencyTracker();
         List<Source> sources = getSourcesToQuery(query, onlyCorpus);
diff --git a/tests/src/com/android/quicksearchbox/MockLogger.java b/src/com/android/quicksearchbox/NoLogger.java
similarity index 91%
rename from tests/src/com/android/quicksearchbox/MockLogger.java
rename to src/com/android/quicksearchbox/NoLogger.java
index 38dbf23..32d6a06 100644
--- a/tests/src/com/android/quicksearchbox/MockLogger.java
+++ b/src/com/android/quicksearchbox/NoLogger.java
@@ -20,11 +20,11 @@
 import java.util.List;
 
 /**
- * Mock {@link Logger} implementation.
+ * Dummy {@link Logger} implementation.
  */
-public class MockLogger implements Logger {
+public class NoLogger implements Logger {
 
-    public MockLogger() {
+    public NoLogger() {
     }
 
     public void logStart(int latency, String intentSource, Corpus corpus,
diff --git a/src/com/android/quicksearchbox/QsbApplication.java b/src/com/android/quicksearchbox/QsbApplication.java
index f3ffc63..e95c094 100644
--- a/src/com/android/quicksearchbox/QsbApplication.java
+++ b/src/com/android/quicksearchbox/QsbApplication.java
@@ -16,6 +16,8 @@
 
 package com.android.quicksearchbox;
 
+import com.android.quicksearchbox.google.GoogleClient;
+import com.android.quicksearchbox.google.GoogleSuggestClient;
 import com.android.quicksearchbox.ui.CorpusViewFactory;
 import com.android.quicksearchbox.ui.CorpusViewInflater;
 import com.android.quicksearchbox.ui.DelayingSuggestionsAdapter;
@@ -29,7 +31,10 @@
 import com.android.quicksearchbox.util.SingleThreadNamedTaskExecutor;
 import com.google.common.util.concurrent.NamingThreadFactory;
 
-import android.app.Application;
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Process;
@@ -38,8 +43,11 @@
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadFactory;
 
-public class QsbApplication extends Application {
+public class QsbApplication {
 
+    private final Context mContext;
+
+    private int mVersionCode;
     private Handler mUiThreadHandler;
     private Config mConfig;
     private Corpora mCorpora;
@@ -51,12 +59,39 @@
     private SuggestionsProvider mSuggestionsProvider;
     private SuggestionViewFactory mSuggestionViewFactory;
     private CorpusViewFactory mCorpusViewFactory;
+    private GoogleClient mGoogleClient;
+    private VoiceSearch mVoiceSearch;
     private Logger mLogger;
 
-    @Override
-    public void onTerminate() {
-        close();
-        super.onTerminate();
+    public QsbApplication(Context context) {
+        mContext = context;
+    }
+
+    public static boolean isFroyoOrLater() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO;
+    }
+
+    public static QsbApplication get(Context context) {
+        return ((QsbApplicationWrapper) context.getApplicationContext()).getApp();
+    }
+
+    protected Context getContext() {
+        return mContext;
+    }
+
+    public int getVersionCode() {
+        if (mVersionCode == 0) {
+            try {
+                PackageManager pm = getContext().getPackageManager();
+                PackageInfo pkgInfo = pm.getPackageInfo(getContext().getPackageName(), 0);
+                mVersionCode = pkgInfo.versionCode;
+            } catch (PackageManager.NameNotFoundException ex) {
+                // The current package should always exist, how else could we
+                // run code from it?
+                throw new RuntimeException(ex);
+            }
+        }
+        return mVersionCode;
     }
 
     protected void checkThread() {
@@ -109,7 +144,7 @@
     }
 
     protected Config createConfig() {
-        return new Config(this);
+        return new Config(getContext());
     }
 
     /**
@@ -125,7 +160,7 @@
     }
 
     protected Corpora createCorpora() {
-        SearchableCorpora corpora = new SearchableCorpora(this, createSources(),
+        SearchableCorpora corpora = new SearchableCorpora(getContext(), createSources(),
                 createCorpusFactory());
         corpora.update();
         return corpora;
@@ -143,12 +178,12 @@
     }
 
     protected Sources createSources() {
-        return new SearchableSources(this);
+        return new SearchableSources(getContext());
     }
 
     protected CorpusFactory createCorpusFactory() {
         int numWebCorpusThreads = getConfig().getNumWebCorpusThreads();
-        return new SearchableCorpusFactory(this, getConfig(),
+        return new SearchableCorpusFactory(getContext(), getConfig(),
                 createExecutorFactory(numWebCorpusThreads));
     }
 
@@ -193,7 +228,7 @@
         ThreadFactory logThreadFactory = new NamingThreadFactory("ShortcutRepositoryWriter #%d",
                 new PriorityThreadFactory(Process.THREAD_PRIORITY_BACKGROUND));
         Executor logExecutor = Executors.newSingleThreadExecutor(logThreadFactory);
-        return ShortcutRepositoryImplLog.create(this, getConfig(), getCorpora(),
+        return ShortcutRepositoryImplLog.create(getContext(), getConfig(), getCorpora(),
             getShortcutRefresher(), getMainThreadHandler(), logExecutor);
     }
 
@@ -227,7 +262,6 @@
     }
 
     protected NamedTaskExecutor createSourceTaskExecutor() {
-        Config config = getConfig();
         ThreadFactory queryThreadFactory = getQueryThreadFactory();
         return new PerNameExecutor(SingleThreadNamedTaskExecutor.factory(queryThreadFactory));
     }
@@ -290,7 +324,7 @@
     }
 
     protected SuggestionViewFactory createSuggestionViewFactory() {
-        return new SuggestionViewInflater(this);
+        return new SuggestionViewInflater(getContext());
     }
 
     /**
@@ -306,7 +340,7 @@
     }
 
     protected CorpusViewFactory createCorpusViewFactory() {
-        return new CorpusViewInflater(this);
+        return new CorpusViewInflater(getContext());
     }
 
     /**
@@ -314,13 +348,43 @@
      * May only be called from the main thread.
      */
     public SuggestionsAdapter createSuggestionsAdapter() {
-        Config config = getConfig();
         SuggestionViewFactory viewFactory = getSuggestionViewFactory();
         DelayingSuggestionsAdapter adapter = new DelayingSuggestionsAdapter(viewFactory);
         return adapter;
     }
 
     /**
+     * Gets the Google client.
+     * May only be called from the main thread.
+     */
+    public GoogleClient getGoogleClient() {
+        checkThread();
+        if (mGoogleClient == null) {
+            mGoogleClient = createGoogleClient();
+        }
+        return mGoogleClient;
+    }
+
+    protected GoogleClient createGoogleClient() {
+        return new GoogleSuggestClient(getContext());
+    }
+
+    /**
+     * Gets Voice Search utilities.
+     */
+    public VoiceSearch getVoiceSearch() {
+        checkThread();
+        if (mVoiceSearch == null) {
+            mVoiceSearch = createVoiceSearch();
+        }
+        return mVoiceSearch;
+    }
+
+    protected VoiceSearch createVoiceSearch() {
+        return new VoiceSearch(getContext());
+    }
+
+    /**
      * Gets the event logger.
      * May only be called from the main thread.
      */
@@ -333,6 +397,6 @@
     }
 
     protected Logger createLogger() {
-        return new EventLogLogger(this, getConfig());
+        return new EventLogLogger(getContext(), getConfig());
     }
 }
diff --git a/src/com/android/quicksearchbox/QsbApplicationWrapper.java b/src/com/android/quicksearchbox/QsbApplicationWrapper.java
new file mode 100644
index 0000000..7329cdf
--- /dev/null
+++ b/src/com/android/quicksearchbox/QsbApplicationWrapper.java
@@ -0,0 +1,46 @@
+/*
+ * 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.Application;
+
+public class QsbApplicationWrapper extends Application {
+
+    private QsbApplication mApp;
+
+    @Override
+    public void onTerminate() {
+        synchronized (this) {
+            if (mApp != null) {
+                mApp.close();
+            }
+        }
+        super.onTerminate();
+    }
+
+    public synchronized QsbApplication getApp() {
+        if (mApp == null) {
+            mApp = createQsbApplication();
+        }
+        return mApp;
+    }
+
+    protected QsbApplication createQsbApplication() {
+        return new QsbApplication(this);
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/QueryTask.java b/src/com/android/quicksearchbox/QueryTask.java
index 2b8c8f4..1d55522 100644
--- a/src/com/android/quicksearchbox/QueryTask.java
+++ b/src/com/android/quicksearchbox/QueryTask.java
@@ -22,8 +22,6 @@
 
 import android.os.Handler;
 
-import java.util.Iterator;
-
 /**
  * A task that gets suggestions from a corpus.
  */
diff --git a/src/com/android/quicksearchbox/SearchActivity.java b/src/com/android/quicksearchbox/SearchActivity.java
index 318691b..7919b96 100644
--- a/src/com/android/quicksearchbox/SearchActivity.java
+++ b/src/com/android/quicksearchbox/SearchActivity.java
@@ -99,8 +99,6 @@
     protected ImageButton mVoiceSearchButton;
     protected ImageButton mCorpusIndicator;
 
-    private VoiceSearch mVoiceSearch;
-
     private Corpus mCorpus;
     private Bundle mAppSearchData;
     private boolean mUpdateSuggestions;
@@ -140,8 +138,6 @@
         mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn);
         mCorpusIndicator = (ImageButton) findViewById(R.id.corpus_indicator);
 
-        mVoiceSearch = new VoiceSearch(this);
-
         mQueryTextView.addTextChangedListener(new SearchTextWatcher());
         mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener());
         mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener());
@@ -295,7 +291,7 @@
     }
 
     private QsbApplication getQsbApplication() {
-        return (QsbApplication) getApplication();
+        return QsbApplication.get(this);
     }
 
     private Config getConfig() {
@@ -318,6 +314,10 @@
         return getQsbApplication().getCorpusViewFactory();
     }
 
+    private VoiceSearch getVoiceSearch() {
+        return QsbApplication.get(this).getVoiceSearch();
+    }
+
     private Logger getLogger() {
         return getQsbApplication().getLogger();
     }
@@ -423,7 +423,7 @@
     }
 
     protected void updateVoiceSearchButton(boolean queryEmpty) {
-        if (queryEmpty && mVoiceSearch.shouldShowVoiceSearch(mCorpus)) {
+        if (queryEmpty && getVoiceSearch().shouldShowVoiceSearch(mCorpus)) {
             mVoiceSearchButton.setVisibility(View.VISIBLE);
             mQueryTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
         } else {
diff --git a/src/com/android/quicksearchbox/SearchSettings.java b/src/com/android/quicksearchbox/SearchSettings.java
index 80fe835..f91964a 100644
--- a/src/com/android/quicksearchbox/SearchSettings.java
+++ b/src/com/android/quicksearchbox/SearchSettings.java
@@ -59,6 +59,7 @@
     private static final String CLEAR_SHORTCUTS_PREF = "clear_shortcuts";
     private static final String SEARCH_ENGINE_SETTINGS_PREF = "search_engine_settings";
     private static final String SEARCH_CORPORA_PREF = "search_corpora";
+    private static final String VOICE_SEARCH_CATEGORY = "voice_search_settings_category";
 
     // Prefix of per-corpus enable preference
     private static final String CORPUS_ENABLED_PREF_PREFIX = "enable_corpus_";
@@ -90,7 +91,14 @@
         corporaPreference.setIntent(getSearchableItemsIntent(this));
 
         mClearShortcutsPreference.setOnPreferenceClickListener(this);
-        mVoiceSearchHintsPreference.setOnPreferenceClickListener(this);
+
+        if (getConfig().allowVoiceSearchHints()) {
+            mVoiceSearchHintsPreference.setOnPreferenceClickListener(this);
+        } else {
+            preferenceScreen.removePreference(
+                    preferenceScreen.findPreference(VOICE_SEARCH_CATEGORY));
+            mVoiceSearchHintsPreference = null;
+        }
 
         updateClearShortcutsPreference();
         populateSearchEnginePreference();
@@ -125,12 +133,12 @@
         return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
     }
 
-    private QsbApplication getQsbApplication() {
-        return (QsbApplication) getApplication();
+    private ShortcutRepository getShortcuts() {
+        return QsbApplication.get(this).getShortcutRepository();
     }
 
-    private ShortcutRepository getShortcuts() {
-        return getQsbApplication().getShortcutRepository();
+    private Config getConfig() {
+        return QsbApplication.get(this).getConfig();
     }
 
     /**
diff --git a/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java b/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java
index b07b21f..935cda3 100644
--- a/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java
+++ b/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java
@@ -54,6 +54,12 @@
         if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
             finish();
         }
+
+        // If there is only All, or only All and one other corpus, there is no
+        // point in asking the user to select a corpus.
+        if (getCorpusRanker().getRankedCorpora().size() <= 1) {
+            selectCorpus(null);
+        }
     }
 
     @Override
@@ -106,16 +112,12 @@
         return prefs.getString(getCorpusPrefKey(appWidgetId), null);
     }
 
-    private QsbApplication getQsbApplication() {
-        return (QsbApplication) getApplication();
-    }
-
     private CorpusRanker getCorpusRanker() {
-        return getQsbApplication().getCorpusRanker();
+        return QsbApplication.get(this).getCorpusRanker();
     }
 
     private CorpusViewFactory getViewFactory() {
-        return getQsbApplication().getCorpusViewFactory();
+        return QsbApplication.get(this).getCorpusViewFactory();
     }
 
     private class SourceClickListener implements AdapterView.OnItemClickListener {
diff --git a/src/com/android/quicksearchbox/SearchWidgetProvider.java b/src/com/android/quicksearchbox/SearchWidgetProvider.java
index 9a0e62c..ca45aac 100644
--- a/src/com/android/quicksearchbox/SearchWidgetProvider.java
+++ b/src/com/android/quicksearchbox/SearchWidgetProvider.java
@@ -32,6 +32,7 @@
 import android.content.SharedPreferences;
 import android.graphics.Typeface;
 import android.net.Uri;
+import android.os.Build;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.speech.RecognizerIntent;
@@ -186,7 +187,7 @@
 
     private static Intent getVoiceSearchIntent(Context context, Corpus corpus,
             Bundle widgetAppData) {
-        VoiceSearch voiceSearch = new VoiceSearch(context);
+        VoiceSearch voiceSearch = QsbApplication.get(context).getVoiceSearch();
         if (!voiceSearch.shouldShowVoiceSearch(corpus)) return null;
         if (corpus == null) {
             return voiceSearch.createVoiceWebSearchIntent(widgetAppData);
@@ -224,13 +225,18 @@
         return spannedHint;
     }
 
+    private static boolean areVoiceSearchHintsEnabled(Context context) {
+        return getConfig(context).allowVoiceSearchHints()
+                && SearchSettings.areVoiceSearchHintsEnabled(context);
+    }
+
     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)) {
+        if (enabled && areVoiceSearchHintsEnabled(context)) {
             // Do one update immediately, and then at VOICE_SEARCH_HINT_UPDATE_INTERVAL intervals
             getHintsFromVoiceSearch(context);
             long period = VOICE_SEARCH_HINT_UPDATE_INTERVAL;
@@ -243,7 +249,7 @@
      * Requests an asynchronous update of the voice search hints.
      */
     private static void getHintsFromVoiceSearch(Context context) {
-        if (!SearchSettings.areVoiceSearchHintsEnabled(context)) return;
+        if (!areVoiceSearchHintsEnabled(context)) return;
         Intent intent = new Intent(RecognizerIntent.ACTION_GET_LANGUAGE_DETAILS);
         intent.putExtra(Recognition.EXTRA_HINT_CONTEXT, Recognition.HINT_CONTEXT_LAUNCHER);
         if (DBG) Log.d(TAG, "Broadcasting " + intent);
@@ -290,16 +296,16 @@
         return i;
     }
 
-    private static QsbApplication getQsbApplication(Context context) {
-        return (QsbApplication) context.getApplicationContext();
+    private static Config getConfig(Context context) {
+        return QsbApplication.get(context).getConfig();
     }
 
     private static Corpora getCorpora(Context context) {
-        return getQsbApplication(context).getCorpora();
+        return QsbApplication.get(context).getCorpora();
     }
 
     private static CorpusViewFactory getCorpusViewFactory(Context context) {
-        return getQsbApplication(context).getCorpusViewFactory();
+        return QsbApplication.get(context).getCorpusViewFactory();
     }
 
     private static class SearchWidgetState {
@@ -356,13 +362,19 @@
         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);
+            // Before Froyo, android.resource URI could not be used in ImageViews.
+            if (QsbApplication.isFroyoOrLater()) {
+                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);
+            // setBackgroundResource did not have @RemotableViewMethod before Froyo
+            if (QsbApplication.isFroyoOrLater()) {
+                views.setInt(R.id.search_widget_text, "setBackgroundResource",
+                        mQueryTextViewBackgroundResource);
+            }
             setOnClickActivityIntent(context, views, R.id.search_widget_text,
                     mQueryTextViewIntent);
             // Voice Search button
diff --git a/src/com/android/quicksearchbox/SearchableItemsSettings.java b/src/com/android/quicksearchbox/SearchableItemsSettings.java
index a279593..2547f8b 100644
--- a/src/com/android/quicksearchbox/SearchableItemsSettings.java
+++ b/src/com/android/quicksearchbox/SearchableItemsSettings.java
@@ -52,12 +52,8 @@
         populateSourcePreference();
     }
 
-    private QsbApplication getQsbApplication() {
-        return (QsbApplication) getApplication();
-    }
-
     private Corpora getCorpora() {
-        return getQsbApplication().getCorpora();
+        return QsbApplication.get(this).getCorpora();
     }
 
     /**
diff --git a/src/com/android/quicksearchbox/SearchableSource.java b/src/com/android/quicksearchbox/SearchableSource.java
index 2e71c49..5ec1282 100644
--- a/src/com/android/quicksearchbox/SearchableSource.java
+++ b/src/com/android/quicksearchbox/SearchableSource.java
@@ -42,9 +42,8 @@
 
 /**
  * Represents a single suggestion source, e.g. Contacts.
- *
  */
-public class SearchableSource implements Source {
+public class SearchableSource extends AbstractSource {
 
     private static final boolean DBG = false;
     private static final String TAG = "QSB.SearchableSource";
@@ -53,8 +52,6 @@
     // The extra key used in an intent to the speech recognizer for in-app voice search.
     private static final String EXTRA_CALLING_PACKAGE = "calling_package";
 
-    private final Context mContext;
-
     private final SearchableInfo mSearchable;
 
     private final String mName;
@@ -69,12 +66,10 @@
     // Cached icon for the activity
     private Drawable.ConstantState mSourceIcon = null;
 
-    private IconLoader mIconLoader;
-
     public SearchableSource(Context context, SearchableInfo searchable)
             throws NameNotFoundException {
+        super(context);
         ComponentName componentName = searchable.getSearchActivity();
-        mContext = context;
         mSearchable = searchable;
         mName = componentName.flattenToShortString();
         PackageManager pm = context.getPackageManager();
@@ -83,10 +78,6 @@
         mVersionCode = pkgInfo.versionCode;
     }
 
-    protected Context getContext() {
-        return mContext;
-    }
-
     protected SearchableInfo getSearchableInfo() {
         return mSearchable;
     }
@@ -122,7 +113,7 @@
      * TODO: Shouldn't this be a PackageManager / Context / ContentResolver method?
      */
     private boolean canRead(Uri uri) {
-        ProviderInfo provider = mContext.getPackageManager().resolveContentProvider(
+        ProviderInfo provider = getContext().getPackageManager().resolveContentProvider(
                 uri.getAuthority(), 0);
         if (provider == null) {
             Log.w(TAG, getName() + " has bad suggestion authority " + uri.getAuthority());
@@ -135,7 +126,7 @@
         }
         int pid = android.os.Process.myPid();
         int uid = android.os.Process.myUid();
-        if (mContext.checkPermission(readPermission, pid, uid)
+        if (getContext().checkPermission(readPermission, pid, uid)
                 == PackageManager.PERMISSION_GRANTED) {
             // We have permission to read everything in the content provider
             return true;
@@ -151,7 +142,7 @@
             String pathReadPermission = perm.getReadPermission();
             if (pathReadPermission != null
                     && perm.match(path)
-                    && mContext.checkPermission(pathReadPermission, pid, uid)
+                    && getContext().checkPermission(pathReadPermission, pid, uid)
                             == PackageManager.PERMISSION_GRANTED) {
                 // We have the path permission
                 return true;
@@ -161,19 +152,6 @@
         return false;
     }
 
-    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 getIntentComponent() {
         return mSearchable.getSearchActivity();
     }
@@ -186,18 +164,22 @@
         return mName;
     }
 
-    public Drawable getIcon(String drawableId) {
-        return getIconLoader().getIcon(drawableId);
-    }
-
-    public Uri getIconUri(String drawableId) {
-        return getIconLoader().getIconUri(drawableId);
+    @Override
+    protected String getIconPackage() {
+        // Get icons from the package containing the suggestion provider, if any
+        String iconPackage = mSearchable.getSuggestPackage();
+        if (iconPackage != null) {
+            return iconPackage;
+        } else {
+            // Fall back to the package containing the searchable activity
+            return mSearchable.getSearchActivity().getPackageName();
+        }
     }
 
     public CharSequence getLabel() {
         if (mLabel == null) {
             // Load label lazily
-            mLabel = mActivityInfo.loadLabel(mContext.getPackageManager());
+            mLabel = mActivityInfo.loadLabel(getContext().getPackageManager());
         }
         return mLabel;
     }
@@ -218,7 +200,7 @@
         if (mSourceIcon == null) {
             // Load icon lazily
             int iconRes = getSourceIconResource();
-            PackageManager pm = mContext.getPackageManager();
+            PackageManager pm = getContext().getPackageManager();
             Drawable icon = pm.getDrawable(mActivityInfo.packageName, iconRes,
                     mActivityInfo.applicationInfo);
             // Can't share Drawable instances, save constant state instead.
@@ -243,33 +225,13 @@
         return mSearchable.getVoiceSearchEnabled();
     }
 
-    public Intent createSearchIntent(String query, Bundle appData) {
-        return createSourceSearchIntent(getIntentComponent(), query, appData);
-    }
-
-    public static Intent createSourceSearchIntent(ComponentName activity, String query,
-            Bundle appData) {
-        if (activity == null) {
-            Log.w(TAG, "Tried to create search intent with no target activity");
-            return null;
-        }
-        Intent intent = new Intent(Intent.ACTION_SEARCH);
-        intent.setComponent(activity);
-        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);
-        }
-        return intent;
+    public boolean isLocationAware() {
+        return false;
     }
 
     public Intent createVoiceSearchIntent(Bundle appData) {
         if (mSearchable.getVoiceSearchLaunchWebSearch()) {
-            return new VoiceSearch(mContext).createVoiceWebSearchIntent(appData);
+            return createVoiceWebSearchIntent(appData);
         } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
             return createVoiceAppSearchIntent(appData);
         }
@@ -338,24 +300,24 @@
 
     public SourceResult getSuggestions(String query, int queryLimit, boolean onlySource) {
         try {
-            Cursor cursor = getSuggestions(mContext, mSearchable, query, queryLimit);
+            Cursor cursor = getSuggestions(getContext(), mSearchable, query, queryLimit);
             if (DBG) Log.d(TAG, toString() + "[" + query + "] returned.");
-            return new CursorBackedSourceResult(query, cursor);
+            return new CursorBackedSourceResult(this, query, cursor);
         } catch (RuntimeException ex) {
             Log.e(TAG, toString() + "[" + query + "] failed", ex);
-            return new CursorBackedSourceResult(query);
+            return new CursorBackedSourceResult(this, query);
         }
     }
 
     public SuggestionCursor refreshShortcut(String shortcutId, String extraData) {
         Cursor cursor = null;
         try {
-            cursor = getValidationCursor(mContext, mSearchable, shortcutId, extraData);
+            cursor = getValidationCursor(getContext(), mSearchable, shortcutId, extraData);
             if (DBG) Log.d(TAG, toString() + "[" + shortcutId + "] returned.");
             if (cursor != null && cursor.getCount() > 0) {
                 cursor.moveToFirst();
             }
-            return new CursorBackedSourceResult(null, cursor);
+            return new CursorBackedSourceResult(this, null, cursor);
         } catch (RuntimeException ex) {
             Log.e(TAG, toString() + "[" + shortcutId + "] failed", ex);
             if (cursor != null) {
@@ -366,37 +328,6 @@
         }
     }
 
-    private class CursorBackedSourceResult extends CursorBackedSuggestionCursor
-            implements SourceResult {
-
-        public CursorBackedSourceResult(String userQuery) {
-            this(userQuery, null);
-        }
-
-        public CursorBackedSourceResult(String userQuery, Cursor cursor) {
-            super(userQuery, cursor);
-        }
-
-        public Source getSource() {
-            return SearchableSource.this;
-        }
-
-        @Override
-        public Source getSuggestionSource() {
-            return SearchableSource.this;
-        }
-
-        public boolean isSuggestionShortcut() {
-            return false;
-        }
-
-        @Override
-        public String toString() {
-            return SearchableSource.this + "[" + getUserQuery() + "]";
-        }
-
-    }
-
     /**
      * This is a copy of {@link SearchManager#getSuggestions(SearchableInfo, String)}.
      */
@@ -484,25 +415,6 @@
         return mSearchable.queryAfterZeroResults();
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (o != null && o.getClass().equals(this.getClass())) {
-            SearchableSource s = (SearchableSource) o;
-            return s.mName.equals(mName);
-        }
-        return false;
-    }
-
-    @Override
-    public int hashCode() {
-        return mName.hashCode();
-    }
-
-    @Override
-    public String toString() {
-        return "SearchableSource{component=" + getName() + "}";
-    }
-
     public String getDefaultIntentAction() {
         return mSearchable.getSuggestIntentAction();
     }
@@ -513,7 +425,7 @@
 
     private CharSequence getText(int id) {
         if (id == 0) return null;
-        return mContext.getPackageManager().getText(mActivityInfo.packageName, id,
+        return getContext().getPackageManager().getText(mActivityInfo.packageName, id,
                 mActivityInfo.applicationInfo);
     }
 
diff --git a/src/com/android/quicksearchbox/SearchableSources.java b/src/com/android/quicksearchbox/SearchableSources.java
index 649df1d..edb66e8 100644
--- a/src/com/android/quicksearchbox/SearchableSources.java
+++ b/src/com/android/quicksearchbox/SearchableSources.java
@@ -16,12 +16,12 @@
 
 package com.android.quicksearchbox;
 
+import com.android.quicksearchbox.google.GoogleSource;
+
 import android.app.SearchManager;
 import android.app.SearchableInfo;
 import android.content.ComponentName;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.util.Log;
 
@@ -56,6 +56,14 @@
         mSearchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
     }
 
+    protected Context getContext() {
+        return mContext;
+    }
+
+    protected SearchManager getSearchManager() {
+        return mSearchManager;
+    }
+
     public Collection<Source> getSources() {
         return mSources.values();
     }
@@ -100,24 +108,8 @@
         mSources.put(source.getName(), source);
     }
 
-    private Source createWebSearchSource() {
-        ComponentName name = getWebSearchComponent();
-        SearchableInfo webSearchable = mSearchManager.getSearchableInfo(name);
-        if (webSearchable == null) {
-            Log.e(TAG, "Web search source " + name + " is not searchable.");
-            return null;
-        }
-        return createSearchableSource(webSearchable);
-    }
-
-    private ComponentName getWebSearchComponent() {
-        // Looks for an activity in the current package that handles ACTION_WEB_SEARCH.
-        // This indirect method is used to allow easy replacement of the web
-        // search activity when extending this package.
-        Intent webSearchIntent = new Intent(Intent.ACTION_WEB_SEARCH);
-        webSearchIntent.setPackage(mContext.getPackageName());
-        PackageManager pm = mContext.getPackageManager();
-        return webSearchIntent.resolveActivity(pm);
+    protected Source createWebSearchSource() {
+        return new GoogleSource(getContext());
     }
 
     private SearchableSource createSearchableSource(SearchableInfo searchable) {
diff --git a/src/com/android/quicksearchbox/ShortcutRepositoryImplLog.java b/src/com/android/quicksearchbox/ShortcutRepositoryImplLog.java
index 1331afb..08b8503 100644
--- a/src/com/android/quicksearchbox/ShortcutRepositoryImplLog.java
+++ b/src/com/android/quicksearchbox/ShortcutRepositoryImplLog.java
@@ -293,6 +293,7 @@
         if (refreshed == null || refreshed.getCount() == 0) {
             shortcut = null;
         } else {
+            refreshed.moveTo(0);
             shortcut = makeShortcutRow(refreshed);
         }
 
diff --git a/src/com/android/quicksearchbox/ShortcutsProvider.java b/src/com/android/quicksearchbox/ShortcutsProvider.java
index a8bed1c..66b6730 100644
--- a/src/com/android/quicksearchbox/ShortcutsProvider.java
+++ b/src/com/android/quicksearchbox/ShortcutsProvider.java
@@ -182,7 +182,7 @@
     }
 
     private QsbApplication getQsbApplication() {
-        return (QsbApplication) getContext().getApplicationContext();
+        return QsbApplication.get(getContext());
     }
 
     private ShortcutRepository getShortcutRepository() {
diff --git a/src/com/android/quicksearchbox/SingleSourceCorpus.java b/src/com/android/quicksearchbox/SingleSourceCorpus.java
index 79f215d..8f3bf34 100644
--- a/src/com/android/quicksearchbox/SingleSourceCorpus.java
+++ b/src/com/android/quicksearchbox/SingleSourceCorpus.java
@@ -97,6 +97,10 @@
         return false;
     }
 
+    public boolean isLocationAware() {
+        return mSource.isLocationAware();
+    }
+
     public Collection<Source> getSources() {
         return Collections.singletonList(mSource);
     }
diff --git a/src/com/android/quicksearchbox/Source.java b/src/com/android/quicksearchbox/Source.java
index 2d0a434..dafd901 100644
--- a/src/com/android/quicksearchbox/Source.java
+++ b/src/com/android/quicksearchbox/Source.java
@@ -101,6 +101,8 @@
 
     boolean voiceSearchEnabled();
 
+    boolean isLocationAware();
+
     Intent createSearchIntent(String query, Bundle appData);
 
     Intent createVoiceSearchIntent(Bundle appData);
diff --git a/src/com/android/quicksearchbox/VoiceSearch.java b/src/com/android/quicksearchbox/VoiceSearch.java
index acc5860..2821e34 100644
--- a/src/com/android/quicksearchbox/VoiceSearch.java
+++ b/src/com/android/quicksearchbox/VoiceSearch.java
@@ -34,6 +34,10 @@
         mContext = context;
     }
 
+    protected Context getContext() {
+        return mContext;
+    }
+
     public boolean shouldShowVoiceSearch(Corpus corpus) {
         if (corpus != null && !corpus.voiceSearchEnabled()) {
             return false;
diff --git a/src/com/android/quicksearchbox/WebCorpus.java b/src/com/android/quicksearchbox/WebCorpus.java
index e6442ac..b55536e 100644
--- a/src/com/android/quicksearchbox/WebCorpus.java
+++ b/src/com/android/quicksearchbox/WebCorpus.java
@@ -101,7 +101,11 @@
     }
 
     public Intent createVoiceSearchIntent(Bundle appData) {
-        return new VoiceSearch(getContext()).createVoiceWebSearchIntent(appData);
+        if (mWebSearchSource != null){
+            return mWebSearchSource.createVoiceSearchIntent(appData);
+        } else {
+            return null;
+        }
     }
 
     private int getCorpusIconResource() {
diff --git a/src/com/android/quicksearchbox/google/GoogleClient.java b/src/com/android/quicksearchbox/google/GoogleClient.java
new file mode 100644
index 0000000..5d578ae
--- /dev/null
+++ b/src/com/android/quicksearchbox/google/GoogleClient.java
@@ -0,0 +1,32 @@
+/*
+ * 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.google;
+
+import android.content.ComponentName;
+import android.database.Cursor;
+
+/**
+ * Interface for Google suggestion clients.
+ */
+public interface GoogleClient {
+
+    public ComponentName getIntentComponent();
+
+    public Cursor query(String query);
+
+    public Cursor refreshShortcut(String shortcutId, String oldExtraData);
+
+}
diff --git a/src/com/android/quicksearchbox/google/GoogleSettings.java b/src/com/android/quicksearchbox/google/GoogleSettings.java
index a9cc102..86b4c48 100644
--- a/src/com/android/quicksearchbox/google/GoogleSettings.java
+++ b/src/com/android/quicksearchbox/google/GoogleSettings.java
@@ -39,7 +39,6 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         addPreferencesFromResource(R.xml.google_preferences);
-        PreferenceScreen preferenceScreen = getPreferenceScreen();
         mShowWebSuggestionsPreference = (CheckBoxPreference)
                 findPreference(SHOW_WEB_SUGGESTIONS_PREF);
         mShowWebSuggestionsPreference.setOnPreferenceClickListener(this);
diff --git a/src/com/android/quicksearchbox/google/GoogleSource.java b/src/com/android/quicksearchbox/google/GoogleSource.java
new file mode 100644
index 0000000..ab83174
--- /dev/null
+++ b/src/com/android/quicksearchbox/google/GoogleSource.java
@@ -0,0 +1,136 @@
+/*
+ * 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.google;
+
+import com.android.quicksearchbox.AbstractSource;
+import com.android.quicksearchbox.CursorBackedSourceResult;
+import com.android.quicksearchbox.IconLoader;
+import com.android.quicksearchbox.QsbApplication;
+import com.android.quicksearchbox.R;
+import com.android.quicksearchbox.SourceResult;
+import com.android.quicksearchbox.SuggestionCursor;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+
+/**
+ * Special source implementation for Google suggestions.
+ */
+public class GoogleSource extends AbstractSource {
+
+    private static final String GOOGLE_SOURCE_NAME = "google";
+
+    private final GoogleClient mClient;
+
+    public GoogleSource(Context context) {
+        super(context);
+        mClient = QsbApplication.get(context).getGoogleClient();
+    }
+
+    public boolean canRead() {
+        return true;
+    }
+
+    public Intent createVoiceSearchIntent(Bundle appData) {
+        return createVoiceWebSearchIntent(appData);
+    }
+
+    public String getDefaultIntentAction() {
+        return Intent.ACTION_WEB_SEARCH;
+    }
+
+    public String getDefaultIntentData() {
+        return null;
+    }
+
+    public CharSequence getHint() {
+        return getContext().getString(R.string.google_search_hint);
+    }
+
+    @Override
+    protected String getIconPackage() {
+        return getContext().getPackageName();
+    }
+
+    public ComponentName getIntentComponent() {
+        return mClient.getIntentComponent();
+    }
+
+    public CharSequence getLabel() {
+        return getContext().getString(R.string.google_search_label);
+    }
+
+    public String getName() {
+        return GOOGLE_SOURCE_NAME;
+    }
+
+    public int getQueryThreshold() {
+        return 0;
+    }
+
+    public CharSequence getSettingsDescription() {
+        return getContext().getString(R.string.google_search_description);
+    }
+
+    public Drawable getSourceIcon() {
+        return getContext().getResources().getDrawable(getSourceIconResource());
+    }
+
+    public Uri getSourceIconUri() {
+        return Uri.parse("android.resource://" + getContext().getPackageName()
+                + "/" +  getSourceIconResource());
+    }
+
+    private int getSourceIconResource() {
+        return R.drawable.google_icon;
+    }
+
+    public SourceResult getSuggestions(String query, int queryLimit, boolean onlySource) {
+        Cursor cursor = mClient.query(query);
+        return new CursorBackedSourceResult(this, query, cursor);
+    }
+
+    public int getVersionCode() {
+        return QsbApplication.get(getContext()).getVersionCode();
+    }
+
+    public boolean queryAfterZeroResults() {
+        return true;
+    }
+
+    public SuggestionCursor refreshShortcut(String shortcutId, String extraData) {
+        Cursor cursor = mClient.refreshShortcut(shortcutId, extraData);
+        return new CursorBackedSourceResult(this, null, cursor);
+    }
+
+    public boolean voiceSearchEnabled() {
+        return true;
+    }
+
+    public boolean isWebSuggestionSource() {
+        return true;
+    }
+
+    public boolean isLocationAware() {
+        return true;
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/google/GoogleSuggestClient.java b/src/com/android/quicksearchbox/google/GoogleSuggestClient.java
new file mode 100644
index 0000000..13b8e93
--- /dev/null
+++ b/src/com/android/quicksearchbox/google/GoogleSuggestClient.java
@@ -0,0 +1,279 @@
+/*
+ * 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.google;
+
+import com.android.quicksearchbox.R;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.params.HttpParams;
+import org.apache.http.params.HttpProtocolParams;
+import org.apache.http.util.EntityUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import android.app.SearchManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.util.Locale;
+
+/**
+ * Use network-based Google Suggests to provide search suggestions.
+ */
+public class GoogleSuggestClient implements GoogleClient {
+
+    private static final boolean DBG = false;
+    private static final String LOG_TAG = "GoogleSearch";
+
+    private static final String USER_AGENT = "Android/1.0";
+    private String mSuggestUri;
+    private static final int HTTP_TIMEOUT_MS = 1000;
+
+    // TODO: this should be defined somewhere
+    private static final String HTTP_TIMEOUT = "http.connection-manager.timeout";
+
+    // Indexes into COLUMNS
+    private static final int COL_ID = 0;
+    private static final int COL_TEXT_1 = 1;
+    private static final int COL_TEXT_2 = 2;
+    private static final int COL_ICON_1 = 3;
+    private static final int COL_ICON_2 = 4;
+    private static final int COL_QUERY = 5;
+
+    /* The suggestion columns used */
+    private static final String[] COLUMNS = new String[] {
+        "_id",
+        SearchManager.SUGGEST_COLUMN_TEXT_1,
+        SearchManager.SUGGEST_COLUMN_TEXT_2,
+        SearchManager.SUGGEST_COLUMN_ICON_1,
+        SearchManager.SUGGEST_COLUMN_ICON_2,
+        SearchManager.SUGGEST_COLUMN_QUERY
+    };
+
+    private Context mContext;
+    private HttpClient mHttpClient;
+
+    public GoogleSuggestClient(Context context) {
+        mContext = context;
+        mHttpClient = new DefaultHttpClient();
+        HttpParams params = mHttpClient.getParams();
+        HttpProtocolParams.setUserAgent(params, USER_AGENT);
+        params.setLongParameter(HTTP_TIMEOUT, HTTP_TIMEOUT_MS);
+
+        // NOTE:  Do not look up the resource here;  Localization changes may not have completed
+        // yet (e.g. we may still be reading the SIM card).
+        mSuggestUri = null;
+    }
+
+    protected Context getContext() {
+        return mContext;
+    }
+
+    public ComponentName getIntentComponent() {
+        return new ComponentName(getContext(), GoogleSearch.class);
+    }
+
+    /**
+     * Queries for a given search term and returns a cursor containing
+     * suggestions ordered by best match.
+     */
+    public Cursor query(String query) {
+        if (TextUtils.isEmpty(query)) {
+            return null;
+        }
+        if (!isNetworkConnected()) {
+            Log.i(LOG_TAG, "Not connected to network.");
+            return null;
+        }
+        try {
+            query = URLEncoder.encode(query, "UTF-8");
+            // NOTE:  This code uses resources to optionally select the search Uri, based on the
+            // MCC value from the SIM.  iThe default string will most likely be fine.  It is
+            // paramerterized to accept info from the Locale, the language code is the first
+            // parameter (%1$s) and the country code is the second (%2$s).  This code *must*
+            // function in the same way as a similar lookup in
+            // com.android.browser.BrowserActivity#onCreate().  If you change
+            // either of these functions, change them both.  (The same is true for the underlying
+            // resource strings, which are stored in mcc-specific xml files.)
+            if (mSuggestUri == null) {
+                Locale l = Locale.getDefault();
+                String language = l.getLanguage();
+                String country = l.getCountry().toLowerCase();
+                // Chinese and Portuguese have two langauge variants.
+                if ("zh".equals(language)) {
+                    if ("cn".equals(country)) {
+                        language = "zh-CN";
+                    } else if ("tw".equals(country)) {
+                        language = "zh-TW";
+                    }
+                } else if ("pt".equals(language)) {
+                    if ("br".equals(country)) {
+                        language = "pt-BR";
+                    } else if ("pt".equals(country)) {
+                        language = "pt-PT";
+                    }
+                }
+                mSuggestUri = getContext().getResources().getString(R.string.google_suggest_base,
+                                                                    language,
+                                                                    country)
+                        + "json=true&q=";
+            }
+
+            String suggestUri = mSuggestUri + query;
+            if (DBG) Log.d(LOG_TAG, "Sending request: " + suggestUri);
+            HttpGet method = new HttpGet(suggestUri);
+            HttpResponse response = mHttpClient.execute(method);
+            if (response.getStatusLine().getStatusCode() == 200) {
+
+                /* Goto http://www.google.com/complete/search?json=true&q=foo
+                 * to see what the data format looks like. It's basically a json
+                 * array containing 4 other arrays. We only care about the middle
+                 * 2 which contain the suggestions and their popularity.
+                 */
+                JSONArray results = new JSONArray(EntityUtils.toString(response.getEntity()));
+                JSONArray suggestions = results.getJSONArray(1);
+                JSONArray popularity = results.getJSONArray(2);
+                if (DBG) Log.d(LOG_TAG, "Got " + suggestions.length() + " results");
+                return new SuggestionsCursor(suggestions, popularity);
+            } else {
+                if (DBG) Log.d(LOG_TAG, "Request failed " + response.getStatusLine());
+            }
+        } catch (UnsupportedEncodingException e) {
+            Log.w(LOG_TAG, "Error", e);
+        } catch (IOException e) {
+            Log.w(LOG_TAG, "Error", e);
+        } catch (JSONException e) {
+            Log.w(LOG_TAG, "Error", e);
+        }
+        return null;
+    }
+
+    public Cursor refreshShortcut(String shortcutId, String oldExtraData) {
+        return null;
+    }
+
+    private boolean isNetworkConnected() {
+        NetworkInfo networkInfo = getActiveNetworkInfo();
+        return networkInfo != null && networkInfo.isConnected();
+    }
+
+    private NetworkInfo getActiveNetworkInfo() {
+        ConnectivityManager connectivity =
+                (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (connectivity == null) {
+            return null;
+        }
+        return connectivity.getActiveNetworkInfo();
+    }
+
+    private static class SuggestionsCursor extends AbstractCursor {
+
+        /* Contains the actual suggestions */
+        final JSONArray mSuggestions;
+
+        /* This contains the popularity of each suggestion
+         * i.e. 165,000 results. It's not related to sorting.
+         */
+        final JSONArray mPopularity;
+        public SuggestionsCursor(JSONArray suggestions, JSONArray popularity) {
+            mSuggestions = suggestions;
+            mPopularity = popularity;
+        }
+
+        @Override
+        public int getCount() {
+            return mSuggestions.length();
+        }
+
+        @Override
+        public String[] getColumnNames() {
+            return COLUMNS;
+        }
+
+        @Override
+        public String getString(int column) {
+            if (mPos == -1) return null;
+            try {
+                switch (column) {
+                    case COL_ID:
+                        return String.valueOf(mPos);
+                    case COL_TEXT_1:
+                    case COL_QUERY:
+                        return mSuggestions.getString(mPos);
+                    case COL_TEXT_2:
+                        return mPopularity.getString(mPos);
+                    case COL_ICON_1:
+                        return String.valueOf(R.drawable.magnifying_glass);
+                    case COL_ICON_2:
+                        return null;
+                    default:
+                        Log.w(LOG_TAG, "Bad column: " + column);
+                        return null;
+                }
+            } catch (JSONException e) {
+                Log.w(LOG_TAG, "Error parsing response: " + e);
+                return null;
+            }
+
+        }
+
+        @Override
+        public double getDouble(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public float getFloat(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int getInt(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public long getLong(int column) {
+            if (column == COL_ID) {
+                return mPos;        // use row# as the _Id
+            }
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public short getShort(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isNull(int column) {
+            throw new UnsupportedOperationException();
+        }
+    }
+}
diff --git a/src/com/android/quicksearchbox/google/GoogleSuggestionProvider.java b/src/com/android/quicksearchbox/google/GoogleSuggestionProvider.java
index b461a98..f5c7e5a 100644
--- a/src/com/android/quicksearchbox/google/GoogleSuggestionProvider.java
+++ b/src/com/android/quicksearchbox/google/GoogleSuggestionProvider.java
@@ -16,80 +16,35 @@
 
 package com.android.quicksearchbox.google;
 
-import com.android.quicksearchbox.R;
-
-import org.apache.http.HttpResponse;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.params.HttpParams;
-import org.apache.http.util.EntityUtils;
-import org.json.JSONArray;
-import org.json.JSONException;
+import com.android.quicksearchbox.QsbApplication;
+import com.android.quicksearchbox.google.GoogleClient;
 
 import android.app.SearchManager;
 import android.content.ContentProvider;
 import android.content.ContentValues;
 import android.content.Context;
-import android.database.AbstractCursor;
+import android.content.UriMatcher;
 import android.database.Cursor;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
 import android.net.Uri;
-import android.net.http.AndroidHttpClient;
-import android.text.TextUtils;
-import android.util.Log;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.util.Locale;
 
 /**
- * Use network-based Google Suggests to provide search suggestions.
- *
- * Future:  Merge live suggestions with saved recent queries
+ * A suggestion provider which provides content from Genie, a service that offers
+ * a superset of the content provided by Google Suggest.
  */
 public class GoogleSuggestionProvider extends ContentProvider {
 
-    private static final boolean DBG = false;
-    private static final String LOG_TAG = "GoogleSearch";
+    // UriMatcher constants
+    private static final int SEARCH_SUGGEST = 0;
+    private static final int SEARCH_SHORTCUT = 1;
 
-    private static final String USER_AGENT = "Android/1.0";
-    private String mSuggestUri;
-    private static final int HTTP_TIMEOUT_MS = 1000;
+    private UriMatcher mUriMatcher;
 
-    // TODO: this should be defined somewhere
-    private static final String HTTP_TIMEOUT = "http.connection-manager.timeout";
-
-    // Indexes into COLUMNS
-    private static final int COL_ID = 0;
-    private static final int COL_TEXT_1 = 1;
-    private static final int COL_TEXT_2 = 2;
-    private static final int COL_ICON_1 = 3;
-    private static final int COL_ICON_2 = 4;
-    private static final int COL_QUERY = 5;
-
-    /* The suggestion columns used */
-    private static final String[] COLUMNS = new String[] {
-        "_id",
-        SearchManager.SUGGEST_COLUMN_TEXT_1,
-        SearchManager.SUGGEST_COLUMN_TEXT_2,
-        SearchManager.SUGGEST_COLUMN_ICON_1,
-        SearchManager.SUGGEST_COLUMN_ICON_2,
-        SearchManager.SUGGEST_COLUMN_QUERY
-    };
-
-    private HttpClient mHttpClient;
+    private GoogleClient mClient;
 
     @Override
     public boolean onCreate() {
-        mHttpClient = AndroidHttpClient.newInstance(USER_AGENT, getContext());
-        HttpParams params = mHttpClient.getParams();
-        params.setLongParameter(HTTP_TIMEOUT, HTTP_TIMEOUT_MS);
-
-        // NOTE:  Do not look up the resource here;  Localization changes may not have completed
-        // yet (e.g. we may still be reading the SIM card).
-        mSuggestUri = null;
+        mClient = QsbApplication.get(getContext()).getGoogleClient();
+        mUriMatcher = buildUriMatcher(getContext());
         return true;
     }
 
@@ -102,82 +57,23 @@
         return SearchManager.SUGGEST_MIME_TYPE;
     }
 
-    /**
-     * Queries for a given search term and returns a cursor containing
-     * suggestions ordered by best match.
-     */
     @Override
     public Cursor query(Uri uri, String[] projection, String selection,
             String[] selectionArgs, String sortOrder) {
-        String query = getQuery(uri);
-        if (TextUtils.isEmpty(query)) {
-            return null;
-        }
-        if (!isNetworkConnected()) {
-            Log.i(LOG_TAG, "Not connected to network.");
-            return null;
-        }
-        try {
-            query = URLEncoder.encode(query, "UTF-8");
-            // NOTE:  This code uses resources to optionally select the search Uri, based on the
-            // MCC value from the SIM.  iThe default string will most likely be fine.  It is
-            // paramerterized to accept info from the Locale, the language code is the first
-            // parameter (%1$s) and the country code is the second (%2$s).  This code *must*
-            // function in the same way as a similar lookup in
-            // com.android.browser.BrowserActivity#onCreate().  If you change
-            // either of these functions, change them both.  (The same is true for the underlying
-            // resource strings, which are stored in mcc-specific xml files.)
-            if (mSuggestUri == null) {
-                Locale l = Locale.getDefault();
-                String language = l.getLanguage();
-                String country = l.getCountry().toLowerCase();
-                // Chinese and Portuguese have two langauge variants.
-                if ("zh".equals(language)) {
-                    if ("cn".equals(country)) {
-                        language = "zh-CN";
-                    } else if ("tw".equals(country)) {
-                        language = "zh-TW";
-                    }
-                } else if ("pt".equals(language)) {
-                    if ("br".equals(country)) {
-                        language = "pt-BR";
-                    } else if ("pt".equals(country)) {
-                        language = "pt-PT";
-                    }
-                }
-                mSuggestUri = getContext().getResources().getString(R.string.google_suggest_base,
-                                                                    language,
-                                                                    country)
-                        + "json=true&q=";
-            }
 
-            String suggestUri = mSuggestUri + query;
-            if (DBG) Log.d(LOG_TAG, "Sending request: " + suggestUri);
-            HttpGet method = new HttpGet(suggestUri);
-            HttpResponse response = mHttpClient.execute(method);
-            if (response.getStatusLine().getStatusCode() == 200) {
+        int match = mUriMatcher.match(uri);
 
-                /* Goto http://www.google.com/complete/search?json=true&q=foo
-                 * to see what the data format looks like. It's basically a json
-                 * array containing 4 other arrays. We only care about the middle
-                 * 2 which contain the suggestions and their popularity.
-                 */
-                JSONArray results = new JSONArray(EntityUtils.toString(response.getEntity()));
-                JSONArray suggestions = results.getJSONArray(1);
-                JSONArray popularity = results.getJSONArray(2);
-                if (DBG) Log.d(LOG_TAG, "Got " + suggestions.length() + " results");
-                return new SuggestionsCursor(suggestions, popularity);
-            } else {
-                if (DBG) Log.d(LOG_TAG, "Request failed " + response.getStatusLine());
-            }
-        } catch (UnsupportedEncodingException e) {
-            Log.w(LOG_TAG, "Error", e);
-        } catch (IOException e) {
-            Log.w(LOG_TAG, "Error", e);
-        } catch (JSONException e) {
-            Log.w(LOG_TAG, "Error", e);
+        if (match == SEARCH_SUGGEST) {
+            String query = getQuery(uri);
+            return mClient.query(query);
+        } else if (match == SEARCH_SHORTCUT) {
+            String query = getQuery(uri);
+            String extraData =
+                uri.getQueryParameter(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
+            return mClient.refreshShortcut(query, extraData);
+        } else {
+            throw new IllegalArgumentException("Unknown URI " + uri);
         }
-        return null;
     }
 
     /**
@@ -191,105 +87,6 @@
         }
     }
 
-    private boolean isNetworkConnected() {
-        NetworkInfo networkInfo = getActiveNetworkInfo();
-        return networkInfo != null && networkInfo.isConnected();
-    }
-
-    private NetworkInfo getActiveNetworkInfo() {
-        ConnectivityManager connectivity =
-                (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
-        if (connectivity == null) {
-            return null;
-        }
-        return connectivity.getActiveNetworkInfo();
-    }
-
-    private static class SuggestionsCursor extends AbstractCursor {
-
-        /* Contains the actual suggestions */
-        final JSONArray mSuggestions;
-
-        /* This contains the popularity of each suggestion
-         * i.e. 165,000 results. It's not related to sorting.
-         */
-        final JSONArray mPopularity;
-        public SuggestionsCursor(JSONArray suggestions, JSONArray popularity) {
-            mSuggestions = suggestions;
-            mPopularity = popularity;
-        }
-
-        @Override
-        public int getCount() {
-            return mSuggestions.length();
-        }
-
-        @Override
-        public String[] getColumnNames() {
-            return COLUMNS;
-        }
-
-        @Override
-        public String getString(int column) {
-            if (mPos == -1) return null;
-            try {
-                switch (column) {
-                    case COL_ID:
-                        return String.valueOf(mPos);
-                    case COL_TEXT_1:
-                    case COL_QUERY:
-                        return mSuggestions.getString(mPos);
-                    case COL_TEXT_2:
-                        return mPopularity.getString(mPos);
-                    case COL_ICON_1:
-                        return String.valueOf(R.drawable.magnifying_glass);
-                    case COL_ICON_2:
-                        return null;
-                    default:
-                        Log.w(LOG_TAG, "Bad column: " + column);
-                        return null;
-                }
-            } catch (JSONException e) {
-                Log.w(LOG_TAG, "Error parsing response: " + e);
-                return null;
-            }
-
-        }
-
-        @Override
-        public double getDouble(int column) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public float getFloat(int column) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public int getInt(int column) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public long getLong(int column) {
-            if (column == COL_ID) {
-                return mPos;        // use row# as the _Id
-            }
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public short getShort(int column) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public boolean isNull(int column) {
-            throw new UnsupportedOperationException();
-        }
-    }
-
     @Override
     public Uri insert(Uri uri, ContentValues values) {
         throw new UnsupportedOperationException();
@@ -305,4 +102,23 @@
     public int delete(Uri uri, String selection, String[] selectionArgs) {
         throw new UnsupportedOperationException();
     }
+
+    private UriMatcher buildUriMatcher(Context context) {
+        String authority = getAuthority(context);
+        UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
+        matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY,
+                SEARCH_SUGGEST);
+        matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
+                SEARCH_SUGGEST);
+        matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_SHORTCUT,
+                SEARCH_SHORTCUT);
+        matcher.addURI(authority, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
+                SEARCH_SHORTCUT);
+        return matcher;
+    }
+
+    protected String getAuthority(Context context) {
+        return context.getPackageName() + ".google";
+    }
+
 }
diff --git a/src/com/android/quicksearchbox/ui/ContactSuggestionView.java b/src/com/android/quicksearchbox/ui/ContactSuggestionView.java
index 0164c4c..6922b1d 100644
--- a/src/com/android/quicksearchbox/ui/ContactSuggestionView.java
+++ b/src/com/android/quicksearchbox/ui/ContactSuggestionView.java
@@ -16,12 +16,13 @@
 
 package com.android.quicksearchbox.ui;
 
+import com.android.quicksearchbox.R;
+import com.android.quicksearchbox.SuggestionCursor;
+
 import android.content.Context;
 import android.net.Uri;
 import android.util.AttributeSet;
 import android.widget.QuickContactBadge;
-import com.android.quicksearchbox.R;
-import com.android.quicksearchbox.SuggestionCursor;
 
 /**
  * View for contacts appearing in the suggestions list.
diff --git a/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java b/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java
index 8c45251..a82f0e9 100644
--- a/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java
+++ b/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java
@@ -21,7 +21,6 @@
 import com.android.quicksearchbox.SuggestionCursor;
 
 import android.content.Context;
-import android.content.Intent;
 import android.content.res.ColorStateList;
 import android.graphics.drawable.Drawable;
 import android.text.Html;
@@ -140,14 +139,15 @@
 
     public Drawable getSuggestionDrawableIcon1(SuggestionCursor suggestion) {
         Source source = suggestion.getSuggestionSource();
-        String icon1Id = suggestion.getSuggestionIcon1();
-        Drawable icon1 = source.getIcon(icon1Id);
+        String iconId = suggestion.getSuggestionIcon1();
+        Drawable icon1 = iconId == null ? null : source.getIcon(iconId);
         return icon1 == null ? source.getSourceIcon() : icon1;
     }
 
     public Drawable getSuggestionDrawableIcon2(SuggestionCursor suggestion) {
         Source source = suggestion.getSuggestionSource();
-        return source.getIcon(suggestion.getSuggestionIcon2());
+        String iconId = suggestion.getSuggestionIcon2();
+        return iconId == null ? null : source.getIcon(iconId);
     }
 
     private CharSequence formatText(String str, String format) {
diff --git a/tests/partial/src/com/android/quicksearchbox/tests/partial/PartialSuggestionLauncher.java b/tests/partial/src/com/android/quicksearchbox/tests/partial/PartialSuggestionLauncher.java
index bfff996..e088676 100644
--- a/tests/partial/src/com/android/quicksearchbox/tests/partial/PartialSuggestionLauncher.java
+++ b/tests/partial/src/com/android/quicksearchbox/tests/partial/PartialSuggestionLauncher.java
@@ -16,9 +16,7 @@
 
 package com.android.quicksearchbox.tests.partial;
 
-import com.android.quicksearchbox.tests.partial.R;
 import android.app.Activity;
-import android.content.Intent;
 import android.os.Bundle;
 
 public class PartialSuggestionLauncher extends Activity {
diff --git a/tests/src/com/android/quicksearchbox/MockCorpus.java b/tests/src/com/android/quicksearchbox/MockCorpus.java
index 3b029e3..1a25626 100644
--- a/tests/src/com/android/quicksearchbox/MockCorpus.java
+++ b/tests/src/com/android/quicksearchbox/MockCorpus.java
@@ -155,4 +155,8 @@
         return false;
     }
 
+    public boolean isLocationAware() {
+        return false;
+    }
+
 }
diff --git a/tests/src/com/android/quicksearchbox/MockSource.java b/tests/src/com/android/quicksearchbox/MockSource.java
index be56b29..ddf584c 100644
--- a/tests/src/com/android/quicksearchbox/MockSource.java
+++ b/tests/src/com/android/quicksearchbox/MockSource.java
@@ -105,6 +105,10 @@
         return true;
     }
 
+    public boolean isLocationAware() {
+        return false;
+    }
+
     public SourceResult getSuggestions(String query, int queryLimit, boolean onlySource) {
         if (query.length() == 0) {
             return null;
diff --git a/tests/src/com/android/quicksearchbox/RankAwarePromoterTest.java b/tests/src/com/android/quicksearchbox/RankAwarePromoterTest.java
index 4fe3467..52ba9e6 100644
--- a/tests/src/com/android/quicksearchbox/RankAwarePromoterTest.java
+++ b/tests/src/com/android/quicksearchbox/RankAwarePromoterTest.java
@@ -20,8 +20,6 @@
 import android.test.suitebuilder.annotation.SmallTest;
 
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 
 /**
diff --git a/tests/src/com/android/quicksearchbox/SearchActivityTest.java b/tests/src/com/android/quicksearchbox/SearchActivityTest.java
index cee0317..b64549c 100644
--- a/tests/src/com/android/quicksearchbox/SearchActivityTest.java
+++ b/tests/src/com/android/quicksearchbox/SearchActivityTest.java
@@ -15,8 +15,6 @@
  */
 package com.android.quicksearchbox;
 
-import com.android.quicksearchbox.SearchActivity;
-
 import android.test.ActivityInstrumentationTestCase2;
 
 /**
diff --git a/tests/src/com/android/quicksearchbox/SuggestionsProviderImplTest.java b/tests/src/com/android/quicksearchbox/SuggestionsProviderImplTest.java
index 89c2319..59b3c4a 100644
--- a/tests/src/com/android/quicksearchbox/SuggestionsProviderImplTest.java
+++ b/tests/src/com/android/quicksearchbox/SuggestionsProviderImplTest.java
@@ -43,7 +43,7 @@
         mCorpora.addCorpus(MockCorpus.CORPUS_1);
         mCorpora.addCorpus(MockCorpus.CORPUS_2);
         CorpusRanker corpusRanker = new LexicographicalCorpusRanker(mCorpora);
-        Logger logger = new MockLogger();
+        Logger logger = new NoLogger();
         mProvider = new SuggestionsProviderImpl(config,
                 mTaskExecutor,
                 publishThread,