Merge Google and Browser sources and call them Web

This required a gigantic refactoring of QuickSearchBox to work nicely.

This change also reduces the number of promoted source to 3,
since Web is now one source instead of two.

As a side effect of the recatoring, VoiceSearch now searches
the selected corpus (fixes http://b/issue?id=2438309)

Fixes http://b/issue?id=2365770

Change-Id: Ife8d40ef62ea004e8d0f20a60e9196fc589f01fc
diff --git a/src/com/android/quicksearchbox/AbstractCorpus.java b/src/com/android/quicksearchbox/AbstractCorpus.java
new file mode 100644
index 0000000..c6bae42
--- /dev/null
+++ b/src/com/android/quicksearchbox/AbstractCorpus.java
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+
+/**
+ * Base class for corpus implementations.
+ */
+public abstract class AbstractCorpus implements Corpus {
+
+    public AbstractCorpus() {
+    }
+
+    @Override
+    public String toString() {
+        return getName();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o != null && getClass().equals(o.getClass())) {
+            return getName().equals(((Corpus) o).getName());
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return getName().hashCode();
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/AbstractSourceSuggestionCursor.java b/src/com/android/quicksearchbox/AbstractSourceSuggestionCursor.java
deleted file mode 100644
index 960acaa..0000000
--- a/src/com/android/quicksearchbox/AbstractSourceSuggestionCursor.java
+++ /dev/null
@@ -1,67 +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.content.ComponentName;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-
-/**
- * Base class for SuggestionCursor implementations that can get a {@link Source}
- * object for each suggestion (possibly the same for all suggestions).
- *
- */
-public abstract class AbstractSourceSuggestionCursor extends AbstractSuggestionCursor {
-
-    public AbstractSourceSuggestionCursor(String userQuery) {
-        super(userQuery);
-    }
-
-    /**
-     * Gets the source for the current suggestion.
-     */
-    protected abstract Source getSource();
-
-    public ComponentName getSourceComponentName() {
-        return getSource().getComponentName();
-    }
-
-    public String getLogName() {
-        return getSource().getLogName();
-    }
-
-    public CharSequence getSourceLabel() {
-        return getSource().getLabel();
-    }
-
-    public Drawable getSourceIcon() {
-        return getSource().getSourceIcon();
-    }
-
-    public Uri getSourceIconUri() {
-        return getSource().getSourceIconUri();
-    }
-
-    public Drawable getIcon(String drawableId) {
-        return getSource().getIcon(drawableId);
-    }
-
-    public Uri getIconUri(String iconId) {
-        return getSource().getIconUri(iconId);
-    }
-
-}
diff --git a/src/com/android/quicksearchbox/AbstractSuggestionCursor.java b/src/com/android/quicksearchbox/AbstractSuggestionCursor.java
index 1909426..66f634d 100644
--- a/src/com/android/quicksearchbox/AbstractSuggestionCursor.java
+++ b/src/com/android/quicksearchbox/AbstractSuggestionCursor.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2009 The Android Open Source Project
+ * 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.
@@ -16,23 +16,14 @@
 
 package com.android.quicksearchbox;
 
-import android.database.DataSetObservable;
-import android.database.DataSetObserver;
-import android.graphics.drawable.Drawable;
-import android.text.Html;
-import android.text.TextUtils;
 
 /**
- * Base class for SuggestionCursor implementations.
- *
+ * Base class for suggestion cursors.
  */
 public abstract class AbstractSuggestionCursor implements SuggestionCursor {
 
-    /** The user query that returned these suggestions. */
     private final String mUserQuery;
 
-    private final DataSetObservable mDataSetObservable = new DataSetObservable();
-
     public AbstractSuggestionCursor(String userQuery) {
         mUserQuery = userQuery;
     }
@@ -41,55 +32,4 @@
         return mUserQuery;
     }
 
-    public void registerDataSetObserver(DataSetObserver observer) {
-        mDataSetObservable.registerObserver(observer);
-    }
-
-    public void unregisterDataSetObserver(DataSetObserver observer) {
-        mDataSetObservable.unregisterObserver(observer);
-    }
-
-    protected void notifyDataSetChanged() {
-        mDataSetObservable.notifyChanged();
-    }
-
-    public void close() {
-        mDataSetObservable.unregisterAll();
-    }
-
-    public Drawable getSuggestionDrawableIcon1() {
-        String icon1Id = getSuggestionIcon1();
-        Drawable icon1 = getIcon(icon1Id);
-        return icon1 == null ? getSourceIcon() : icon1;
-    }
-
-    public Drawable getSuggestionDrawableIcon2() {
-        return getIcon(getSuggestionIcon2());
-    }
-
-    public CharSequence getSuggestionFormattedText1() {
-        return formatText(getSuggestionText1());
-    }
-
-    public CharSequence getSuggestionFormattedText2() {
-        return formatText(getSuggestionText2());
-    }
-
-    private CharSequence formatText(String str) {
-        boolean isHtml = "html".equals(getSuggestionFormat());
-        if (isHtml && looksLikeHtml(str)) {
-            return Html.fromHtml(str);
-        } else {
-            return str;
-        }
-    }
-
-    private boolean looksLikeHtml(String str) {
-        if (TextUtils.isEmpty(str)) return false;
-        for (int i = str.length() - 1; i >= 0; i--) {
-            char c = str.charAt(i);
-            if (c == '>' || c == '&') return true;
-        }
-        return false;
-    }
 }
diff --git a/src/com/android/quicksearchbox/AbstractSuggestionCursorWrapper.java b/src/com/android/quicksearchbox/AbstractSuggestionCursorWrapper.java
new file mode 100644
index 0000000..cf84dc7
--- /dev/null
+++ b/src/com/android/quicksearchbox/AbstractSuggestionCursorWrapper.java
@@ -0,0 +1,93 @@
+/*
+ * 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;
+
+/**
+ * A SuggestionCursor that delegates all suggestions-specific calls to one or more
+ * other suggestion cursors.
+ */
+public abstract class AbstractSuggestionCursorWrapper extends AbstractSuggestionCursor {
+
+    public AbstractSuggestionCursorWrapper(String userQuery) {
+        super(userQuery);
+    }
+
+    /**
+     * Gets the SuggestionCursor to use for the current suggestion.
+     */
+    protected abstract SuggestionCursor current();
+
+    public String getShortcutId() {
+        return current().getShortcutId();
+    }
+
+    public String getSuggestionDisplayQuery() {
+        return current().getSuggestionDisplayQuery();
+    }
+
+    public String getSuggestionFormat() {
+        return current().getSuggestionFormat();
+    }
+
+    public String getSuggestionIcon1() {
+        return current().getSuggestionIcon1();
+    }
+
+    public String getSuggestionIcon2() {
+        return current().getSuggestionIcon2();
+    }
+
+    public String getSuggestionIntentAction() {
+        return current().getSuggestionIntentAction();
+    }
+
+    public String getSuggestionIntentDataString() {
+        return current().getSuggestionIntentDataString();
+    }
+
+    public String getSuggestionIntentExtraData() {
+        return current().getSuggestionIntentExtraData();
+    }
+
+    public String getSuggestionKey() {
+        return current().getSuggestionKey();
+    }
+
+    public String getSuggestionLogType() {
+        return current().getSuggestionLogType();
+    }
+
+    public String getSuggestionQuery() {
+        return current().getSuggestionQuery();
+    }
+
+    public Source getSuggestionSource() {
+        return current().getSuggestionSource();
+    }
+
+    public String getSuggestionText1() {
+        return current().getSuggestionText1();
+    }
+
+    public String getSuggestionText2() {
+        return current().getSuggestionText2();
+    }
+
+    public boolean isSpinnerWhileRefreshing() {
+        return current().isSpinnerWhileRefreshing();
+    }
+}
diff --git a/src/com/android/quicksearchbox/AbstractSuggestionsProvider.java b/src/com/android/quicksearchbox/AbstractSuggestionsProvider.java
index e6e5846..322aa8d 100644
--- a/src/com/android/quicksearchbox/AbstractSuggestionsProvider.java
+++ b/src/com/android/quicksearchbox/AbstractSuggestionsProvider.java
@@ -68,7 +68,7 @@
         }
     }
 
-    public abstract ArrayList<Source> getOrderedSources();
+    public abstract ArrayList<Corpus> getOrderedCorpora();
 
     protected abstract SuggestionCursor getShortcutsForQuery(String query);
 
@@ -76,27 +76,27 @@
      * Gets the sources that should be queried for the given query.
      *
      */
-    private ArrayList<Source> getSourcesToQuery(String query) {
+    private ArrayList<Corpus> getCorporaToQuery(String query) {
         if (query.length() == 0) {
-            return new ArrayList<Source>();
+            return new ArrayList<Corpus>(0);
         }
-        ArrayList<Source> orderedSources = getOrderedSources();
-        ArrayList<Source> sourcesToQuery = new ArrayList<Source>(orderedSources.size());
-        for (Source source : orderedSources) {
-            if (shouldQuerySource(source, query)) {
-                sourcesToQuery.add(source);
+        ArrayList<Corpus> orderedCorpora = getOrderedCorpora();
+        ArrayList<Corpus> corporaToQuery = new ArrayList<Corpus>(orderedCorpora.size());
+        for (Corpus corpus : orderedCorpora) {
+            if (shouldQueryCorpus(corpus, query)) {
+                corporaToQuery.add(corpus);
             }
         }
-        return sourcesToQuery;
+        return corporaToQuery;
     }
 
-    protected boolean shouldQuerySource(Source source, String query) {
-        return mShouldQueryStrategy.shouldQuerySource(source, query);
+    protected boolean shouldQueryCorpus(Corpus corpus, String query) {
+        return mShouldQueryStrategy.shouldQueryCorpus(corpus, query);
     }
 
-    private void updateShouldQueryStrategy(SuggestionCursor cursor) {
+    private void updateShouldQueryStrategy(CorpusResult cursor) {
         if (cursor.getCount() == 0) {
-            mShouldQueryStrategy.onZeroResults(cursor.getSourceComponentName(),
+            mShouldQueryStrategy.onZeroResults(cursor.getCorpus(),
                     cursor.getUserQuery());
         }
     }
@@ -104,18 +104,18 @@
     public Suggestions getSuggestions(String query) {
         if (DBG) Log.d(TAG, "getSuggestions(" + query + ")");
         cancelPendingTasks();
-        ArrayList<Source> sourcesToQuery = getSourcesToQuery(query);
+        ArrayList<Corpus> corporaToQuery = getCorporaToQuery(query);
         final Suggestions suggestions = new Suggestions(mPromoter,
                 mConfig.getMaxPromotedSuggestions(),
                 query,
-                sourcesToQuery.size());
+                corporaToQuery.size());
         SuggestionCursor shortcuts = getShortcutsForQuery(query);
         if (shortcuts != null) {
             suggestions.setShortcuts(shortcuts);
         }
 
         // Fast path for the zero sources case
-        if (sourcesToQuery.size() == 0) {
+        if (corporaToQuery.size() == 0) {
             return suggestions;
         }
 
@@ -126,8 +126,8 @@
                 mBatchingExecutor, suggestions);
 
         int maxResultsPerSource = mConfig.getMaxResultsPerSource();
-        for (Source source : sourcesToQuery) {
-            QueryTask task = new QueryTask(query, source, maxResultsPerSource, receiver);
+        for (Corpus corpus : corporaToQuery) {
+            QueryTask task = new QueryTask(query, corpus, maxResultsPerSource, receiver);
             mBatchingExecutor.execute(task);
         }
 
@@ -144,11 +144,11 @@
             mSuggestions = suggestions;
         }
 
-        public void receiveSuggestionCursor(final SuggestionCursor cursor) {
+        public void receiveSuggestionCursor(final CorpusResult cursor) {
             updateShouldQueryStrategy(cursor);
             mPublishThread.post(new Runnable() {
                 public void run() {
-                    mSuggestions.addSourceResult(cursor);
+                    mSuggestions.addCorpusResult(cursor);
                     if (!mSuggestions.isClosed()) {
                         executeNextBatchIfNeeded();
                     }
@@ -172,30 +172,26 @@
      */
     private static class QueryTask implements SourceTask {
         private final String mQuery;
-        private final Source mSource;
+        private final Corpus mCorpus;
         private final int mQueryLimit;
         private final SuggestionCursorReceiver mReceiver;
 
-        public QueryTask(String query, Source source, int queryLimit,
+        public QueryTask(String query, Corpus corpus, int queryLimit,
                 SuggestionCursorReceiver receiver) {
             mQuery = query;
-            mSource = source;
+            mCorpus = corpus;
             mQueryLimit = queryLimit;
             mReceiver = receiver;
         }
 
-        public Source getSource() {
-            return mSource;
-        }
-
         public void run() {
-            SuggestionCursor cursor = mSource.getSuggestions(mQuery, mQueryLimit);
+            CorpusResult cursor = mCorpus.getSuggestions(mQuery, mQueryLimit);
             mReceiver.receiveSuggestionCursor(cursor);
         }
 
         @Override
         public String toString() {
-            return mSource + "[" + mQuery + "]";
+            return mCorpus + "[" + mQuery + "]";
         }
     }
 }
diff --git a/src/com/android/quicksearchbox/Config.java b/src/com/android/quicksearchbox/Config.java
index a0d40d6..ea27959 100644
--- a/src/com/android/quicksearchbox/Config.java
+++ b/src/com/android/quicksearchbox/Config.java
@@ -36,7 +36,7 @@
 
     private static final long DAY_MILLIS = 86400000L;
 
-    private static final int NUM_PROMOTED_SOURCES = 4;
+    private static final int NUM_PROMOTED_SOURCES = 3;
     private static final int MAX_PROMOTED_SUGGESTIONS = 8;
     private static final int MAX_RESULTS_PER_SOURCE = 50;
     private static final long SOURCE_TIMEOUT_MILLIS = 10000;
@@ -55,7 +55,7 @@
     private static final long THREAD_START_DELAY_MILLIS = 100;
 
     private final Context mContext;
-    private HashSet<String> mTrustedPackages;
+    private HashSet<String> mDefaultCorpora;
 
     /**
      * Creates a new config that uses hard-coded default values.
@@ -76,35 +76,29 @@
     public void close() {
     }
 
-    private HashSet<String> loadTrustedPackages() {
-        HashSet<String> trusted = new HashSet<String>();
-
+    private HashSet<String> loadDefaultCorpora() {
+        HashSet<String> defaultCorpora = new HashSet<String>();
         try {
-            // Get the list of trusted packages from a resource, which allows vendor overlays.
-            String[] trustedPackages = mContext.getResources().getStringArray(
-                    R.array.trusted_search_providers);
-            if (trustedPackages == null) {
-                Log.w(TAG, "Could not load list of trusted search providers, trusting none");
-                return trusted;
+            // Get the list of default corpora from a resource, which allows vendor overlays.
+            String[] corpora = mContext.getResources().getStringArray(R.array.default_corpora);
+            for (String corpus : corpora) {
+                defaultCorpora.add(corpus);
             }
-            for (String trustedPackage : trustedPackages) {
-                trusted.add(trustedPackage);
-            }
-            return trusted;
+            return defaultCorpora;
         } catch (Resources.NotFoundException ex) {
-            Log.w(TAG, "Could not load list of trusted search providers, trusting none");
-            return trusted;
+            Log.e(TAG, "Could not load default corpora", ex);
+            return defaultCorpora;
         }
     }
 
     /**
      * Checks if we trust the given source not to be spammy.
      */
-    public synchronized boolean isTrustedSource(String packageName) {
-        if (mTrustedPackages == null) {
-            mTrustedPackages = loadTrustedPackages();
+    public synchronized boolean isCorpusEnabledByDefault(String corpusName) {
+        if (mDefaultCorpora == null) {
+            mDefaultCorpora = loadDefaultCorpora();
         }
-        return mTrustedPackages.contains(packageName);
+        return mDefaultCorpora.contains(corpusName);
     }
 
     /**
diff --git a/src/com/android/quicksearchbox/Corpora.java b/src/com/android/quicksearchbox/Corpora.java
new file mode 100644
index 0000000..e1ced04
--- /dev/null
+++ b/src/com/android/quicksearchbox/Corpora.java
@@ -0,0 +1,57 @@
+/*
+ * 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.content.ComponentName;
+
+import java.util.Collection;
+
+/**
+ * Maintains the set of available and enabled corpora.
+ */
+public interface Corpora {
+
+    boolean isCorpusEnabled(Corpus corpus);
+
+    /**
+     * Checks if a corpus should be enabled by default.
+     */
+    boolean isCorpusDefaultEnabled(Corpus corpus);
+
+    /**
+     * Gets all corpora, including the web corpus.
+     *
+     * @return Callers must not modify the returned collection.
+     */
+    Collection<Corpus> getAllCorpora();
+
+    Collection<Corpus> getEnabledCorpora();
+
+    /**
+     * Gets a corpus by name.
+     *
+     * @return A corpus, or null.
+     */
+    Corpus getCorpus(String name);
+
+    Source getSource(ComponentName name);
+
+    /**
+     * Gets the corpus that contains the given source.
+     */
+    Corpus getCorpusForSource(Source source);
+}
diff --git a/src/com/android/quicksearchbox/Corpus.java b/src/com/android/quicksearchbox/Corpus.java
new file mode 100644
index 0000000..5853a81
--- /dev/null
+++ b/src/com/android/quicksearchbox/Corpus.java
@@ -0,0 +1,78 @@
+/*
+ * 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.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+
+/**
+ * A corpus is a user-visible set of suggestions. A corpus gets suggestions from one
+ * or more sources.
+ *
+ * Objects that implement this interface should override {@link Object#equals(Object)}
+ * and {@link Object#hashCode()} so that they can be used as keys in hash maps.
+ */
+public interface Corpus {
+
+    /**
+     * Gets the localized, human-readable label for this corpus.
+     */
+    CharSequence getLabel();
+
+    /**
+     * Gets the icon for this corpus.
+     */
+    Drawable getCorpusIcon();
+
+    /**
+     * Gets the icon URI for this corpus.
+     */
+    Uri getCorpusIconUri();
+
+    /**
+     * Gets the description to use for this corpus in system search settings.
+     */
+    CharSequence getSettingsDescription();
+
+    /**
+     * Gets suggestions from this corpus.
+     *
+     * @param query The user query.
+     * @param queryLimit An advisory maximum number of results that the source should return.
+     * @return The suggestion results.
+     */
+    CorpusResult getSuggestions(String query, int queryLimit);
+
+    /**
+     * Gets the unique name for this corpus.
+     */
+    String getName();
+
+    int getQueryThreshold();
+
+    boolean queryAfterZeroResults();
+
+    boolean voiceSearchEnabled();
+
+    Intent createSearchIntent(String query, Bundle appData);
+
+    Intent createVoiceSearchIntent(Bundle appData);
+
+    boolean isWebCorpus();
+}
diff --git a/src/com/android/quicksearchbox/SourceFactory.java b/src/com/android/quicksearchbox/CorpusRanker.java
similarity index 68%
rename from src/com/android/quicksearchbox/SourceFactory.java
rename to src/com/android/quicksearchbox/CorpusRanker.java
index e661498..e9f43c3 100644
--- a/src/com/android/quicksearchbox/SourceFactory.java
+++ b/src/com/android/quicksearchbox/CorpusRanker.java
@@ -16,14 +16,14 @@
 
 package com.android.quicksearchbox;
 
-import android.app.SearchableInfo;
+import java.util.ArrayList;
+import java.util.Collection;
 
-public interface SourceFactory {
+/**
+ * Orders corpora by importance.
+ */
+public interface CorpusRanker {
 
-    // TODO: perhaps SearchManager.getSearchablesInGlobalSearch()
-    // should return List<ComponentName>, so we could use ComponentName here?
-    Source createSource(SearchableInfo searchable);
-
-    Source createWebSearchSource();
+    ArrayList<Corpus> rankCorpora(Collection<Corpus> corpora);
 
 }
diff --git a/src/com/android/quicksearchbox/SourceFactory.java b/src/com/android/quicksearchbox/CorpusResult.java
similarity index 62%
copy from src/com/android/quicksearchbox/SourceFactory.java
copy to src/com/android/quicksearchbox/CorpusResult.java
index e661498..ce2bd9d 100644
--- a/src/com/android/quicksearchbox/SourceFactory.java
+++ b/src/com/android/quicksearchbox/CorpusResult.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2010 The Android Open Source Project
+ * 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.
@@ -16,14 +16,20 @@
 
 package com.android.quicksearchbox;
 
-import android.app.SearchableInfo;
 
-public interface SourceFactory {
+/**
+ * A sequence of suggestions from a single corpus.
+ */
+public interface CorpusResult extends SuggestionCursor {
 
-    // TODO: perhaps SearchManager.getSearchablesInGlobalSearch()
-    // should return List<ComponentName>, so we could use ComponentName here?
-    Source createSource(SearchableInfo searchable);
+    /**
+     * Gets the corpus that produced these suggestions.
+     */
+    Corpus getCorpus();
 
-    Source createWebSearchSource();
+    /**
+     * The user query that returned these suggestions.
+     */
+    String getUserQuery();
 
 }
diff --git a/src/com/android/quicksearchbox/SelectSearchSourceDialog.java b/src/com/android/quicksearchbox/CorpusSelectionDialog.java
similarity index 67%
rename from src/com/android/quicksearchbox/SelectSearchSourceDialog.java
rename to src/com/android/quicksearchbox/CorpusSelectionDialog.java
index 47655c1..3005fe2 100644
--- a/src/com/android/quicksearchbox/SelectSearchSourceDialog.java
+++ b/src/com/android/quicksearchbox/CorpusSelectionDialog.java
@@ -16,14 +16,12 @@
 
 package com.android.quicksearchbox;
 
-import com.android.quicksearchbox.ui.SearchSourceSelector;
-import com.android.quicksearchbox.ui.SourcesAdapter;
+import com.android.quicksearchbox.ui.CorporaAdapter;
 import com.android.quicksearchbox.ui.SuggestionViewFactory;
 
 import android.app.Dialog;
 import android.app.SearchManager;
 import android.content.ActivityNotFoundException;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
@@ -38,35 +36,35 @@
 
 
 /**
- * Search source selection dialog.
+ * Corpus selection dialog.
  */
-public class SelectSearchSourceDialog extends Dialog {
+public class CorpusSelectionDialog extends Dialog {
 
     private static final boolean DBG = true;
     private static final String TAG = "QSB.SelectSearchSourceDialog";
 
-    private GridView mSourceList;
+    private GridView mCorpusGrid;
 
-    private ComponentName mSource;
+    private Corpus mCorpus;
 
     private String mQuery;
 
     private Bundle mAppData;
 
-    public SelectSearchSourceDialog(Context context) {
+    public CorpusSelectionDialog(Context context) {
         super(context, R.style.Theme_SelectSearchSource);
-        setContentView(R.layout.select_search_source);
-        mSourceList = (GridView) findViewById(R.id.source_list);
-        mSourceList.setOnItemClickListener(new SourceClickListener());
+        setContentView(R.layout.corpus_selection_dialog);
+        mCorpusGrid = (GridView) findViewById(R.id.corpus_grid);
+        mCorpusGrid.setOnItemClickListener(new CorpusClickListener());
         // TODO: for some reason, putting this in the XML layout instead makes
         // the list items unclickable.
-        mSourceList.setFocusable(true);
+        mCorpusGrid.setFocusable(true);
         setCanceledOnTouchOutside(true);
         positionWindow();
     }
 
-    public void setSource(ComponentName source) {
-        mSource = source;
+    public void setCorpus(Corpus corpus) {
+        mCorpus = corpus;
     }
 
     public void setQuery(String query) {
@@ -79,8 +77,8 @@
 
     private void positionWindow() {
         Resources resources = getContext().getResources();
-        int x = resources.getDimensionPixelSize(R.dimen.select_source_x);
-        int y = resources.getDimensionPixelSize(R.dimen.select_source_y);
+        int x = resources.getDimensionPixelSize(R.dimen.corpus_selection_dialog_x);
+        int y = resources.getDimensionPixelSize(R.dimen.corpus_selection_dialog_y);
         positionArrowAt(x, y);
     }
 
@@ -104,46 +102,50 @@
     @Override
     protected void onStart() {
         super.onStart();
-        updateSources();
+        updateCorpora();
     }
 
-    private void updateSources() {
-        mSourceList.setAdapter(new SourcesAdapter(getViewFactory(), getGlobalSuggestionsProvider()));
+    private void updateCorpora() {
+        mCorpusGrid.setAdapter(
+                new CorporaAdapter(getViewFactory(), getCorpora(), getCorpusRanker()));
     }
 
     private QsbApplication getQsbApplication() {
         return (QsbApplication) getContext().getApplicationContext();
     }
 
-    private SuggestionsProvider getGlobalSuggestionsProvider() {
-        return getQsbApplication().getGlobalSuggestionsProvider();
+    private Corpora getCorpora() {
+        return getQsbApplication().getCorpora();
+    }
+
+    private CorpusRanker getCorpusRanker() {
+        return getQsbApplication().getCorpusRanker();
     }
 
     private SuggestionViewFactory getViewFactory() {
         return getQsbApplication().getSuggestionViewFactory();
     }
 
-    protected void selectSource(Source source) {
+    protected void selectCorpus(Corpus corpus) {
         dismiss();
         // If a new source was selected, start QSB with that source.
         // If the old source was selected, just finish.
-        if (!isCurrentSource(source)) {
-            switchSource(source);
+        if (!isCurrentCorpus(corpus)) {
+            switchCorpus(corpus);
         }
     }
 
-    private boolean isCurrentSource(Source source) {
-        if (source == null) return mSource == null;
-        return source.getComponentName().equals(mSource);
+    private boolean isCurrentCorpus(Corpus corpus) {
+        if (corpus == null) return mCorpus == null;
+        return corpus.equals(mCorpus);
     }
 
-    private void switchSource(Source source) {
-        if (DBG) Log.d(TAG, "switchSource(" + source + ")");
+    private void switchCorpus(Corpus corpus) {
+        if (DBG) Log.d(TAG, "switchSource(" + corpus + ")");
 
         Intent searchIntent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
         searchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        ComponentName sourceName = source == null ? null : source.getComponentName();
-        SearchSourceSelector.setSource(searchIntent, sourceName);
+        searchIntent.setData(SearchActivity.getCorpusUri(corpus));
         searchIntent.putExtra(SearchManager.QUERY, mQuery);
         searchIntent.putExtra(SearchManager.APP_DATA, mAppData);
 
@@ -155,10 +157,10 @@
         }
     }
 
-    private class SourceClickListener implements AdapterView.OnItemClickListener {
+    private class CorpusClickListener implements AdapterView.OnItemClickListener {
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-            Source source = (Source) parent.getItemAtPosition(position);
-            selectSource(source);
+            Corpus corpus = (Corpus) parent.getItemAtPosition(position);
+            selectCorpus(corpus);
         }
     }
 }
diff --git a/src/com/android/quicksearchbox/CursorBackedSuggestionCursor.java b/src/com/android/quicksearchbox/CursorBackedSuggestionCursor.java
index 4c88594..e1c618b 100644
--- a/src/com/android/quicksearchbox/CursorBackedSuggestionCursor.java
+++ b/src/com/android/quicksearchbox/CursorBackedSuggestionCursor.java
@@ -17,20 +17,13 @@
 package com.android.quicksearchbox;
 
 import android.app.SearchManager;
-import android.content.ComponentName;
-import android.content.Context;
 import android.content.Intent;
 import android.database.Cursor;
-import android.graphics.Rect;
+import android.database.DataSetObserver;
 import android.net.Uri;
-import android.os.Bundle;
-import android.os.Looper;
 import android.util.Log;
-import android.view.KeyEvent;
 
-import java.net.URISyntaxException;
-
-public abstract class CursorBackedSuggestionCursor extends AbstractSourceSuggestionCursor {
+public abstract class CursorBackedSuggestionCursor extends AbstractSuggestionCursor {
 
     private static final boolean DBG = false;
     protected static final String TAG = "QSB.CursorBackedSuggestionCursor";
@@ -38,22 +31,22 @@
     /** The suggestions, or {@code null} if the suggestions query failed. */
     protected final Cursor mCursor;
 
-    /** Column index of {@link SearchManager.SUGGEST_COLUMN_FORMAT} in @{link mCursor}. */
+    /** Column index of {@link SearchManager#SUGGEST_COLUMN_FORMAT} in @{link mCursor}. */
     private final int mFormatCol;
 
-    /** Column index of {@link SearchManager.SUGGEST_COLUMN_TEXT_1} in @{link mCursor}. */
+    /** Column index of {@link SearchManager#SUGGEST_COLUMN_TEXT_1} in @{link mCursor}. */
     private final int mText1Col;
 
-    /** Column index of {@link SearchManager.SUGGEST_COLUMN_TEXT_2} in @{link mCursor}. */
+    /** Column index of {@link SearchManager#SUGGEST_COLUMN_TEXT_2} in @{link mCursor}. */
     private final int mText2Col;
 
-    /** Column index of {@link SearchManager.SUGGEST_COLUMN_ICON_1} in @{link mCursor}. */
+    /** Column index of {@link SearchManager#SUGGEST_COLUMN_ICON_1} in @{link mCursor}. */
     private final int mIcon1Col;
 
-    /** Column index of {@link SearchManager.SUGGEST_COLUMN_ICON_1} in @{link mCursor}. */
+    /** Column index of {@link SearchManager#SUGGEST_COLUMN_ICON_1} in @{link mCursor}. */
     private final int mIcon2Col;
 
-    /** Column index of {@link SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING}
+    /** Column index of {@link SearchManager#SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING}
      * in @{link mCursor}.
      **/
     private final int mRefreshSpinnerCol;
@@ -72,24 +65,10 @@
         mRefreshSpinnerCol = getColumnIndex(SearchManager.SUGGEST_COLUMN_SPINNER_WHILE_REFRESHING);
     }
 
-    protected String getDefaultIntentAction() {
-        return getSource().getDefaultIntentAction();
-    }
+    public abstract Source getSuggestionSource();
 
-    protected String getDefaultIntentData() {
-        return getSource().getDefaultIntentData();
-    }
-
-    protected boolean shouldRewriteQueryFromData() {
-        return getSource().shouldRewriteQueryFromData();
-    }
-
-    protected boolean shouldRewriteQueryFromText() {
-        return getSource().shouldRewriteQueryFromText();
-    }
-
-    public boolean isFailed() {
-        return mCursor == null;
+    public String getSuggestionLogType() {
+        return getSuggestionSource().getLogName();
     }
 
     public void close() {
@@ -126,11 +105,9 @@
             throw new IllegalStateException("moveTo(" + pos + ") after close()");
         }
         // TODO: all operations on cross-process cursors can throw random exceptions
-        if (mCursor == null || pos < 0 || pos >= mCursor.getCount()) {
-            throw new IndexOutOfBoundsException(pos + ", count=" + getCount());
+        if (!mCursor.moveToPosition(pos)) {
+            throw new IllegalArgumentException("Move to " + pos + ", count=" + getCount());
         }
-        // TODO: all operations on cross-process cursors can throw random exceptions
-        mCursor.moveToPosition(pos);
     }
 
     public int getPosition() {
@@ -140,26 +117,6 @@
         return mCursor.getPosition();
     }
 
-    public String getSuggestionDisplayQuery() {
-        String query = getSuggestionQuery();
-        if (query != null) {
-            return query;
-        }
-        if (shouldRewriteQueryFromData()) {
-            String data = getSuggestionIntentDataString();
-            if (data != null) {
-                return data;
-            }
-        }
-        if (shouldRewriteQueryFromText()) {
-            String text1 = getSuggestionText1();
-            if (text1 != null) {
-                return text1;
-            }
-        }
-        return null;
-    }
-
     public String getShortcutId() {
         return getStringOrNull(SearchManager.SUGGEST_COLUMN_SHORTCUT_ID);
     }
@@ -188,67 +145,14 @@
         return "true".equals(getStringOrNull(mRefreshSpinnerCol));
     }
 
-    public Intent getSuggestionIntent(Context context, Bundle appSearchData,
-            int actionKey, String actionMsg) {
-        String action = getSuggestionIntentAction();
-        Uri data = getSuggestionIntentData();
-        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(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);
-        }
-        if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
-            intent.putExtra(SearchManager.ACTION_KEY, actionKey);
-            intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
-        }
-        // TODO: Use this to tell sources this comes form global search
-        // The constants are currently hidden.
-        //        intent.putExtra(SearchManager.SEARCH_MODE,
-        //                SearchManager.MODE_GLOBAL_SEARCH_SUGGESTION);
-        intent.setComponent(getSourceComponentName());
-        return intent;
-    }
-
-    public String getActionKeyMsg(int keyCode) {
-        String result = null;
-        String column = getSource().getSuggestActionMsgColumn(keyCode);
-        if (column != null) {
-            result = getStringOrNull(column);
-        }
-        // If the cursor didn't give us a message, see if there's a single message defined
-        // for the actionkey (for all suggestions)
-        if (result == null) {
-            result = getSource().getSuggestActionMsg(keyCode);
-        }
-        return result;
-    }
-
     /**
      * Gets the intent action for the current suggestion.
      */
-    protected String getSuggestionIntentAction() {
+    public String getSuggestionIntentAction() {
         // use specific action if supplied, or default action if supplied, or fixed default
         String action = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
         if (action == null) {
-            action = getDefaultIntentAction();
+            action = getSuggestionSource().getDefaultIntentAction();
             if (action == null) {
                 action = Intent.ACTION_SEARCH;
             }
@@ -259,7 +163,7 @@
     /**
      * Gets the query for the current suggestion.
      */
-    protected String getSuggestionQuery() {
+    public String getSuggestionQuery() {
         return getStringOrNull(SearchManager.SUGGEST_COLUMN_QUERY);
     }
 
@@ -267,7 +171,7 @@
          // use specific data if supplied, or default data if supplied
          String data = getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_DATA);
          if (data == null) {
-             data = getDefaultIntentData();
+             data = getSuggestionSource().getDefaultIntentData();
          }
          // then, if an ID was provided, append it.
          if (data != null) {
@@ -280,22 +184,35 @@
      }
 
     /**
-     * Gets the intent data for the current suggestion.
-     */
-    protected Uri getSuggestionIntentData() {
-        String data = getSuggestionIntentDataString();
-        return (data == null) ? null : Uri.parse(data);
-    }
-
-    /**
      * Gets the intent extra data for the current suggestion.
      */
     public String getSuggestionIntentExtraData() {
         return getStringOrNull(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
     }
 
+    public String getSuggestionDisplayQuery() {
+        String query = getSuggestionQuery();
+        if (query != null) {
+            return query;
+        }
+        Source source = getSuggestionSource();
+        if (source.shouldRewriteQueryFromData()) {
+            String data = getSuggestionIntentDataString();
+            if (data != null) {
+                return data;
+            }
+        }
+        if (source.shouldRewriteQueryFromText()) {
+            String text1 = getSuggestionText1();
+            if (text1 != null) {
+                return text1;
+            }
+        }
+        return null;
+    }
+
     /**
-     * Gets the index of a column in {@link mCursor} by name.
+     * Gets the index of a column in {@link #mCursor} by name.
      *
      * @return The index, or {@code -1} if the column was not found.
      */
@@ -306,7 +223,7 @@
     }
 
     /**
-     * Gets the string value of a column in {@link mCursor} by column index.
+     * Gets the string value of a column in {@link #mCursor} by column index.
      *
      * @param col Column index.
      * @return The string value, or {@code null}.
@@ -328,7 +245,7 @@
     }
 
     /**
-     * Gets the string value of a column in {@link mCursor} by column name.
+     * Gets the string value of a column in {@link #mCursor} by column name.
      *
      * @param colName Column name.
      * @return The string value, or {@code null}.
@@ -357,4 +274,12 @@
                 .append(query)
                 .toString();
     }
+
+    public void registerDataSetObserver(DataSetObserver observer) {
+        // We don't watch Cursor-backed SuggestionCursors for changes
+    }
+
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        // We don't watch Cursor-backed SuggestionCursors for changes
+    }
 }
diff --git a/src/com/android/quicksearchbox/DefaultCorpusRanker.java b/src/com/android/quicksearchbox/DefaultCorpusRanker.java
new file mode 100644
index 0000000..263abc8
--- /dev/null
+++ b/src/com/android/quicksearchbox/DefaultCorpusRanker.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.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+import java.util.PriorityQueue;
+
+public class DefaultCorpusRanker implements CorpusRanker {
+
+    private static final boolean DBG = true;
+    private static final String TAG = "QSB.DefaultCorpusRanker";
+
+    private final ShortcutRepository mShortcuts;
+
+    public DefaultCorpusRanker(ShortcutRepository shortcuts) {
+        mShortcuts = shortcuts;
+    }
+
+    private static class ScoredCorpus implements Comparable<ScoredCorpus> {
+        public final Corpus mCorpus;
+        public final int mScore;
+        public ScoredCorpus(Corpus corpus, int score) {
+            mCorpus = corpus;
+            mScore = score;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (! (o instanceof ScoredCorpus)) return false;
+            ScoredCorpus sc = (ScoredCorpus) o;
+            return mCorpus.equals(sc.mCorpus) && mScore == sc.mScore;
+        }
+
+        public int compareTo(ScoredCorpus another) {
+            int scoreDiff = mScore - another.mScore;
+            if (scoreDiff != 0) {
+                return scoreDiff;
+            } else {
+                return mCorpus.getName().compareTo(another.mCorpus.getName());
+            }
+        }
+
+        @Override
+        public String toString() {
+            return mCorpus + ":" + mScore;
+        }
+    }
+
+    /**
+     * Scores a corpus. Higher score is better.
+     */
+    private int getCorpusScore(Corpus corpus, Map<String,Integer> clickScores) {
+        if (corpus.isWebCorpus()) {
+            return Integer.MAX_VALUE;
+        }
+        Integer clickScore = clickScores.get(clickScores);
+        if (clickScore != null) {
+            return clickScore;
+        }
+        return 0;
+    }
+
+    public ArrayList<Corpus> rankCorpora(Collection<Corpus> corpora) {
+        if (DBG) Log.d(TAG, "Ranking: " + corpora);
+        // For some reason, PriorityQueue throws IllegalArgumentException if given
+        // an initial capacity < 1.
+        int capacity = 1 + corpora.size();
+        // PriorityQueue returns smallest element first, so we need a reverse comparator
+        PriorityQueue<ScoredCorpus> queue =
+                new PriorityQueue<ScoredCorpus>(capacity,
+                        new ReverseComparator<ScoredCorpus>());
+        Map<String,Integer> clickScores = mShortcuts.getCorpusScores();
+        for (Corpus corpus : corpora) {
+            int score = getCorpusScore(corpus, clickScores);
+            queue.add(new ScoredCorpus(corpus, score));
+        }
+        if (DBG) Log.d(TAG, "Corpus scores: " + queue);
+        ArrayList<Corpus> ordered = new ArrayList<Corpus>(queue.size());
+        for (ScoredCorpus scoredCorpus : queue) {
+            ordered.add(scoredCorpus.mCorpus);
+        }
+        return ordered;
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/DelayingSourceTaskExecutor.java b/src/com/android/quicksearchbox/DelayingSourceTaskExecutor.java
index df57d75..1e813ae 100644
--- a/src/com/android/quicksearchbox/DelayingSourceTaskExecutor.java
+++ b/src/com/android/quicksearchbox/DelayingSourceTaskExecutor.java
@@ -67,7 +67,7 @@
     }
 
     /**
-     * Thread that moves tasks from {@link #mDelayedTasks} to {@link mExecutor}
+     * Thread that moves tasks from {@link #mDelayedTasks} to {@link #mExecutor}
      * then the executor is running no tasks, or after a fixed delay.
      */
     private static class TaskDelayer extends Thread implements SourceTaskExecutor {
@@ -210,7 +210,7 @@
 
         /**
          * Blocks until {@code mRunningTaskCount <= 0}, waiting for at most
-         * {@link timeoutNanos}.
+         * {@code timeoutNanos}.
          *
          * @param timeoutNanos The maximum time to wait, in nanoseconds.
          * @return {@code false} if the timeout has expired upon return, else {@code true}.
diff --git a/src/com/android/quicksearchbox/EventLogLogger.java b/src/com/android/quicksearchbox/EventLogLogger.java
index 427a4d1..f1240a1 100644
--- a/src/com/android/quicksearchbox/EventLogLogger.java
+++ b/src/com/android/quicksearchbox/EventLogLogger.java
@@ -16,7 +16,6 @@
 
 package com.android.quicksearchbox;
 
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
@@ -61,52 +60,52 @@
         return mVersionCode;
     }
 
-    public void logStart(int latency, String intentSource, Source currentSearchSource,
-            ArrayList<Source> orderedSources) {
+    public void logStart(int latency, String intentSource, Corpus corpus,
+            ArrayList<Corpus> orderedCorpora) {
         String packageName = mContext.getPackageName();
         int version = mVersionCode;
         // TODO: Add more info to startMethod
         String startMethod = intentSource;
-        String currentSource = getSourceLogName(currentSearchSource);
-        String enabledSources = getSourceLogNames(orderedSources);
+        String currentCorpus = getCorpusLogName(corpus);
+        String enabledCorpora = getCorpusLogNames(orderedCorpora);
         if (DBG){
             debug("qsb_start", packageName, version, startMethod, latency,
-                    currentSource, enabledSources);
+                    currentCorpus, enabledCorpora);
         }
         EventLogTags.writeQsbStart(packageName, version, startMethod,
-                latency, currentSource, enabledSources);
+                latency, currentCorpus, enabledCorpora);
     }
 
     public void logSuggestionClick(int position,
-            SuggestionCursor suggestionCursor, ArrayList<Source> queriedSources) {
+            SuggestionCursor suggestionCursor, ArrayList<Corpus> queriedCorpora) {
         String suggestions = getSuggestions(suggestionCursor);
-        String sources = getSourceLogNames(queriedSources);
+        String corpora = getCorpusLogNames(queriedCorpora);
         int numChars = suggestionCursor.getUserQuery().length();
-        EventLogTags.writeQsbClick(position, suggestions, sources, numChars);
+        EventLogTags.writeQsbClick(position, suggestions, corpora, numChars);
     }
 
-    public void logSearch(Source searchSource, int startMethod, int numChars) {
-        String sourceName = getSourceLogName(searchSource);
-        EventLogTags.writeQsbSearch(sourceName, startMethod, numChars);
+    public void logSearch(Corpus corpus, int startMethod, int numChars) {
+        String corpusName = getCorpusLogName(corpus);
+        EventLogTags.writeQsbSearch(corpusName, startMethod, numChars);
     }
 
-    public void logVoiceSearch(Source searchSource) {
-        String sourceName = getSourceLogName(searchSource);
-        EventLogTags.writeQsbVoiceSearch(sourceName);
+    public void logVoiceSearch(Corpus corpus) {
+        String corpusName = getCorpusLogName(corpus);
+        EventLogTags.writeQsbVoiceSearch(corpusName);
     }
 
-    public void logExit(SuggestionCursor suggestionCursor) {
+    public void logExit(SuggestionCursor suggestionCursor, int numChars) {
         String suggestions = getSuggestions(suggestionCursor);
-        EventLogTags.writeQsbExit(suggestions);
+        EventLogTags.writeQsbExit(suggestions, numChars);
     }
 
     public void logWebLatency() {
         
     }
 
-    private String getSourceLogName(Source source) {
-        if (source == null) return null;
-        return source.getLogName();
+    private String getCorpusLogName(Corpus corpus) {
+        if (corpus == null) return null;
+        return corpus.getName();
     }
 
     private String getSuggestions(SuggestionCursor suggestionCursor) {
@@ -115,17 +114,17 @@
         for (int i = 0; i < count; i++) {
             if (i > 0) sb.append(LIST_SEPARATOR);
             suggestionCursor.moveTo(i);
-            sb.append(suggestionCursor.getLogName());
+            sb.append(suggestionCursor.getSuggestionLogType());
         }
         return sb.toString();
     }
 
-    private String getSourceLogNames(ArrayList<Source> sources) {
+    private String getCorpusLogNames(ArrayList<Corpus> corpora) {
         StringBuilder sb = new StringBuilder();
-        final int count = sources.size();
+        final int count = corpora.size();
         for (int i = 0; i < count; i++) {
             if (i > 0) sb.append(LIST_SEPARATOR);
-            sb.append(getSourceLogName(sources.get(i)));
+            sb.append(getCorpusLogName(corpora.get(i)));
         }
         return sb.toString();
     }
diff --git a/src/com/android/quicksearchbox/EventLogTags.logtags b/src/com/android/quicksearchbox/EventLogTags.logtags
index 42f18ba..cb5a3da 100644
--- a/src/com/android/quicksearchbox/EventLogTags.logtags
+++ b/src/com/android/quicksearchbox/EventLogTags.logtags
@@ -2,34 +2,55 @@
 
 option java_package com.android.quicksearchbox
 
-# TODO: Include start method:
-# - home screen widget
-# - through source selector
-# - by touching text field
-# - search hard key on home screen
-# - menu -> search on home screen
-# - source selector in in-app search dialog
-# - search hardkey in in-app search dialog
-# - search hardkey in non-searchable app
-# - app called SearchManager.startSearch()
+# QSB started
 # @param name Package name of the QSB app.
 # @param version QSB app versionCode value.
-# @param sources A string containing a pipe-separated list of source names,
-#                ordered by source ranking.
+# @param start_method
+#   TODO: Define values for start_method:
+#     - home screen widget
+#     - through source selector
+#     - by touching text field
+#     - search hard key on home screen
+#     - menu -> search on home screen
+#     - source selector in in-app search dialog
+#     - search hardkey in in-app search dialog
+#     - search hardkey in non-searchable app
+#     - app called SearchManager.startSearch()
+# @param latency start-up latency as seen by QSB
+# @param search_source name of the initially selected search source
+# @param enabled_sources A pipe-separated list of source names, ordered by source ranking.
 #                TODO: Which are promoted?
-80000 qsb_start (name|3),(version|1),(start_method|3),(latency|1|3),(current_source|3),(enabled_sources|3)
+80000 qsb_start (name|3),(version|1),(start_method|3),(latency|1|3),(search_source|3),(enabled_sources|3)
 
+# User clicked on a suggestion
+# @param position 0-based index of the clicked suggestion
+# @param A pipe-separated list of suggestion log names.
+#   TODO: define format of suggestion log names
+# @param queried_sources A pipe-separated list of the sources that were queried to produce
+#        the list of suggestions shown.
+# @param Number of characters in the query typed by the user
 # TODO: action key?
-# TODO: number of chars in typed query?
 # TODO: latency?
 80100 qsb_click (position|1),(suggestions|3),(queried_sources|3),(num_chars|1)
-# TODO: define method values (search button, enter key)
-80102 qsb_search (search_source|3),(method|1),(num_chars|1)
-80103 qsb_voice_search (source|3)
-# User left QSB without clicking / searching
-# TODO: number of chars typed?
-80104 qsb_exit (suggestions|3)
 
+# User launched a typed search
+# @param search_source Name of the selected search source
+# @param method
+#     SEARCH_METHOD_BUTTON = 0
+#     SEARCH_METHOD_KEYBOARD = 1
+# @param num_chars The number of characters in the search query
+80102 qsb_search (search_source|3),(method|1),(num_chars|1)
+
+# User launched a Voice Search
+# @param search_source Name of the selected search source
+80103 qsb_voice_search (search_source|3)
+
+# User left QSB without clicking / searching
+# @param suggestions The suggestions shown when the user left QSB. See qsb_click above.
+# @param num_chars The number of characters in the query text field when the user left
+80104 qsb_exit (suggestions|3),(num_chars|1)
+
+# Suggestion latency of the web suggestion source
 # This is total latency from typing a character to having all results.
 # Log only for N% of queries?
 # TODO: blended vs single-app
diff --git a/src/com/android/quicksearchbox/GlobalSuggestionsProvider.java b/src/com/android/quicksearchbox/GlobalSuggestionsProvider.java
index 4d571eb..268c18c 100644
--- a/src/com/android/quicksearchbox/GlobalSuggestionsProvider.java
+++ b/src/com/android/quicksearchbox/GlobalSuggestionsProvider.java
@@ -16,12 +16,9 @@
 
 package com.android.quicksearchbox;
 
-import android.content.ComponentName;
 import android.os.Handler;
-import android.util.Log;
 
 import java.util.ArrayList;
-import java.util.LinkedHashSet;
 
 /**
  * A suggestions provider that gets suggestions from all enabled sources that
@@ -29,50 +26,28 @@
  */
 public class GlobalSuggestionsProvider extends AbstractSuggestionsProvider {
 
-    private static final boolean DBG = true;
-    private static final String TAG = "QSB.GlobalSuggestionsProvider";
+    private final Corpora mCorpora;
 
-    private final SourceLookup mSources;
+    private final CorpusRanker mCorpusRanker;
 
     private final ShortcutRepository mShortcutRepo;
 
-    public GlobalSuggestionsProvider(Config config, SourceLookup sources,
+    public GlobalSuggestionsProvider(Config config, Corpora corpora,
             SourceTaskExecutor queryExecutor,
             Handler publishThread,
             Promoter promoter,
+            CorpusRanker corpusRanker,
             ShortcutRepository shortcutRepo) {
         super(config, queryExecutor, publishThread, promoter);
-        mSources = sources;
+        mCorpora = corpora;
+        mCorpusRanker = corpusRanker;
         mShortcutRepo = shortcutRepo;
     }
 
     // TODO: Cache this list?
     @Override
-    public ArrayList<Source> getOrderedSources() {
-        // Using a LinkedHashSet to get the sources in the order added while
-        // avoiding duplicates.
-        LinkedHashSet<Source> orderedSources = new LinkedHashSet<Source>();
-        if (mSources.areWebSuggestionsEnabled()) {
-            // Add web search source first, so that it's always queried first,
-            // to do network traffic while the rest are using the CPU.
-            Source webSource = mSources.getWebSearchSource();
-            if (webSource != null) {
-                orderedSources.add(webSource);
-            }
-        }
-        // Then add all ranked sources
-        ArrayList<ComponentName> rankedSources = mShortcutRepo.getSourceRanking();
-        if (DBG) Log.d(TAG, "Ranked sources: " + rankedSources);
-        for (ComponentName sourceName : rankedSources) {
-            Source source = mSources.getSourceByComponentName(sourceName);
-            if (source != null && mSources.isEnabledSource(source)) {
-                orderedSources.add(source);
-            }
-        }
-        // Last, add all unranked enabled sources.
-        orderedSources.addAll(mSources.getEnabledSources());
-        if (DBG) Log.d(TAG, "All sources ordered " + orderedSources);
-        return new ArrayList<Source>(orderedSources);
+    public ArrayList<Corpus> getOrderedCorpora() {
+        return mCorpusRanker.rankCorpora(mCorpora.getEnabledCorpora());
     }
 
     @Override
diff --git a/src/com/android/quicksearchbox/Launcher.java b/src/com/android/quicksearchbox/Launcher.java
index b280e38..cc43ced 100644
--- a/src/com/android/quicksearchbox/Launcher.java
+++ b/src/com/android/quicksearchbox/Launcher.java
@@ -24,8 +24,6 @@
 import android.os.Bundle;
 import android.speech.RecognizerIntent;
 import android.util.Log;
-import android.webkit.URLUtil;
-import com.android.common.Patterns;
 
 /**
  * Launches suggestions and searches.
@@ -42,8 +40,6 @@
 
     /**
      * Data sent by the app that launched QSB.
-     *
-     * @param appSearchData
      */
     public Launcher(Context context) {
         mContext = context;
@@ -53,106 +49,75 @@
         mAppSearchData = appSearchData;
     }
 
-    public boolean isVoiceSearchAvailable() {
-        Intent intent = createVoiceSearchIntent();
-        ResolveInfo ri = mContext.getPackageManager().
+    public static boolean shouldShowVoiceSearch(Context context, Corpus corpus) {
+        if (corpus != null && !corpus.voiceSearchEnabled()) {
+            return false;
+        }
+        Intent intent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
+        ResolveInfo ri = context.getPackageManager().
                 resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
         return ri != null;
     }
 
-    public void startVoiceSearch() {
-        launchIntent(createVoiceSearchIntent());
-    }
-
-    public void startSearch(Source source, String query) {
-        if (source == null) {
-            startWebSearch(query);
+    public void startVoiceSearch(Corpus corpus) {
+        if (corpus == null) {
+            launchIntent(WebCorpus.createVoiceWebSearchIntent(mAppSearchData));
         } else {
-            Intent intent = createSourceSearchIntent(source, query);
-            launchIntent(intent);
+            launchIntent(corpus.createVoiceSearchIntent(mAppSearchData));
         }
     }
 
-    /**
-     * Launches a web search.
-     */
-    public void startWebSearch(String query)  {
-        Intent intent = Patterns.WEB_URL.matcher(query).matches()
-                ? createBrowseIntent(query)
-                : createWebSearchIntent(query);
-        if (intent != null) {
-            launchIntent(intent);
+    public void startSearch(Corpus corpus, String query) {
+        if (corpus == null) {
+            launchIntent(WebCorpus.createWebIntent(query, mAppSearchData));
+        } else {
+            launchIntent(corpus.createSearchIntent(query, mAppSearchData));
         }
     }
 
-    private Intent createVoiceSearchIntent() {
-        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);
-        // TODO: Should we include SearchManager.APP_DATA in the voice search intent?
-        // SearchDialog doesn't seem to, but it would make sense.
-        return intent;
-    }
-
-    // TODO: not all apps handle ACTION_SEARCH properly, e.g. ApplicationsProvider.
-    // Maybe we should add a flag to searchable, so that QSB can hide the search button?
-    private Intent createSourceSearchIntent(Source source, String query) {
-        Intent intent = new Intent(Intent.ACTION_SEARCH);
-        intent.setComponent(source.getComponentName());
-        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 (mAppSearchData != null) {
-            intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
-        }
-        return intent;
-    }
-
-    private Intent createWebSearchIntent(String query) {
-        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 (mAppSearchData != null) {
-            intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
-        }
-        // 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 Intent createBrowseIntent(String url) {
-        Intent intent = new Intent(Intent.ACTION_VIEW);
-        intent.addCategory(Intent.CATEGORY_BROWSABLE);
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        url = URLUtil.guessUrl(url);
-        intent.setData(Uri.parse(url));
-        return intent;
-    }
-
     /**
      * Launches a suggestion.
      */
-    public void launchSuggestion(SuggestionPosition suggestionPos,
-            int actionKey, String actionMsg) {
-        SuggestionCursor suggestion = suggestionPos.getSuggestion();
-        Intent intent = suggestion.getSuggestionIntent(mContext, mAppSearchData,
-                actionKey, actionMsg);
-        if (intent != null) {
-            launchIntent(intent);
+    public void launchSuggestion(SuggestionCursor cursor, int position) {
+        launchIntent(getSuggestionIntent(cursor, position));
+    }
+
+    public Intent getSuggestionIntent(SuggestionCursor cursor, int position) {
+        cursor.moveTo(position);
+        String action = cursor.getSuggestionIntentAction();
+        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 (mAppSearchData != null) {
+            intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
+        }
+
+        intent.setComponent(cursor.getSuggestionSource().getComponentName());
+        return intent;
     }
 
     private void launchIntent(Intent intent) {
+        if (intent == null) {
+            return;
+        }
         try {
             mContext.startActivity(intent);
         } catch (RuntimeException ex) {
diff --git a/src/com/android/quicksearchbox/ListSuggestionCursor.java b/src/com/android/quicksearchbox/ListSuggestionCursor.java
index 55d7d64..db9dd42 100644
--- a/src/com/android/quicksearchbox/ListSuggestionCursor.java
+++ b/src/com/android/quicksearchbox/ListSuggestionCursor.java
@@ -16,13 +16,8 @@
 
 package com.android.quicksearchbox;
 
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Bundle;
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
 
 import java.util.ArrayList;
 
@@ -34,6 +29,8 @@
  */
 public class ListSuggestionCursor extends AbstractSuggestionCursor {
 
+    private final DataSetObservable mDataSetObservable = new DataSetObservable();
+
     private final ArrayList<SuggestionPosition> mSuggestions;
 
     private int mPos;
@@ -57,11 +54,6 @@
 
     public void close() {
         mSuggestions.clear();
-        super.close();
-    }
-
-    public boolean isFailed() {
-        return false;
     }
 
     public int getPosition() {
@@ -84,42 +76,37 @@
         return mSuggestions.size();
     }
 
-    private SuggestionCursor current() {
-        return mSuggestions.get(mPos).getSuggestion();
+    protected SuggestionCursor current() {
+        return mSuggestions.get(mPos).current();
     }
 
-    public Drawable getIcon(String iconId) {
-        return current().getIcon(iconId);
+    /**
+     * Register an observer that is called when changes happen to this data set.
+     *
+     * @param observer gets notified when the data set changes.
+     */
+    public void registerDataSetObserver(DataSetObserver observer) {
+        mDataSetObservable.registerObserver(observer);
     }
 
-    public Uri getIconUri(String iconId) {
-        return current().getIconUri(iconId);
+    /**
+     * Unregister an observer that has previously been registered with 
+     * {@link #registerDataSetObserver(DataSetObserver)}
+     *
+     * @param observer the observer to unregister.
+     */
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        mDataSetObservable.unregisterObserver(observer);
+    }
+
+    protected void notifyDataSetChanged() {
+        mDataSetObservable.notifyChanged();
     }
 
     public String getShortcutId() {
         return current().getShortcutId();
     }
 
-    public ComponentName getSourceComponentName() {
-        return current().getSourceComponentName();
-    }
-
-    public String getLogName() {
-        return current().getLogName();
-    }
-
-    public Drawable getSourceIcon() {
-        return current().getSourceIcon();
-    }
-
-    public Uri getSourceIconUri() {
-        return current().getSourceIconUri();
-    }
-
-    public CharSequence getSourceLabel() {
-        return current().getSourceLabel();
-    }
-
     public String getSuggestionDisplayQuery() {
         return current().getSuggestionDisplayQuery();
     }
@@ -136,21 +123,32 @@
         return current().getSuggestionIcon2();
     }
 
-    public boolean isSpinnerWhileRefreshing() {
-        return current().isSpinnerWhileRefreshing();
+    public String getSuggestionIntentAction() {
+        return current().getSuggestionIntentAction();
     }
 
-    public Intent getSuggestionIntent(Context context, Bundle appSearchData, int actionKey,
-            String actionMsg) {
-        return current().getSuggestionIntent(context, appSearchData, actionKey, actionMsg);
+    public String getSuggestionIntentDataString() {
+        return current().getSuggestionIntentDataString();
     }
 
     public String getSuggestionIntentExtraData() {
         return current().getSuggestionIntentExtraData();
     }
 
-    public String getSuggestionIntentDataString() {
-        return current().getSuggestionIntentDataString();
+    public String getSuggestionKey() {
+        return current().getSuggestionKey();
+    }
+
+    public String getSuggestionLogType() {
+        return current().getSuggestionLogType();
+    }
+
+    public String getSuggestionQuery() {
+        return current().getSuggestionQuery();
+    }
+
+    public Source getSuggestionSource() {
+        return current().getSuggestionSource();
     }
 
     public String getSuggestionText1() {
@@ -161,12 +159,7 @@
         return current().getSuggestionText2();
     }
 
-    public String getSuggestionKey() {
-        return current().getSuggestionKey();
+    public boolean isSpinnerWhileRefreshing() {
+        return current().isSpinnerWhileRefreshing();
     }
-
-    public String getActionKeyMsg(int keyCode) {
-        return current().getActionKeyMsg(keyCode);
-    }
-
 }
diff --git a/src/com/android/quicksearchbox/ListSuggestionCursorNoDuplicates.java b/src/com/android/quicksearchbox/ListSuggestionCursorNoDuplicates.java
index 37fb58e..7925766 100644
--- a/src/com/android/quicksearchbox/ListSuggestionCursorNoDuplicates.java
+++ b/src/com/android/quicksearchbox/ListSuggestionCursorNoDuplicates.java
@@ -23,14 +23,13 @@
 /**
  * A SuggestionCursor that is backed by a list of SuggestionPosition objects
  * and doesn't allow duplicate suggestions.
- *
  */
 public class ListSuggestionCursorNoDuplicates extends ListSuggestionCursor {
 
     private static final boolean DBG = true;
     private static final String TAG = "QSB.ListSuggestionCursorNoDuplicates";
 
-    private HashSet<String> mSuggestionKeys;
+    private final HashSet<String> mSuggestionKeys;
 
     public ListSuggestionCursorNoDuplicates(String userQuery) {
         super(userQuery);
@@ -39,8 +38,7 @@
 
     @Override
     public boolean add(SuggestionPosition suggestionPos) {
-        SuggestionCursor suggestion = suggestionPos.getSuggestion();
-        String key = suggestion.getSuggestionKey();
+        String key = suggestionPos.current().getSuggestionKey();
         if (mSuggestionKeys.add(key)) {
             return super.add(suggestionPos);
         } else {
diff --git a/src/com/android/quicksearchbox/Logger.java b/src/com/android/quicksearchbox/Logger.java
index 47798b8..8716768 100644
--- a/src/com/android/quicksearchbox/Logger.java
+++ b/src/com/android/quicksearchbox/Logger.java
@@ -32,42 +32,40 @@
      *
      * @param latency User-visible start-up latency in milliseconds.
      */
-    void logStart(int latency, String intentSource, Source currentSearchSource,
-            ArrayList<Source> orderedSources);
+    void logStart(int latency, String intentSource, Corpus corpus,
+            ArrayList<Corpus> orderedCorpora);
 
     /**
      * Called when a suggestion is clicked.
      *
      * @param position 0-based position of the suggestion in the UI.
      * @param suggestionCursor all the suggestions shown in the UI.
-     * @param queriedSources all sources that were queried to produce the suggestions in
+     * @param queriedCorpora all corpora that were queried to produce the suggestions in
      *        {@code suggestionCursor}, ordered by rank.
      */
     void logSuggestionClick(int position, SuggestionCursor suggestionCursor,
-            ArrayList<Source> queriedSources);
+            ArrayList<Corpus> queriedCorpora);
 
     /**
      * The user launched a search.
      *
-     * @param searchSource The search source. {@code null} means web search.
      * @param startMethod One of {@link #SEARCH_METHOD_BUTTON} or {@link #SEARCH_METHOD_KEYBOARD}.
      * @param numChars The number of characters in the query.
      */
-    void logSearch(Source searchSource, int startMethod, int numChars);
+    void logSearch(Corpus corpus, int startMethod, int numChars);
 
     /**
      * The user launched a voice search.
-     *
-     * @param searchSource The search source. {@code null} means web search.
      */
-    void logVoiceSearch(Source searchSource);
+    void logVoiceSearch(Corpus corpus);
 
     /**
      * The user left QSB without performing any action (click suggestions, search or voice search).
      *
      * @param suggestionCursor all the suggestions shown in the UI when the user left
+     * @param numChars The number of characters in the query typed when the user left.
      */
-    void logExit(SuggestionCursor suggestionCursor);
+    void logExit(SuggestionCursor suggestionCursor, int numChars);
 
     void logWebLatency();
 
diff --git a/src/com/android/quicksearchbox/PackageIconLoader.java b/src/com/android/quicksearchbox/PackageIconLoader.java
index bc78daa..28facd7 100644
--- a/src/com/android/quicksearchbox/PackageIconLoader.java
+++ b/src/com/android/quicksearchbox/PackageIconLoader.java
@@ -34,9 +34,8 @@
 /**
  * Loads icons from other packages.
  *
- * Code partly stolen from {@link ContentResolver} and {@link android.app.SuggestionsAdapter}.
- *
- */
+ * Code partly stolen from {@link ContentResolver} and android.app.SuggestionsAdapter.
+  */
 public class PackageIconLoader implements IconLoader {
 
     private static final boolean DBG = false;
diff --git a/src/com/android/quicksearchbox/Promoter.java b/src/com/android/quicksearchbox/Promoter.java
index 4200f4e..ea25dd3 100644
--- a/src/com/android/quicksearchbox/Promoter.java
+++ b/src/com/android/quicksearchbox/Promoter.java
@@ -33,7 +33,7 @@
      * @param promoted List to add the promoted suggestions to.
      */
     void pickPromoted(SuggestionCursor shortcuts,
-            ArrayList<SuggestionCursor> suggestions, int maxPromoted,
+            ArrayList<CorpusResult> suggestions, int maxPromoted,
             ListSuggestionCursor promoted);
 
 }
diff --git a/src/com/android/quicksearchbox/QsbApplication.java b/src/com/android/quicksearchbox/QsbApplication.java
index 444cb96..0c2ff9e 100644
--- a/src/com/android/quicksearchbox/QsbApplication.java
+++ b/src/com/android/quicksearchbox/QsbApplication.java
@@ -29,17 +29,15 @@
 
 public class QsbApplication extends Application {
 
-    private static final String TAG ="QSB.QsbApplication";
-
     private Handler mUiThreadHandler;
     private Config mConfig;
-    private Sources mSources;
+    private SearchableCorpora mCorpora;
+    private CorpusRanker mCorpusRanker;
     private ShortcutRepository mShortcutRepository;
     private ShortcutRefresher mShortcutRefresher;
     private SourceTaskExecutor mSourceTaskExecutor;
     private SuggestionsProvider mGlobalSuggestionsProvider;
     private SuggestionViewFactory mSuggestionViewFactory;
-    private SourceFactory mSourceFactory;
     private Logger mLogger;
 
     @Override
@@ -48,14 +46,22 @@
         super.onTerminate();
     }
 
+    protected void checkThread() {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            throw new IllegalStateException("Accessed Application object from thread "
+                    + Thread.currentThread().getName());
+        }
+    }
+
     protected void close() {
+        checkThread();
         if (mConfig != null) {
             mConfig.close();
             mConfig = null;
         }
-        if (mSources != null) {
-            mSources.close();
-            mSources = null;
+        if (mCorpora != null) {
+            mCorpora.close();
+            mCorpora = null;
         }
         if (mShortcutRepository != null) {
             mShortcutRepository.close();
@@ -71,18 +77,18 @@
         }
     }
 
-    public Handler getUiThreadHandler() {
+    public synchronized Handler getMainThreadHandler() {
         if (mUiThreadHandler == null) {
-            mUiThreadHandler = createUiThreadHandler();
+            mUiThreadHandler = new Handler(Looper.getMainLooper());
         }
         return mUiThreadHandler;
     }
 
-    protected Handler createUiThreadHandler() {
-        return new Handler(Looper.myLooper());
-    }
-
-    public Config getConfig() {
+    /**
+     * Gets the QSB configuration object.
+     * May be called from any thread.
+     */
+    public synchronized Config getConfig() {
         if (mConfig == null) {
             mConfig = createConfig();
         }
@@ -93,27 +99,63 @@
         return new Config(this);
     }
 
-    public SourceLookup getSources() {
-        if (mSources == null) {
-            mSources = createSources();
+    /**
+     * Gets the corpora.
+     * May only be called from the main thread.
+     */
+    public Corpora getCorpora() {
+        checkThread();
+        if (mCorpora == null) {
+            mCorpora = createCorpora();
         }
-        return mSources;
+        return mCorpora;
     }
 
-    protected Sources createSources() {
-        Sources sources = new Sources(this, getConfig(), getSourceFactory());
-        sources.load();
-        return sources;
+    protected SearchableCorpora createCorpora() {
+        SearchableCorpora corpora = new SearchableCorpora(this, getConfig(), getMainThreadHandler());
+        corpora.load();
+        return corpora;
     }
 
+    /**
+     * Gets the corpus ranker.
+     * May only be called from the main thread.
+     */
+    public CorpusRanker getCorpusRanker() {
+        checkThread();
+        if (mCorpusRanker == null) {
+            mCorpusRanker = createCorpusRanker();
+        }
+        return mCorpusRanker;
+    }
+
+    protected CorpusRanker createCorpusRanker() {
+        return new DefaultCorpusRanker(getShortcutRepository());
+    }
+
+    /**
+     * Gets the shortcut repository.
+     * May only be called from the main thread.
+     */
     public ShortcutRepository getShortcutRepository() {
+        checkThread();
         if (mShortcutRepository == null) {
             mShortcutRepository = createShortcutRepository();
         }
         return mShortcutRepository;
     }
 
+    protected ShortcutRepository createShortcutRepository() {
+        return ShortcutRepositoryImplLog.create(this, getConfig(), getCorpora(),
+            getShortcutRefresher(), getMainThreadHandler());
+    }
+
+    /**
+     * Gets the shortcut refresher.
+     * May only be called from the main thread.
+     */
     public ShortcutRefresher getShortcutRefresher() {
+        checkThread();
         if (mShortcutRefresher == null) {
             mShortcutRefresher = createShortcutRefresher();
         }
@@ -122,15 +164,15 @@
 
     protected ShortcutRefresher createShortcutRefresher() {
         // For now, ShortcutRefresher gets its own SourceTaskExecutor
-        return new ShortcutRefresher(createSourceTaskExecutor(), getSources());
+        return new ShortcutRefresher(createSourceTaskExecutor());
     }
 
-    protected ShortcutRepository createShortcutRepository() {
-        return ShortcutRepositoryImplLog.create(this, getConfig(), getSources(),
-            getShortcutRefresher(), getUiThreadHandler());
-    }
-
+    /**
+     * Gets the source task executor.
+     * May only be called from the main thread.
+     */
     public SourceTaskExecutor getSourceTaskExecutor() {
+        checkThread();
         if (mSourceTaskExecutor == null) {
             mSourceTaskExecutor = createSourceTaskExecutor();
         }
@@ -144,28 +186,33 @@
         return new DelayingSourceTaskExecutor(config, queryThreadFactory);
     }
 
-
-    public SuggestionsProvider getSuggestionsProvider(Source source) {
-        if (source == null) {
+    /**
+     * Gets the suggestion provider for a corpus.
+     * May only be called from the main thread.
+     */
+    public SuggestionsProvider getSuggestionsProvider(Corpus corpus) {
+        checkThread();
+        if (corpus == null) {
             return getGlobalSuggestionsProvider();
         }
         // TODO: Cache this to avoid creating a new one for each key press
-        return createSuggestionsProvider(source);
+        return createSuggestionsProvider(corpus);
     }
 
-    protected SuggestionsProvider createSuggestionsProvider(Source source) {
+    protected SuggestionsProvider createSuggestionsProvider(Corpus corpus) {
         // TODO: We could use simpler promoter here
         Promoter promoter =  new ShortcutPromoter(new RoundRobinPromoter());
-        SingleSourceSuggestionsProvider provider = new SingleSourceSuggestionsProvider(getConfig(),
-                source,
+        SingleCorpusSuggestionsProvider provider = new SingleCorpusSuggestionsProvider(getConfig(),
+                corpus,
                 getSourceTaskExecutor(),
-                getUiThreadHandler(),
+                getMainThreadHandler(),
                 promoter,
                 getShortcutRepository());
         return provider;
     }
 
-    public SuggestionsProvider getGlobalSuggestionsProvider() {
+    protected SuggestionsProvider getGlobalSuggestionsProvider() {
+        checkThread();
         if (mGlobalSuggestionsProvider == null) {
             mGlobalSuggestionsProvider = createGlobalSuggestionsProvider();
         }
@@ -173,18 +220,23 @@
     }
 
     protected SuggestionsProvider createGlobalSuggestionsProvider() {
-        Handler uiThread = new Handler(Looper.myLooper());
         Promoter promoter =  new ShortcutPromoter(new RoundRobinPromoter());
         GlobalSuggestionsProvider provider = new GlobalSuggestionsProvider(getConfig(),
-                getSources(),
+                getCorpora(),
                 getSourceTaskExecutor(),
-                uiThread,
+                getMainThreadHandler(),
                 promoter,
+                getCorpusRanker(),
                 getShortcutRepository());
         return provider;
     }
 
+    /**
+     * Gets the suggestion view factory.
+     * May only be called from the main thread.
+     */
     public SuggestionViewFactory getSuggestionViewFactory() {
+        checkThread();
         if (mSuggestionViewFactory == null) {
             mSuggestionViewFactory = createSuggestionViewFactory();
         }
@@ -195,6 +247,10 @@
         return new SuggestionViewInflater(this);
     }
 
+    /**
+     * Creates a suggestions adapter.
+     * May only be called from the main thread.
+     */
     public SuggestionsAdapter createSuggestionsAdapter() {
         Config config = getConfig();
         SuggestionViewFactory viewFactory = getSuggestionViewFactory();
@@ -202,18 +258,12 @@
         return adapter;
     }
 
-    public SourceFactory getSourceFactory() {
-        if (mSourceFactory == null) {
-            mSourceFactory = createSourceFactory();
-        }
-        return mSourceFactory;
-    }
-
-    protected SourceFactory createSourceFactory() {
-        return new SearchableSourceFactory(this);
-    }
-
+    /**
+     * Gets the event logger.
+     * May only be called from the main thread.
+     */
     public Logger getLogger() {
+        checkThread();
         if (mLogger == null) {
             mLogger = createLogger();
         }
diff --git a/src/com/android/quicksearchbox/SourceFactory.java b/src/com/android/quicksearchbox/ReverseComparator.java
similarity index 68%
copy from src/com/android/quicksearchbox/SourceFactory.java
copy to src/com/android/quicksearchbox/ReverseComparator.java
index e661498..4b611f7 100644
--- a/src/com/android/quicksearchbox/SourceFactory.java
+++ b/src/com/android/quicksearchbox/ReverseComparator.java
@@ -16,14 +16,12 @@
 
 package com.android.quicksearchbox;
 
-import android.app.SearchableInfo;
+import java.util.Comparator;
 
-public interface SourceFactory {
+public class ReverseComparator<T extends Comparable<T>> implements Comparator<T> {
 
-    // TODO: perhaps SearchManager.getSearchablesInGlobalSearch()
-    // should return List<ComponentName>, so we could use ComponentName here?
-    Source createSource(SearchableInfo searchable);
-
-    Source createWebSearchSource();
+    public int compare(T o1, T o2) {
+        return o2.compareTo(o1);
+    }
 
 }
diff --git a/src/com/android/quicksearchbox/RoundRobinPromoter.java b/src/com/android/quicksearchbox/RoundRobinPromoter.java
index 1ad4cf7..3728144 100644
--- a/src/com/android/quicksearchbox/RoundRobinPromoter.java
+++ b/src/com/android/quicksearchbox/RoundRobinPromoter.java
@@ -36,7 +36,7 @@
     }
 
     public void pickPromoted(SuggestionCursor shortcuts,
-            ArrayList<SuggestionCursor> suggestions, int maxPromoted,
+            ArrayList<CorpusResult> suggestions, int maxPromoted,
             ListSuggestionCursor promoted) {
         if (DBG) Log.d(TAG, "pickPromoted(maxPromoted = " + maxPromoted + ")");
         final int sourceCount = suggestions.size();
diff --git a/src/com/android/quicksearchbox/SearchActivity.java b/src/com/android/quicksearchbox/SearchActivity.java
index 315b659..c309851 100644
--- a/src/com/android/quicksearchbox/SearchActivity.java
+++ b/src/com/android/quicksearchbox/SearchActivity.java
@@ -17,7 +17,7 @@
 package com.android.quicksearchbox;
 
 import com.android.common.Search;
-import com.android.quicksearchbox.ui.SearchSourceSelector;
+import com.android.quicksearchbox.ui.CorpusIndicator;
 import com.android.quicksearchbox.ui.SuggestionClickListener;
 import com.android.quicksearchbox.ui.SuggestionSelectionListener;
 import com.android.quicksearchbox.ui.SuggestionViewFactory;
@@ -27,11 +27,11 @@
 import android.app.Activity;
 import android.app.Dialog;
 import android.app.SearchManager;
-import android.content.ComponentName;
 import android.content.Intent;
 import android.database.DataSetObserver;
 import android.graphics.drawable.Animatable;
 import android.graphics.drawable.Drawable;
+import android.net.Uri;
 import android.os.Bundle;
 import android.os.SystemClock;
 import android.text.Editable;
@@ -57,15 +57,17 @@
     private static final boolean DBG = true;
     private static final String TAG = "QSB.SearchActivity";
 
-    public static final String INTENT_ACTION_QSB_AND_SELECT_SEARCH_SOURCE
-            = "com.android.quicksearchbox.action.QSB_AND_SELECT_SEARCH_SOURCE";
+    private static final String SCHEME_CORPUS = "qsb.corpus";
+
+    public static final String INTENT_ACTION_QSB_AND_SELECT_CORPUS
+            = "com.android.quicksearchbox.action.QSB_AND_SELECT_CORPUS";
 
     // Keys for the saved instance state.
-    private static final String INSTANCE_KEY_SOURCE = "source";
+    private static final String INSTANCE_KEY_CORPUS = "corpus";
     private static final String INSTANCE_KEY_USER_QUERY = "query";
 
     // Dialog IDs
-    private static final int DIALOG_SOURCE_SELECTOR = 0;
+    private static final int CORPUS_SELECTION_DIALOG = 0;
 
     // Timestamp for last onCreate()/onNewIntent() call, as returned by SystemClock.uptimeMillis().
     private long mStartTime;
@@ -83,11 +85,11 @@
 
     protected ImageButton mSearchGoButton;
     protected ImageButton mVoiceSearchButton;
-    protected SearchSourceSelector mSourceSelector;
+    protected CorpusIndicator mCorpusIndicator;
 
     private Launcher mLauncher;
 
-    private Source mSource;
+    private Corpus mCorpus;
     private Bundle mAppSearchData;
     private boolean mUpdateSuggestions;
     private String mUserQuery;
@@ -114,18 +116,15 @@
 
         mSearchGoButton = (ImageButton) findViewById(R.id.search_go_btn);
         mVoiceSearchButton = (ImageButton) findViewById(R.id.search_voice_btn);
-        mSourceSelector = new SearchSourceSelector(findViewById(R.id.search_source_selector));
+        mCorpusIndicator = new CorpusIndicator(findViewById(R.id.corpus_indicator));
 
         mLauncher = new Launcher(this);
-        // TODO: should this check for voice search in the current source?
-        mVoiceSearchButton.setVisibility(
-                mLauncher.isVoiceSearchAvailable() ? View.VISIBLE : View.GONE);
 
         mQueryTextView.addTextChangedListener(new SearchTextWatcher());
         mQueryTextView.setOnKeyListener(new QueryTextViewKeyListener());
         mQueryTextView.setOnFocusChangeListener(new QueryTextViewFocusListener());
 
-        mSourceSelector.setOnClickListener(new SourceSelectorClickListener());
+        mCorpusIndicator.setOnClickListener(new CorpusIndicatorClickListener());
 
         mSearchGoButton.setOnClickListener(new SearchGoButtonClickListener());
 
@@ -134,7 +133,7 @@
         ButtonsKeyListener buttonsKeyListener = new ButtonsKeyListener();
         mSearchGoButton.setOnKeyListener(buttonsKeyListener);
         mVoiceSearchButton.setOnKeyListener(buttonsKeyListener);
-        mSourceSelector.setOnKeyListener(buttonsKeyListener);
+        mCorpusIndicator.setOnKeyListener(buttonsKeyListener);
 
         mUpdateSuggestions = true;
 
@@ -164,9 +163,9 @@
 
     protected void restoreInstanceState(Bundle savedInstanceState) {
         if (savedInstanceState == null) return;
-        ComponentName sourceName = savedInstanceState.getParcelable(INSTANCE_KEY_SOURCE);
+        String corpusName = savedInstanceState.getString(INSTANCE_KEY_CORPUS);
         String query = savedInstanceState.getString(INSTANCE_KEY_USER_QUERY);
-        setSource(getSourceByComponentName(sourceName));
+        setCorpus(getCorpus(corpusName));
         setUserQuery(query);
     }
 
@@ -175,61 +174,79 @@
         super.onSaveInstanceState(outState);
         // We don't save appSearchData, since we always get the value
         // from the intent and the user can't change it.
-        outState.putParcelable(INSTANCE_KEY_SOURCE, getSourceName());
+
+        String corpusName = mCorpus == null ? null : mCorpus.getName();
+        outState.putString(INSTANCE_KEY_CORPUS, corpusName);
         outState.putString(INSTANCE_KEY_USER_QUERY, mUserQuery);
     }
 
     private void setupFromIntent(Intent intent) {
         if (DBG) Log.d(TAG, "setupFromIntent(" + intent.toUri(0) + ")");
-        ComponentName sourceName = SearchSourceSelector.getSource(intent);
+        Corpus corpus = getCorpusFromUri(intent.getData());
         String query = intent.getStringExtra(SearchManager.QUERY);
         Bundle appSearchData = intent.getBundleExtra(SearchManager.APP_DATA);
 
-        Source source = getSourceByComponentName(sourceName);
-        setSource(source);
+        setCorpus(corpus);
         setUserQuery(query);
         mSelectAll = intent.getBooleanExtra(SearchManager.EXTRA_SELECT_QUERY, false);
         setAppSearchData(appSearchData);
 
-        if (INTENT_ACTION_QSB_AND_SELECT_SEARCH_SOURCE.equals(intent.getAction())) {
+        if (INTENT_ACTION_QSB_AND_SELECT_CORPUS.equals(intent.getAction())) {
             showSourceSelectorDialog();
         }
     }
 
-    private Source getSourceByComponentName(ComponentName sourceName) {
+    public static Uri getCorpusUri(Corpus corpus) {
+        if (corpus == null) return null;
+        return new Uri.Builder()
+                .scheme(SCHEME_CORPUS)
+                .authority(corpus.getName())
+                .build();
+    }
+
+    private Corpus getCorpusFromUri(Uri uri) {
+        if (uri == null) return null;
+        if (!SCHEME_CORPUS.equals(uri.getScheme())) return null;
+        String name = uri.getAuthority();
+        return getCorpus(name);
+    }
+
+    private Corpus getCorpus(String sourceName) {
         if (sourceName == null) return null;
-        Source source = getSources().getSourceByComponentName(sourceName);
-        if (source == null) {
-            Log.w(TAG, "Unknown source " + sourceName);
+        Corpus corpus = getCorpora().getCorpus(sourceName);
+        if (corpus == null) {
+            Log.w(TAG, "Unknown corpus " + sourceName);
             return null;
         }
-        return source;
+        return corpus;
     }
 
-    private void setSource(Source source) {
-        if (DBG) Log.d(TAG, "setSource(" + source + ")");
-        mSource = source;
+    private void setCorpus(Corpus corpus) {
+        if (DBG) Log.d(TAG, "setCorpus(" + corpus + ")");
+        mCorpus = corpus;
         Drawable sourceIcon;
-        if (source == null) {
+        if (corpus == null) {
             sourceIcon = getSuggestionViewFactory().getGlobalSearchIcon();
         } else {
-            sourceIcon = source.getSourceIcon();
+            sourceIcon = corpus.getCorpusIcon();
         }
-        ComponentName sourceName = getSourceName();
-        mSuggestionsAdapter.setSource(sourceName);
-        mSourceSelector.setSourceIcon(sourceIcon);
-    }
+        mSuggestionsAdapter.setCorpus(corpus);
+        mCorpusIndicator.setSourceIcon(sourceIcon);
 
-    private ComponentName getSourceName() {
-        return mSource == null ? null : mSource.getComponentName();
+        boolean enableVoiceSearch = Launcher.shouldShowVoiceSearch(this, mCorpus);
+        mVoiceSearchButton.setVisibility(enableVoiceSearch ? View.VISIBLE : View.GONE);
     }
 
     private QsbApplication getQsbApplication() {
         return (QsbApplication) getApplication();
     }
 
-    private SourceLookup getSources() {
-        return getQsbApplication().getSources();
+    private Corpora getCorpora() {
+        return getQsbApplication().getCorpora();
+    }
+
+    private CorpusRanker getCorpusRanker() {
+        return getQsbApplication().getCorpusRanker();
     }
 
     private ShortcutRepository getShortcutRepository() {
@@ -237,7 +254,7 @@
     }
 
     private SuggestionsProvider getSuggestionsProvider() {
-        return getQsbApplication().getSuggestionsProvider(mSource);
+        return getQsbApplication().getSuggestionsProvider(mCorpus);
     }
 
     private SuggestionViewFactory getSuggestionViewFactory() {
@@ -280,8 +297,8 @@
             // SystemClock.uptimeMillis() does not advance during deep sleep.
             int latency = (int) (SystemClock.uptimeMillis() - mStartTime);
             String source = getIntent().getStringExtra(Search.SOURCE);
-            getLogger().logStart(latency, source, mSource,
-                    getSuggestionsProvider().getOrderedSources());
+            getLogger().logStart(latency, source, mCorpus,
+                    getSuggestionsProvider().getOrderedCorpora());
         }
     }
 
@@ -357,15 +374,15 @@
     }
 
     protected void showSourceSelectorDialog() {
-        showDialog(DIALOG_SOURCE_SELECTOR);
+        showDialog(CORPUS_SELECTION_DIALOG);
     }
 
 
     @Override
     protected Dialog onCreateDialog(int id, Bundle args) {
         switch (id) {
-            case DIALOG_SOURCE_SELECTOR:
-                return createSourceSelectorDialog();
+            case CORPUS_SELECTION_DIALOG:
+                return createCorpusSelectionDialog();
             default:
                 throw new IllegalArgumentException("Unknown dialog: " + id);
         }
@@ -374,22 +391,22 @@
     @Override
     protected void onPrepareDialog(int id, Dialog dialog, Bundle args) {
         switch (id) {
-            case DIALOG_SOURCE_SELECTOR:
-                prepareSourceSelectorDialog((SelectSearchSourceDialog) dialog);
+            case CORPUS_SELECTION_DIALOG:
+                prepareCorpusSelectionDialog((CorpusSelectionDialog) dialog);
                 break;
             default:
                 throw new IllegalArgumentException("Unknown dialog: " + id);
         }
     }
 
-    protected SelectSearchSourceDialog createSourceSelectorDialog() {
-        SelectSearchSourceDialog dialog = new SelectSearchSourceDialog(this);
+    protected CorpusSelectionDialog createCorpusSelectionDialog() {
+        CorpusSelectionDialog dialog = new CorpusSelectionDialog(this);
         dialog.setOwnerActivity(this);
         return dialog;
     }
 
-    protected void prepareSourceSelectorDialog(SelectSearchSourceDialog dialog) {
-        dialog.setSource(getSourceName());
+    protected void prepareCorpusSelectionDialog(CorpusSelectionDialog dialog) {
+        dialog.setCorpus(mCorpus);
         dialog.setQuery(getQuery());
         dialog.setAppData(mAppSearchData);
     }
@@ -398,70 +415,46 @@
         String query = getQuery();
         if (DBG) Log.d(TAG, "Search clicked, query=" + query);
         mTookAction = true;
-        getLogger().logSearch(mSource, method, query.length());
-        mLauncher.startSearch(mSource, query);
+        getLogger().logSearch(mCorpus, method, query.length());
+        mLauncher.startSearch(mCorpus, query);
     }
 
     protected void onVoiceSearchClicked() {
         if (DBG) Log.d(TAG, "Voice Search clicked");
         mTookAction = true;
-        getLogger().logVoiceSearch(mSource);
+        getLogger().logVoiceSearch(mCorpus);
 
         // TODO: should this start voice search in the current source?
-        mLauncher.startVoiceSearch();
+        mLauncher.startVoiceSearch(mCorpus);
     }
 
-    protected boolean launchSuggestion(SuggestionPosition suggestion) {
-        return launchSuggestion(suggestion, KeyEvent.KEYCODE_UNKNOWN, null);
+    protected SuggestionCursor getSuggestions() {
+        return mSuggestionsAdapter.getCurrentSuggestions();
     }
 
-    protected boolean launchSuggestion(SuggestionPosition suggestion,
-            int actionKey, String actionMsg) {
-        if (DBG) Log.d(TAG, "Launching suggestion " + suggestion);
+    protected boolean launchSuggestion(int position) {
+        if (DBG) Log.d(TAG, "Launching suggestion " + position);
         mTookAction = true;
-        SuggestionCursor suggestions = mSuggestionsAdapter.getCurrentSuggestions();
+        SuggestionCursor suggestions = getSuggestions();
         // TODO: This should be just the queried sources, but currently
         // all sources are queried
-        ArrayList<Source> sources = getSuggestionsProvider().getOrderedSources();
-        getLogger().logSuggestionClick(suggestion.getPosition(), suggestions, sources);
+        ArrayList<Corpus> corpora = getCorpusRanker().rankCorpora(getCorpora().getEnabledCorpora());
+        getLogger().logSuggestionClick(position, suggestions, corpora);
 
-        mLauncher.launchSuggestion(suggestion, actionKey, actionMsg);
-        getShortcutRepository().reportClick(suggestion);
+        mLauncher.launchSuggestion(suggestions, position);
+        getShortcutRepository().reportClick(suggestions, position);
         return true;
     }
 
-    protected boolean onSuggestionLongClicked(SuggestionPosition suggestion) {
-        SuggestionCursor sourceResult = suggestion.getSuggestion();
-        if (DBG) Log.d(TAG, "Long clicked on suggestion " + sourceResult.getSuggestionText1());
+    protected boolean onSuggestionLongClicked(int position) {
+        if (DBG) Log.d(TAG, "Long clicked on suggestion " + position);
         return false;
     }
 
-    protected void onSuggestionSelected(SuggestionPosition suggestion) {
-        if (suggestion == null) {
-            // This happens when a suggestion has been selected with the
-            // dpad / trackball and then a different UI element is touched.
-            // Do nothing, since we want to keep the query of the selection
-            // in the search box.
-            return;
-        }
-        SuggestionCursor sourceResult = suggestion.getSuggestion();
-        String displayQuery = sourceResult.getSuggestionDisplayQuery();
-        if (DBG) {
-            Log.d(TAG, "Selected suggestion " + sourceResult.getSuggestionText1()
-                    + ",displayQuery="+ displayQuery);
-        }
-        if (TextUtils.isEmpty(displayQuery)) {
-            restoreUserQuery();
-        } else {
-            setQuery(displayQuery, false);
-        }
-    }
-
-    protected boolean onSuggestionKeyDown(SuggestionPosition suggestion,
-            int keyCode, KeyEvent event) {
+    protected boolean onSuggestionKeyDown(int position, int keyCode, KeyEvent event) {
         // Treat enter or search as a click
         if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH) {
-            return launchSuggestion(suggestion);
+            return launchSuggestion(position);
         }
 
         if (keyCode == KeyEvent.KEYCODE_DPAD_UP
@@ -484,12 +477,6 @@
             return true;
         }
 
-        // Handle source-specified action keys
-        String actionMsg = suggestion.getSuggestion().getActionKeyMsg(keyCode);
-        if (actionMsg != null) {
-            return launchSuggestion(suggestion, keyCode, actionMsg);
-        }
-
         return false;
     }
 
@@ -502,10 +489,6 @@
         return mSuggestionsView.getSelectedPosition();
     }
 
-    protected SuggestionPosition getSelectedSuggestion() {
-        return mSuggestionsView.getSelectedSuggestion();
-    }
-
     /**
      * Hides the input method.
      */
@@ -654,11 +637,9 @@
     private class SuggestionsViewKeyListener implements View.OnKeyListener {
         public boolean onKey(View v, int keyCode, KeyEvent event) {
             if (event.getAction() == KeyEvent.ACTION_DOWN) {
-                SuggestionPosition suggestion = getSelectedSuggestion();
-                if (suggestion != null) {
-                    if (onSuggestionKeyDown(suggestion, keyCode, event)) {
+                int position = getSelectedPosition();
+                if (onSuggestionKeyDown(position, keyCode, event)) {
                         return true;
-                    }
                 }
             }
             return forwardKeyToQueryTextView(keyCode, event);
@@ -672,18 +653,32 @@
     }
 
     private class ClickHandler implements SuggestionClickListener {
-       public void onSuggestionClicked(SuggestionPosition suggestion) {
-           launchSuggestion(suggestion);
+       public void onSuggestionClicked(int position) {
+           launchSuggestion(position);
        }
 
-       public boolean onSuggestionLongClicked(SuggestionPosition suggestion) {
-           return SearchActivity.this.onSuggestionLongClicked(suggestion);
+       public boolean onSuggestionLongClicked(int position) {
+           return SearchActivity.this.onSuggestionLongClicked(position);
        }
     }
 
     private class SelectionHandler implements SuggestionSelectionListener {
-        public void onSelectionChanged(SuggestionPosition suggestion) {
-            onSuggestionSelected(suggestion);
+        public void onSuggestionSelected(int position) {
+            SuggestionCursor suggestions = getSuggestions();
+            suggestions.moveTo(position);
+            String displayQuery = suggestions.getSuggestionDisplayQuery();
+            if (TextUtils.isEmpty(displayQuery)) {
+                restoreUserQuery();
+            } else {
+                setQuery(displayQuery, false);
+            }
+        }
+
+        public void onNothingSelected() {
+                // This happens when a suggestion has been selected with the
+                // dpad / trackball and then a different UI element is touched.
+                // Do nothing, since we want to keep the query of the selection
+                // in the search box.
         }
     }
 
@@ -699,7 +694,7 @@
     /**
      * Listens for clicks on the search button.
      */
-    private class SourceSelectorClickListener implements View.OnClickListener {
+    private class CorpusIndicatorClickListener implements View.OnClickListener {
         public void onClick(View view) {
             showSourceSelectorDialog();
         }
diff --git a/src/com/android/quicksearchbox/SearchSettings.java b/src/com/android/quicksearchbox/SearchSettings.java
index f9f7978..ffec0a4 100644
--- a/src/com/android/quicksearchbox/SearchSettings.java
+++ b/src/com/android/quicksearchbox/SearchSettings.java
@@ -19,10 +19,13 @@
 import android.app.AlertDialog;
 import android.app.Dialog;
 import android.app.SearchManager;
+import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
+import android.database.ContentObserver;
 import android.os.Bundle;
 import android.preference.CheckBoxPreference;
 import android.preference.Preference;
@@ -31,6 +34,7 @@
 import android.preference.PreferenceScreen;
 import android.preference.Preference.OnPreferenceChangeListener;
 import android.preference.Preference.OnPreferenceClickListener;
+import android.provider.Settings;
 import android.util.Log;
 
 import java.util.List;
@@ -45,13 +49,16 @@
     private static final boolean DBG = false;
     private static final String TAG = "SearchSettings";
 
+    // Name of the preferences file used to store search preference
+    public static final String PREFERENCES_NAME = "SearchSettings";
+
     // 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";
-    private static final String SEARCH_SOURCES_PREF = "search_sources";
+    private static final String SEARCH_CORPORA_PREF = "search_corpora";
 
-    private SourceLookup mSources;
-    private ShortcutRepository mShortcuts;
+    // Preifx of per-corpus enable preference
+    private static final String CORPUS_ENABLED_PREF_PREFIX = "enable_corpus_";
 
     // References to the top-level preference objects
     private Preference mClearShortcutsPreference;
@@ -65,9 +72,7 @@
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
-        mSources = getSources();
-        mShortcuts = getQSBApplication().getShortcutRepository();
-        getPreferenceManager().setSharedPreferencesName(Sources.PREFERENCES_NAME);
+        getPreferenceManager().setSharedPreferencesName(PREFERENCES_NAME);
 
         addPreferencesFromResource(R.xml.preferences);
 
@@ -76,7 +81,7 @@
         mSearchEngineSettingsPreference = (PreferenceScreen) preferenceScreen.findPreference(
                 SEARCH_ENGINE_SETTINGS_PREF);
         mSourcePreferences = (PreferenceGroup) getPreferenceScreen().findPreference(
-                SEARCH_SOURCES_PREF);
+                SEARCH_CORPORA_PREF);
 
         mClearShortcutsPreference.setOnPreferenceClickListener(this);
 
@@ -85,22 +90,35 @@
         populateSearchEnginePreference();
     }
 
-    @Override
-    protected void onDestroy() {
-        mShortcuts.close();
-        super.onDestroy();
+    public static boolean areWebSuggestionsEnabled(Context context) {
+        return (Settings.System.getInt(context.getContentResolver(),
+                Settings.System.SHOW_WEB_SUGGESTIONS,
+                1 /* default on until user actually changes it */) == 1);
     }
 
-    private QsbApplication getQSBApplication() {
+    /**
+     * Gets the preference key of the preference for whether the given corpus
+     * is enabled. The preference is stored in the {@link #PREFERENCES_NAME}
+     * preferences file.
+     */
+    public static String getCorpusEnabledPreference(Corpus corpus) {
+        return CORPUS_ENABLED_PREF_PREFIX + corpus.getName();
+    }
+
+    public static SharedPreferences getSearchPreferences(Context context) {
+        return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+    }
+
+    private QsbApplication getQsbApplication() {
         return (QsbApplication) getApplication();
     }
 
-    private Config getConfig() {
-        return getQSBApplication().getConfig();
+    private Corpora getCorpora() {
+        return getQsbApplication().getCorpora();
     }
 
-    private SourceLookup getSources() {
-        return getQSBApplication().getSources();
+    private ShortcutRepository getShortcuts() {
+        return getQsbApplication().getShortcutRepository();
     }
 
     /**
@@ -108,7 +126,7 @@
      * on whether there is any search history.
      */
     private void updateClearShortcutsPreference() {
-        boolean hasHistory = mShortcuts.hasHistory();
+        boolean hasHistory = getShortcuts().hasHistory();
         if (DBG) Log.d(TAG, "hasHistory()=" + hasHistory);
         mClearShortcutsPreference.setEnabled(hasHistory);
     }
@@ -144,10 +162,10 @@
      * Fills the suggestion source list.
      */
     private void populateSourcePreference() {
-        for (Source source : mSources.getSources()) {
-            Preference pref = createSourcePreference(source);
+        for (Corpus corpus : getCorpora().getAllCorpora()) {
+            Preference pref = createCorpusPreference(corpus);
             if (pref != null) {
-                if (DBG) Log.d(TAG, "Adding search source: " + source);
+                if (DBG) Log.d(TAG, "Adding corpus: " + corpus);
                 mSourcePreferences.addPreference(pref);
             }
         }
@@ -156,14 +174,14 @@
     /**
      * Adds a suggestion source to the list of suggestion source checkbox preferences.
      */
-    private Preference createSourcePreference(Source source) {
+    private Preference createCorpusPreference(Corpus corpus) {
         CheckBoxPreference sourcePref = new CheckBoxPreference(this);
-        sourcePref.setKey(Sources.getSourceEnabledPreference(source));
-        sourcePref.setDefaultValue(mSources.isTrustedSource(source));
+        sourcePref.setKey(getCorpusEnabledPreference(corpus));
+        sourcePref.setDefaultValue(getCorpora().isCorpusDefaultEnabled(corpus));
         sourcePref.setOnPreferenceChangeListener(this);
-        CharSequence label = source.getLabel();
+        CharSequence label = corpus.getLabel();
         sourcePref.setTitle(label);
-        CharSequence description = source.getSettingsDescription();
+        CharSequence description = corpus.getSettingsDescription();
         sourcePref.setSummaryOn(description);
         sourcePref.setSummaryOff(description);
         return sourcePref;
@@ -181,7 +199,7 @@
     }
 
     @Override
-    protected Dialog onCreateDialog(int id) {
+    protected Dialog onCreateDialog(int id, Bundle args) {
         switch (id) {
             case CLEAR_SHORTCUTS_CONFIRM_DIALOG:
                 return new AlertDialog.Builder(this)
@@ -190,7 +208,7 @@
                         .setPositiveButton(R.string.agree, new DialogInterface.OnClickListener() {
                             public void onClick(DialogInterface dialog, int whichButton) {
                                 if (DBG) Log.d(TAG, "Clearing history...");
-                                mShortcuts.clearHistory();
+                                getShortcuts().clearHistory();
                                 updateClearShortcutsPreference();
                             }
                         })
@@ -200,9 +218,9 @@
                 return null;
         }
     }
-    
+
     /**
-     * Informs our listeners (SuggestionSources objects) about the updated settings data.
+     * Informs our listeners about the updated settings data.
      */
     private void broadcastSettingsChanged() {
         // We use a message broadcast since the listeners could be in multiple processes.
@@ -216,4 +234,15 @@
         return true;
     }
 
+    public static void registerShowWebSuggestionsSettingObserver(
+            Context context, ContentObserver observer) {
+        context.getContentResolver().registerContentObserver(
+                Settings.System.getUriFor(Settings.System.SHOW_WEB_SUGGESTIONS),
+                false, observer);
+    }
+
+    public static void unregisterShowWebSuggestionsSettingObserver(
+            Context context, ContentObserver observer) {
+        context.getContentResolver().unregisterContentObserver(observer);
+    }
 }
diff --git a/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java b/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java
index 8a67ff5..3d38bbd 100644
--- a/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java
+++ b/src/com/android/quicksearchbox/SearchWidgetConfigActivity.java
@@ -16,12 +16,11 @@
 
 package com.android.quicksearchbox;
 
-import com.android.quicksearchbox.ui.SourcesAdapter;
+import com.android.quicksearchbox.ui.CorporaAdapter;
 import com.android.quicksearchbox.ui.SuggestionViewFactory;
 
 import android.app.Activity;
 import android.appwidget.AppWidgetManager;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
@@ -37,11 +36,11 @@
     static final String TAG = "QSB.SearchWidgetConfigActivity";
 
     private static final String PREFS_NAME = "SearchWidgetConfig";
-    private static final String WIDGET_SOURCE_PREF_PREFIX = "widget_source_";
+    private static final String WIDGET_CORPUS_PREF_PREFIX = "widget_corpus_";
 
     private int mAppWidgetId;
 
-    private GridView mSourceList;
+    private GridView mCorpusList;
 
     @Override
     public void onCreate(Bundle icicle) {
@@ -49,13 +48,13 @@
 
         setContentView(R.layout.widget_config);
 
-        mSourceList = (GridView) findViewById(R.id.widget_source_list);
-        mSourceList.setOnItemClickListener(new SourceClickListener());
+        mCorpusList = (GridView) findViewById(R.id.widget_corpus_list);
+        mCorpusList.setOnItemClickListener(new SourceClickListener());
         // TODO: for some reason, putting this in the XML layout instead makes
         // the list items unclickable.
-        mSourceList.setFocusable(true);
-        mSourceList.setAdapter(
-                new SourcesAdapter(getViewFactory(), getGlobalSuggestionsProvider()));
+        mCorpusList.setFocusable(true);
+        mCorpusList.setAdapter(new CorporaAdapter(getViewFactory(), getCorpora(),
+                getCorpusRanker()));
 
         Intent intent = getIntent();
         mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
@@ -65,9 +64,9 @@
         }
     }
 
-    protected void selectSource(Source source) {
-        writeWidgetSourcePref(mAppWidgetId, source);
-        updateWidget(source);
+    protected void selectCorpus(Corpus corpus) {
+        writeWidgetCorpusPref(mAppWidgetId, corpus);
+        updateWidget(corpus);
 
         Intent result = new Intent();
         result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
@@ -75,39 +74,42 @@
         finish();
     }
 
-    private void updateWidget(Source source) {
+    private void updateWidget(Corpus corpus) {
         AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(this);
         SearchWidgetProvider.setupSearchWidget(this, appWidgetManager,
-                mAppWidgetId, source);
+                mAppWidgetId, corpus);
     }
 
     private static SharedPreferences getWidgetPreferences(Context context) {
         return context.getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
     }
 
-    private static String getSourcePrefKey(int appWidgetId) {
-        return WIDGET_SOURCE_PREF_PREFIX + appWidgetId;
+    private static String getCorpusPrefKey(int appWidgetId) {
+        return WIDGET_CORPUS_PREF_PREFIX + appWidgetId;
     }
 
-    private void writeWidgetSourcePref(int appWidgetId, Source source) {
-        String sourceName = source == null ? null : source.getFlattenedComponentName();
+    private void writeWidgetCorpusPref(int appWidgetId, Corpus corpus) {
+        String corpusName = corpus == null ? null : corpus.getName();
         SharedPreferences.Editor prefs = getWidgetPreferences(this).edit();
-        prefs.putString(getSourcePrefKey(appWidgetId), sourceName);
+        prefs.putString(getCorpusPrefKey(appWidgetId), corpusName);
         prefs.commit();
     }
 
-    public static ComponentName readWidgetSourcePref(Context context, int appWidgetId) {
+    public static String readWidgetCorpusPref(Context context, int appWidgetId) {
         SharedPreferences prefs = getWidgetPreferences(context);
-        String sourceName = prefs.getString(getSourcePrefKey(appWidgetId), null);
-        return sourceName == null ? null : ComponentName.unflattenFromString(sourceName);
+        return prefs.getString(getCorpusPrefKey(appWidgetId), null);
     }
 
     private QsbApplication getQsbApplication() {
         return (QsbApplication) getApplication();
     }
 
-    private SuggestionsProvider getGlobalSuggestionsProvider() {
-        return getQsbApplication().getGlobalSuggestionsProvider();
+    private Corpora getCorpora() {
+        return getQsbApplication().getCorpora();
+    }
+
+    private CorpusRanker getCorpusRanker() {
+        return getQsbApplication().getCorpusRanker();
     }
 
     private SuggestionViewFactory getViewFactory() {
@@ -116,11 +118,8 @@
 
     private class SourceClickListener implements AdapterView.OnItemClickListener {
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-            Source source = (Source) parent.getItemAtPosition(position);
-            selectSource(source);
+            Corpus corpus = (Corpus) parent.getItemAtPosition(position);
+            selectCorpus(corpus);
         }
     }
 }
-
-
-
diff --git a/src/com/android/quicksearchbox/SearchWidgetProvider.java b/src/com/android/quicksearchbox/SearchWidgetProvider.java
index 7cf692d..54813a6 100644
--- a/src/com/android/quicksearchbox/SearchWidgetProvider.java
+++ b/src/com/android/quicksearchbox/SearchWidgetProvider.java
@@ -16,14 +16,13 @@
 
 package com.android.quicksearchbox;
 
-import com.android.quicksearchbox.ui.SearchSourceSelector;
+import com.android.quicksearchbox.ui.CorpusIndicator;
 import com.android.quicksearchbox.ui.SuggestionViewFactory;
 
 import android.app.PendingIntent;
 import android.app.SearchManager;
 import android.appwidget.AppWidgetManager;
 import android.appwidget.AppWidgetProvider;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
@@ -57,24 +56,21 @@
 
     private void updateSearchWidget(Context context, AppWidgetManager appWidgetManager,
             int appWidgetId) {
-        ComponentName sourceName =
-                SearchWidgetConfigActivity.readWidgetSourcePref(context, appWidgetId);
-        Source source = getSources(context).getSourceByComponentName(sourceName);
-        setupSearchWidget(context, appWidgetManager, appWidgetId, source);
+        String corpusName = SearchWidgetConfigActivity.readWidgetCorpusPref(context, appWidgetId);
+        Corpus corpus = getCorpora(context).getCorpus(corpusName);
+        setupSearchWidget(context, appWidgetManager, appWidgetId, corpus);
     }
 
     public static void setupSearchWidget(Context context, AppWidgetManager appWidgetManager,
-            int appWidgetId, Source source) {
+            int appWidgetId, Corpus corpus) {
         if (DBG) Log.d(TAG, "setupSearchWidget()");
         RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.search_widget);
 
-        ComponentName sourceName = source == null ? null : source.getComponentName();
-
         Bundle widgetAppData = new Bundle();
         widgetAppData.putString(SOURCE, WIDGET_SEARCH_SOURCE);
 
-        // Source selector
-        bindSourceSelector(context, views, widgetAppData, source);
+        // Corpus indicator
+        bindCorpusIndicator(context, views, widgetAppData, corpus);
 
         // Text field
         Intent qsbIntent = new Intent(SearchManager.INTENT_ACTION_GLOBAL_SEARCH);
@@ -82,7 +78,7 @@
                 | Intent.FLAG_ACTIVITY_CLEAR_TOP
                 | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
         qsbIntent.putExtra(SearchManager.APP_DATA, widgetAppData);
-        SearchSourceSelector.setSource(qsbIntent, sourceName);
+        qsbIntent.setData(SearchActivity.getCorpusUri(corpus));
         setOnClickIntent(context, views, R.id.search_widget_text, qsbIntent);
 
         // Voice search button. Only shown if voice search is available.
@@ -106,19 +102,18 @@
         appWidgetManager.updateAppWidget(appWidgetId, views);
     }
 
-    private static void bindSourceSelector(Context context, RemoteViews views,
-            Bundle widgetAppData, Source source) {
-        Uri sourceIconUri = getSourceIconUri(context, source);
-        views.setImageViewUri(SearchSourceSelector.ICON_VIEW_ID, sourceIconUri);
-        ComponentName sourceName = source == null ? null : source.getComponentName();
+    private static void bindCorpusIndicator(Context context, RemoteViews views,
+            Bundle widgetAppData, Corpus corpus) {
+        Uri sourceIconUri = getCorpusIconUri(context, corpus);
+        views.setImageViewUri(CorpusIndicator.ICON_VIEW_ID, sourceIconUri);
 
-        Intent intent = new Intent(SearchActivity.INTENT_ACTION_QSB_AND_SELECT_SEARCH_SOURCE);
+        Intent intent = new Intent(SearchActivity.INTENT_ACTION_QSB_AND_SELECT_CORPUS);
         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);
-        SearchSourceSelector.setSource(intent, sourceName);
-        setOnClickIntent(context, views, SearchSourceSelector.ICON_VIEW_ID, intent);
+        intent.setData(SearchActivity.getCorpusUri(corpus));
+        setOnClickIntent(context, views, CorpusIndicator.ICON_VIEW_ID, intent);
     }
 
     private static void setOnClickIntent(Context context, RemoteViews views,
@@ -127,19 +122,19 @@
         views.setOnClickPendingIntent(viewId, pendingIntent);
     }
 
-    private static Uri getSourceIconUri(Context context, Source source) {
-        if (source == null) {
+    private static Uri getCorpusIconUri(Context context, Corpus corpus) {
+        if (corpus == null) {
             return getSuggestionViewFactory(context).getGlobalSearchIconUri();
         }
-        return source.getSourceIconUri();
+        return corpus.getCorpusIconUri();
     }
 
     private static QsbApplication getQsbApplication(Context context) {
         return (QsbApplication) context.getApplicationContext();
     }
 
-    private static SourceLookup getSources(Context context) {
-        return getQsbApplication(context).getSources();
+    private static Corpora getCorpora(Context context) {
+        return getQsbApplication(context).getCorpora();
     }
 
     private static SuggestionViewFactory getSuggestionViewFactory(Context context) {
diff --git a/src/com/android/quicksearchbox/SearchableCorpora.java b/src/com/android/quicksearchbox/SearchableCorpora.java
new file mode 100644
index 0000000..d3405e3
--- /dev/null
+++ b/src/com/android/quicksearchbox/SearchableCorpora.java
@@ -0,0 +1,225 @@
+/*
+ * 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.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.database.DataSetObserver;
+import android.os.Handler;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * Maintains the list of all suggestion sources.
+ */
+public class SearchableCorpora implements Corpora {
+
+    // set to true to enable the more verbose debug logging for this file
+    private static final boolean DBG = true;
+    private static final String TAG = "QSB.DefaultCorpora";
+
+    private final Context mContext;
+    private final Config mConfig;
+    private final Handler mUiThread;
+    private final SharedPreferences mPreferences;
+
+    private boolean mLoaded = false;
+
+    private SearchableSources mSources;
+    // Maps corpus names to corpora
+    private HashMap<String,Corpus> mCorporaByName;
+    // Maps sources to the corpus that contains them
+    private HashMap<Source,Corpus> mCorporaBySource;
+    // Enabled corpora
+    private List<Corpus> mEnabledCorpora;
+    private Corpus mWebCorpus;
+
+    private boolean mShowWebSuggestions;
+
+    // Updates the inclusion of the web search provider.
+    private ShowWebSuggestionsSettingObserver mShowWebSuggestionsSettingObserver;
+
+    /**
+     *
+     * @param context Used for looking up source information etc.
+     */
+    public SearchableCorpora(Context context, Config config, Handler uiThread) {
+        mContext = context;
+        mConfig = config;
+        mUiThread = uiThread;
+        mPreferences = SearchSettings.getSearchPreferences(context);
+
+        mSources = new SearchableSources(context, uiThread);
+    }
+
+    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);
+    }
+
+    public Corpus getCorpusForSource(Source source) {
+        checkLoaded();
+        return mCorporaBySource.get(source);
+    }
+
+    public Source getSource(ComponentName name) {
+        checkLoaded();
+        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.");
+        }
+
+        // Listen for web suggestion setting changes
+        mShowWebSuggestionsSettingObserver =
+                new ShowWebSuggestionsSettingObserver(mUiThread);
+        SearchSettings.registerShowWebSuggestionsSettingObserver(mContext,
+                mShowWebSuggestionsSettingObserver);
+        updateWebSuggestionsSetting();
+
+        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();
+
+        SearchSettings.unregisterShowWebSuggestionsSettingObserver(mContext,
+                mShowWebSuggestionsSettingObserver);
+
+        mSources.close();
+        mSources = null;
+        mLoaded = false;
+    }
+
+    private void updateCorpora() {
+        mCorporaByName = new HashMap<String,Corpus>();
+        mCorporaBySource = new HashMap<Source,Corpus>();
+        mEnabledCorpora = new ArrayList<Corpus>();
+
+        Source webSource = mSources.getWebSearchSource();
+        Source browserSource = mSources.getSource(getBrowserSearchComponent());
+        mWebCorpus = new WebCorpus(mContext, webSource, browserSource);
+        addCorpus(mWebCorpus);
+        mCorporaBySource.put(webSource, mWebCorpus);
+        mCorporaBySource.put(browserSource, mWebCorpus);
+
+        // Create corpora for all unclaimed sources
+        for (Source source : mSources.getSources()) {
+            if (!mCorporaBySource.containsKey(source)) {
+                Corpus corpus = new SingleSourceCorpus(source);
+                addCorpus(corpus);
+                mCorporaBySource.put(source, corpus);
+            }
+        }
+
+        if (DBG) Log.d(TAG, "Updated corpora: " + mCorporaBySource.values());
+
+        mEnabledCorpora = Collections.unmodifiableList(mEnabledCorpora);
+    }
+
+    private void addCorpus(Corpus corpus) {
+        mCorporaByName.put(corpus.getName(), corpus);
+        if (isCorpusEnabled(corpus)) {
+            mEnabledCorpora.add(corpus);
+        }
+    }
+
+    private ComponentName getBrowserSearchComponent() {
+        String name = mContext.getString(R.string.browser_search_component);
+        return TextUtils.isEmpty(name) ? null : ComponentName.unflattenFromString(name);
+    }
+
+    public boolean isCorpusEnabled(Corpus corpus) {
+        if (corpus == null) return false;
+        boolean defaultEnabled = isCorpusDefaultEnabled(corpus);
+        String sourceEnabledPref = SearchSettings.getCorpusEnabledPreference(corpus);
+        return mPreferences.getBoolean(sourceEnabledPref, defaultEnabled);
+    }
+
+    public boolean isCorpusDefaultEnabled(Corpus corpus) {
+        String name = corpus.getName();
+        return mConfig.isCorpusEnabledByDefault(name);
+    }
+
+    public boolean shouldShowWebSuggestions() {
+        return mShowWebSuggestions;
+    }
+
+    private void updateWebSuggestionsSetting() {
+        mShowWebSuggestions = SearchSettings.areWebSuggestionsEnabled(mContext);
+    }
+
+    /**
+     * ContentObserver which updates the list of enabled sources to include or exclude
+     * the web search provider depending on the state of the
+     * {@link Settings.System#SHOW_WEB_SUGGESTIONS} setting.
+     */
+    private class ShowWebSuggestionsSettingObserver extends ContentObserver {
+        public ShowWebSuggestionsSettingObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            updateWebSuggestionsSetting();
+        }
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/SearchableSource.java b/src/com/android/quicksearchbox/SearchableSource.java
index a9bcf86..27bfaf9 100644
--- a/src/com/android/quicksearchbox/SearchableSource.java
+++ b/src/com/android/quicksearchbox/SearchableSource.java
@@ -16,17 +16,21 @@
 
 package com.android.quicksearchbox;
 
+import android.app.PendingIntent;
 import android.app.SearchManager;
 import android.app.SearchableInfo;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
+import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.database.Cursor;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.Bundle;
+import android.speech.RecognizerIntent;
 import android.util.Log;
 
 import java.util.Arrays;
@@ -40,6 +44,10 @@
     private static final boolean DBG = true;
     private static final String TAG = "QSB.SearchableSource";
 
+    // TODO: This should be exposed or moved to android-common, see http://b/issue?id=2440614
+    // 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;
@@ -64,6 +72,14 @@
         mIconLoader = createIconLoader(context, searchable.getSuggestPackage());
     }
 
+    protected Context getContext() {
+        return mContext;
+    }
+
+    protected SearchableInfo getSearchableInfo() {
+        return mSearchable;
+    }
+
     private IconLoader createIconLoader(Context context, String providerPackage) {
         if (providerPackage == null) return null;
         try {
@@ -107,12 +123,7 @@
     }
 
     public CharSequence getSettingsDescription() {
-        int res = mSearchable.getSettingsDescriptionId();
-        if (res == 0) {
-            return null;
-        }
-        return mContext.getPackageManager().getText(mActivityInfo.packageName, res,
-                mActivityInfo.applicationInfo);
+        return getText(mSearchable.getSettingsDescriptionId());
     }
 
     public Drawable getSourceIcon() {
@@ -144,14 +155,104 @@
         return (icon != 0) ? icon : android.R.drawable.sym_def_app_icon;
     }
 
-    public SuggestionCursor getSuggestions(String query, int queryLimit) {
+    public boolean voiceSearchEnabled() {
+        return mSearchable.getVoiceSearchEnabled();
+    }
+
+    // TODO: not all apps handle ACTION_SEARCH properly, e.g. ApplicationsProvider.
+    // Maybe we should add a flag to searchable, so that QSB can hide the search button?
+    public Intent createSearchIntent(String query, Bundle appData) {
+        Intent intent = new Intent(Intent.ACTION_SEARCH);
+        intent.setComponent(getComponentName());
+        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 Intent createVoiceSearchIntent(Bundle appData) {
+        if (mSearchable.getVoiceSearchLaunchWebSearch()) {
+            return WebCorpus.createVoiceWebSearchIntent(appData);
+        } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
+            return createVoiceAppSearchIntent(appData);
+        }
+        return null;
+    }
+
+    /**
+     * Create and return an Intent that can launch the voice search activity, perform a specific
+     * voice transcription, and forward the results to the searchable activity.
+     *
+     * This code is copied from SearchDialog
+     *
+     * @return A completely-configured intent ready to send to the voice search activity
+     */
+    private Intent createVoiceAppSearchIntent(Bundle appData) {
+        ComponentName searchActivity = mSearchable.getSearchActivity();
+
+        // create the necessary intent to set up a search-and-forward operation
+        // in the voice search system.   We have to keep the bundle separate,
+        // because it becomes immutable once it enters the PendingIntent
+        Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
+        queryIntent.setComponent(searchActivity);
+        PendingIntent pending = PendingIntent.getActivity(
+                getContext(), 0, queryIntent, PendingIntent.FLAG_ONE_SHOT);
+
+        // Now set up the bundle that will be inserted into the pending intent
+        // when it's time to do the search.  We always build it here (even if empty)
+        // because the voice search activity will always need to insert "QUERY" into
+        // it anyway.
+        Bundle queryExtras = new Bundle();
+        if (appData != null) {
+            queryExtras.putBundle(SearchManager.APP_DATA, appData);
+        }
+
+        // Now build the intent to launch the voice search.  Add all necessary
+        // extras to launch the voice recognizer, and then all the necessary extras
+        // to forward the results to the searchable activity
+        Intent voiceIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+        voiceIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        // Add all of the configuration options supplied by the searchable's metadata
+        String languageModel = getString(mSearchable.getVoiceLanguageModeId());
+        if (languageModel == null) {
+            languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
+        }
+        String prompt = getString(mSearchable.getVoicePromptTextId());
+        String language = getString(mSearchable.getVoiceLanguageId());
+        int maxResults = mSearchable.getVoiceMaxResults();
+        if (maxResults <= 0) {
+            maxResults = 1;
+        }
+
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
+        voiceIntent.putExtra(EXTRA_CALLING_PACKAGE,
+                searchActivity == null ? null : searchActivity.toShortString());
+
+        // Add the values that configure forwarding the results
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
+
+        return voiceIntent;
+    }
+
+    public SourceResult getSuggestions(String query, int queryLimit) {
         try {
             Cursor cursor = getSuggestions(mContext, mSearchable, query, queryLimit);
             if (DBG) Log.d(TAG, toString() + "[" + query + "] returned.");
-            return new SourceResult(this, query, cursor);
+            return new CursorBackedSourceResult(query, cursor);
         } catch (RuntimeException ex) {
             Log.e(TAG, toString() + "[" + query + "] failed", ex);
-            return new SourceResult(this, query);
+            return new CursorBackedSourceResult(query);
         }
     }
 
@@ -163,7 +264,7 @@
             if (cursor != null && cursor.getCount() > 0) {
                 cursor.moveToFirst();
             }
-            return new SourceResult(this, null, cursor);
+            return new CursorBackedSourceResult(null, cursor);
         } catch (RuntimeException ex) {
             Log.e(TAG, toString() + "[" + shortcutId + "] failed", ex);
             if (cursor != null) {
@@ -174,6 +275,28 @@
         }
     }
 
+    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;
+        }
+
+    }
+
     /**
      * This is a copy of {@link SearchManager#getSuggestions(SearchableInfo, String)}.
      */
@@ -273,14 +396,14 @@
     public boolean equals(Object o) {
         if (o != null && o.getClass().equals(this.getClass())) {
             SearchableSource s = (SearchableSource) o;
-            return s.mSearchable.getSearchActivity().equals(mSearchable.getSearchActivity());
+            return s.getComponentName().equals(getComponentName());
         }
         return false;
     }
 
     @Override
     public int hashCode() {
-        return mSearchable.getSearchActivity().hashCode();
+        return getComponentName().hashCode();
     }
 
     @Override
@@ -308,4 +431,14 @@
         return actionKey.getSuggestActionMsgColumn();
     }
 
+    private CharSequence getText(int id) {
+        if (id == 0) return null;
+        return mContext.getPackageManager().getText(mActivityInfo.packageName, id,
+                mActivityInfo.applicationInfo);
+    }
+
+    private String getString(int id) {
+        CharSequence text = getText(id);
+        return text == null ? null : text.toString();
+    }
 }
diff --git a/src/com/android/quicksearchbox/SearchableSourceFactory.java b/src/com/android/quicksearchbox/SearchableSourceFactory.java
deleted file mode 100644
index 61d4450..0000000
--- a/src/com/android/quicksearchbox/SearchableSourceFactory.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * 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.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;
-
-public class SearchableSourceFactory implements SourceFactory {
-
-    private static final String TAG = "QSB.SearchableSourceFactory";
-
-    private final Context mContext;
-
-    private final SearchManager mSearchManager;
-
-    public SearchableSourceFactory(Context context) {
-        mContext = context;
-        mSearchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
-    }
-
-    protected Context getContext() {
-        return mContext;
-    }
-
-    protected 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);
-    }
-
-    public Source createSource(SearchableInfo searchable) {
-        if (searchable == null) return null;
-        try {
-            return new SearchableSource(mContext, searchable);
-        } catch (NameNotFoundException ex) {
-            Log.e(TAG, "Source not found: " + ex);
-            return null;
-        }
-    }
-
-    // TODO: Create a special Source subclass that bypasses the ContentProvider interface
-    public Source createWebSearchSource() {
-        ComponentName sourceName = getWebSearchComponent();
-        SearchableInfo searchable = mSearchManager.getSearchableInfo(sourceName);
-        if (searchable == null) {
-            Log.e(TAG, "Web search source " + sourceName + " is not searchable.");
-            return null;
-        }
-        try {
-            return new WebSource(mContext, searchable);
-        } catch (NameNotFoundException ex) {
-            Log.e(TAG, "Web search source not found: " + sourceName);
-            return null;
-        }
-    }
-
-}
diff --git a/src/com/android/quicksearchbox/SearchableSources.java b/src/com/android/quicksearchbox/SearchableSources.java
new file mode 100644
index 0000000..e3c881b
--- /dev/null
+++ b/src/com/android/quicksearchbox/SearchableSources.java
@@ -0,0 +1,230 @@
+/*
+ * 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.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;
+import java.util.HashMap;
+
+/**
+ * Maintains a list of search sources.
+ */
+public class SearchableSources {
+
+    // set to true to enable the more verbose debug logging for this file
+    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.
+    private HashMap<ComponentName, Source> mSources;
+
+    // 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) {
+        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();
+    }
+
+    public Source getSource(ComponentName name) {
+        return mSources.get(name);
+    }
+
+    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);
+            }
+        }
+    };
+
+    /**
+     * After calling, clients must call {@link #close()} when done with this object.
+     */
+    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();
+    }
+
+    /**
+     * Releases all resources used by this object. It is possible to call
+     * {@link #load()} again after calling this method.
+     */
+    public void close() {
+        if (!mLoaded) {
+            throw new IllegalStateException("close(): Not loaded.");
+        }
+
+        mContext.unregisterReceiver(mBroadcastReceiver);
+
+        mDataSetObservable.unregisterAll();
+
+        mSources = null;
+        mLoaded = false;
+    }
+
+    /**
+     * Loads the list of suggestion sources.
+     */
+    private void updateSources() {
+        if (DBG) Log.d(TAG, "updateSources()");
+        mSources = new HashMap<ComponentName,Source>();
+        for (SearchableInfo searchable : mSearchManager.getSearchablesInGlobalSearch()) {
+            Source source = createSearchableSource(searchable);
+            if (source != null) {
+                if (DBG) Log.d(TAG, "Created source " + source);
+                addSource(source);
+            }
+        }
+
+        mWebSearchSource = createWebSearchSource();
+        addSource(mWebSearchSource);
+    }
+
+    private void addSource(Source source) {
+        mSources.put(source.getComponentName(), 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);
+    }
+
+    private Source createSearchableSource(SearchableInfo searchable) {
+        if (searchable == null) return null;
+        try {
+            return new SearchableSource(mContext, searchable);
+        } catch (NameNotFoundException ex) {
+            Log.e(TAG, "Source not found: " + ex);
+            return null;
+        }
+    }
+
+    /**
+     * Register an observer that is called when changes happen to this data set.
+     *
+     * @param observer gets notified when the data set changes.
+     */
+    public void registerDataSetObserver(DataSetObserver observer) {
+        mDataSetObservable.registerObserver(observer);
+    }
+
+    /**
+     * Unregister an observer that has previously been registered with
+     * {@link #registerDataSetObserver(DataSetObserver)}
+     *
+     * @param observer the observer to unregister.
+     */
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        mDataSetObservable.unregisterObserver(observer);
+    }
+
+    protected void notifyDataSetChanged() {
+        mDataSetObservable.notifyChanged();
+    }
+}
diff --git a/src/com/android/quicksearchbox/ShortcutCursor.java b/src/com/android/quicksearchbox/ShortcutCursor.java
index 9cc1b1d..4aac6a3 100644
--- a/src/com/android/quicksearchbox/ShortcutCursor.java
+++ b/src/com/android/quicksearchbox/ShortcutCursor.java
@@ -16,7 +16,6 @@
 
 package com.android.quicksearchbox;
 
-import android.content.ComponentName;
 import android.util.Log;
 
 import java.util.HashSet;
@@ -44,7 +43,7 @@
         int count = shortcuts.getCount();
         for (int i = 0; i < count; i++) {
             shortcuts.moveTo(i);
-            if (shortcuts.getSource() != null) {
+            if (shortcuts.getSuggestionSource() != null) {
                 add(new SuggestionPosition(shortcuts));
             }
         }
@@ -55,7 +54,7 @@
      * Since this modifies the cursor, it should be called on the UI thread.
      * This class assumes responsibility for closing refreshed.
      */
-    public void refresh(ComponentName source, String shortcutId, SuggestionCursor refreshed) {
+    public void refresh(Source source, String shortcutId, SuggestionCursor refreshed) {
         if (DBG) Log.d(TAG, "refresh " + shortcutId);
         if (mClosed) {
             if (refreshed != null) {
@@ -69,7 +68,7 @@
         int count = getCount();
         for (int i = 0; i < count; i++) {
             moveTo(i);
-            if (shortcutId.equals(getShortcutId()) && source.equals(getSourceComponentName())) {
+            if (shortcutId.equals(getShortcutId()) && source.equals(getSuggestionSource())) {
               if (refreshed != null && refreshed.getCount() > 0) {
                   replaceRow(new SuggestionPosition(refreshed));
               } else {
diff --git a/src/com/android/quicksearchbox/ShortcutPromoter.java b/src/com/android/quicksearchbox/ShortcutPromoter.java
index a0a37e5..ebef712 100644
--- a/src/com/android/quicksearchbox/ShortcutPromoter.java
+++ b/src/com/android/quicksearchbox/ShortcutPromoter.java
@@ -44,7 +44,7 @@
     }
 
     public void pickPromoted(SuggestionCursor shortcuts,
-            ArrayList<SuggestionCursor> suggestions, int maxPromoted,
+            ArrayList<CorpusResult> suggestions, int maxPromoted,
             ListSuggestionCursor promoted) {
         int shortcutCount = shortcuts == null ? 0 : shortcuts.getCount();
         int promotedShortcutCount = Math.min(shortcutCount, maxPromoted);
diff --git a/src/com/android/quicksearchbox/ShortcutRefresher.java b/src/com/android/quicksearchbox/ShortcutRefresher.java
index 1ede007..e1fa613 100644
--- a/src/com/android/quicksearchbox/ShortcutRefresher.java
+++ b/src/com/android/quicksearchbox/ShortcutRefresher.java
@@ -16,8 +16,6 @@
 
 package com.android.quicksearchbox;
 
-import android.content.ComponentName;
-
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
@@ -32,17 +30,16 @@
         /**
          * Called by the ShortcutRefresher when a shortcut has been refreshed.
          *
-         * @param componentName of the source of this shortcut.
+         * @param source source of this shortcut.
          * @param shortcutId the id of the shortcut.
          * @param refreshed the updated shortcut, or {@code null} if the shortcut
          *        is no longer valid and should be deleted.
          */
-        void onShortcutRefreshed(ComponentName componentName, String shortcutId,
+        void onShortcutRefreshed(Source source, String shortcutId,
                 SuggestionCursor refreshed);
     }
 
     private final SourceTaskExecutor mExecutor;
-    private final SourceLookup mSourceLookup;
 
     private final Set<String> mRefreshed = Collections.synchronizedSet(new HashSet<String>());
 
@@ -50,11 +47,9 @@
      * Create a ShortcutRefresher that will refresh shortcuts using the given executor.
      *
      * @param executor Used to execute the tasks.
-     * @param sourceLookup Used to lookup suggestion sources by component name.
      */
-    public ShortcutRefresher(SourceTaskExecutor executor, SourceLookup sourceLookup) {
+    public ShortcutRefresher(SourceTaskExecutor executor) {
         mExecutor = executor;
-        mSourceLookup = sourceLookup;
     }
 
     /**
@@ -69,13 +64,12 @@
             shortcuts.moveTo(i);
             if (shouldRefresh(shortcuts)) {
                 String shortcutId = shortcuts.getShortcutId();
-                ComponentName componentName = shortcuts.getSourceComponentName();
-                Source source = mSourceLookup.getSourceByComponentName(componentName);
+                Source source = shortcuts.getSuggestionSource();
 
                 // If we can't find the source then invalidate the shortcut.
                 // Otherwise, send off the refresh task.
                 if (source == null) {
-                    listener.onShortcutRefreshed(componentName, shortcutId, null);
+                    listener.onShortcutRefreshed(source, shortcutId, null);
                 } else {
                     String extraData = shortcuts.getSuggestionIntentExtraData();
                     ShortcutRefreshTask refreshTask = new ShortcutRefreshTask(
@@ -116,7 +110,7 @@
     }
 
     private static String makeKey(SuggestionCursor shortcut) {
-        return shortcut.getSourceComponentName().flattenToShortString() + "#"
+        return shortcut.getSuggestionSource().getFlattenedComponentName() + "#"
                 + shortcut.getShortcutId();
     }
 
@@ -146,11 +140,8 @@
             // TODO: Add latency tracking and logging.
             SuggestionCursor refreshed = mSource.refreshShortcut(mShortcutId, mExtraData);
             onShortcutRefreshed(refreshed);
-            mListener.onShortcutRefreshed(mSource.getComponentName(), mShortcutId, refreshed);
+            mListener.onShortcutRefreshed(mSource, mShortcutId, refreshed);
         }
 
-        public Source getSource() {
-            return mSource;
-        }
     }
 }
diff --git a/src/com/android/quicksearchbox/ShortcutRepository.java b/src/com/android/quicksearchbox/ShortcutRepository.java
index b89e59f..2aba629 100644
--- a/src/com/android/quicksearchbox/ShortcutRepository.java
+++ b/src/com/android/quicksearchbox/ShortcutRepository.java
@@ -16,10 +16,7 @@
 
 package com.android.quicksearchbox;
 
-import android.content.ComponentName;
-
-import java.util.ArrayList;
-import java.util.List;
+import java.util.Map;
 
 /**
  * Holds information about shortcuts (results the user has clicked on before), and returns
@@ -45,7 +42,7 @@
     /**
      * Reports a click on a suggestion.
      */
-    void reportClick(SuggestionPosition clicked);
+    void reportClick(SuggestionCursor suggestions, int position);
 
     /**
      * @param query The query.
@@ -54,7 +51,8 @@
     SuggestionCursor getShortcutsForQuery(String query);
 
     /**
-     * @return A ranking of suggestion sources based on clicks and impressions.
+     * @return A map for corpus name to score. A higher score means that the corpus
+     *         is more important.
      */
-    ArrayList<ComponentName> getSourceRanking();
+    Map<String,Integer> getCorpusScores();
 }
diff --git a/src/com/android/quicksearchbox/ShortcutRepositoryImplLog.java b/src/com/android/quicksearchbox/ShortcutRepositoryImplLog.java
index 778aca6..870b9f4 100644
--- a/src/com/android/quicksearchbox/ShortcutRepositoryImplLog.java
+++ b/src/com/android/quicksearchbox/ShortcutRepositoryImplLog.java
@@ -17,19 +17,21 @@
 package com.android.quicksearchbox;
 
 import android.app.SearchManager;
-import android.content.*;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteQueryBuilder;
 import android.os.Handler;
 import android.util.Log;
-import android.view.KeyEvent;
 
 import java.io.File;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
+import java.util.Map;
 
 /**
  * A shortcut repository implementation that uses a log of every click.
@@ -45,7 +47,7 @@
     private static final String TAG = "QSB.ShortcutRepositoryImplLog";
 
     private static final String DB_NAME = "qsb-log.db";
-    private static final int DB_VERSION = 25;
+    private static final int DB_VERSION = 26;
 
     private static final String HAS_HISTORY_QUERY =
         "SELECT " + Shortcuts.intent_key.fullName + " FROM " + Shortcuts.TABLE_NAME;
@@ -59,7 +61,7 @@
 
     private final Context mContext;
     private final Config mConfig;
-    private final SourceLookup mSources;
+    private final Corpora mCorpora;
     private final ShortcutRefresher mRefresher;
     private final Handler mUiThread;
     private final DbOpenHelper mOpenHelper;
@@ -68,7 +70,7 @@
      * Create an instance to the repo.
      */
     public static ShortcutRepository create(Context context, Config config,
-            SourceLookup sources, ShortcutRefresher refresher, Handler uiThread) {
+            Corpora sources, ShortcutRefresher refresher, Handler uiThread) {
         return new ShortcutRepositoryImplLog(context, config, sources, refresher,
                 uiThread, DB_NAME);
     }
@@ -77,11 +79,11 @@
      * @param context Used to create / open db
      * @param name The name of the database to create.
      */
-    ShortcutRepositoryImplLog(Context context, Config config, SourceLookup sources,
+    ShortcutRepositoryImplLog(Context context, Config config, Corpora corpora,
             ShortcutRefresher refresher, Handler uiThread, String name) {
         mContext = context;
         mConfig = config;
-        mSources = sources;
+        mCorpora = corpora;
         mRefresher = refresher;
         mUiThread = uiThread;
         mOpenHelper = new DbOpenHelper(context, name, DB_VERSION, config);
@@ -185,8 +187,9 @@
         getOpenHelper().close();
     }
 
-    public void reportClick(SuggestionPosition clicked) {
-        logClick(clicked, System.currentTimeMillis());
+    public void reportClick(SuggestionCursor suggestions, int position) {
+        suggestions.moveTo(position);
+        logClick(suggestions, System.currentTimeMillis());
     }
 
     public SuggestionCursor getShortcutsForQuery(String query) {
@@ -197,8 +200,8 @@
         return shortcuts;
     }
 
-    public ArrayList<ComponentName> getSourceRanking() {
-        return getSourceRanking(mConfig.getMinClicksForSourceRanking());
+    public Map<String,Integer> getCorpusScores() {
+        return getCorpusScores(mConfig.getMinClicksForSourceRanking());
     }
 
 // -------------------------- end ShortcutRepository --------------------------
@@ -222,30 +225,30 @@
             cursor.close();
             return null;
         }
-        return new ShortcutCursor(new SuggestionCursorImpl(mSources, query, cursor));
+        return new ShortcutCursor(new SuggestionCursorImpl(query, cursor));
     }
 
     private void startRefresh(final ShortcutCursor shortcuts) {
         mRefresher.refresh(shortcuts, new ShortcutRefresher.Listener() {
-            public void onShortcutRefreshed(final ComponentName componentName,
+            public void onShortcutRefreshed(final Source source,
                     final String shortcutId, final SuggestionCursor refreshed) {
-                refreshShortcut(componentName, shortcutId, refreshed);
+                refreshShortcut(source, shortcutId, refreshed);
                 mUiThread.post(new Runnable() {
                     public void run() {
-                        shortcuts.refresh(componentName, shortcutId, refreshed);
+                        shortcuts.refresh(source, shortcutId, refreshed);
                     }
                 });
             }
         });
     }
 
-    private void refreshShortcut(ComponentName source, String shortcutId, SuggestionCursor refreshed) {
+    private void refreshShortcut(Source source, String shortcutId, SuggestionCursor refreshed) {
         if (source == null) throw new NullPointerException("source");
         if (shortcutId == null) throw new NullPointerException("shortcutId");
 
         final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
 
-        String[] whereArgs = { shortcutId, source.flattenToShortString() };
+        String[] whereArgs = { shortcutId, source.getFlattenedComponentName() };
         if (refreshed == null || refreshed.getCount() == 0) {
             if (DBG) Log.d(TAG, "Deleting shortcut: " + shortcutId);
             db.delete(Shortcuts.TABLE_NAME, SHORTCUT_BY_ID_WHERE, whereArgs);
@@ -262,17 +265,15 @@
         private final String mSearchSpinner = ContentResolver.SCHEME_ANDROID_RESOURCE
                     + "://" + mContext.getPackageName() + "/"  + R.drawable.search_spinner;
 
-        private final SourceLookup mSources;
         private final HashMap<String, Source> mSourceCache;
 
-        public SuggestionCursorImpl(SourceLookup sources, String userQuery, Cursor cursor) {
+        public SuggestionCursorImpl(String userQuery, Cursor cursor) {
             super(userQuery, cursor);
-            mSources = sources;
             mSourceCache = new HashMap<String, Source>();
         }
 
         @Override
-        protected Source getSource() {
+        public Source getSuggestionSource() {
             // TODO: Using ordinal() is hacky, look up the column instead
             String srcStr = mCursor.getString(Shortcuts.source.ordinal());
             if (srcStr == null) {
@@ -281,7 +282,7 @@
             Source source = mSourceCache.get(srcStr);
             if (source == null) {
                 ComponentName srcName = ComponentName.unflattenFromString(srcStr);
-                source = mSources.getSourceByComponentName(srcName);
+                source = mCorpora.getSource(srcName);
                 // We cache the source so that it can be found quickly, and so
                 // that it doesn't disappear over the lifetime of this cursor.
                 mSourceCache.put(srcStr, source);
@@ -340,34 +341,30 @@
      * @param minClicks The minimum number of clicks a source must have.
      * @return The list of sources, ranked by total clicks.
      */
-    ArrayList<ComponentName> getSourceRanking(int minClicks) {
+    Map<String,Integer> getCorpusScores(int minClicks) {
         SQLiteDatabase db = mOpenHelper.getReadableDatabase();
         final Cursor cursor = db.rawQuery(
                 SOURCE_RANKING_SQL, new String[] { String.valueOf(minClicks) });
         try {
-            final ArrayList<ComponentName> sources =
-                    new ArrayList<ComponentName>(cursor.getCount());
+            Map<String,Integer> corpora = new HashMap<String,Integer>(cursor.getCount());
             while (cursor.moveToNext()) {
-                sources.add(sourceFromCursor(cursor));
+                String name = cursor.getString(SourceStats.corpus.ordinal());
+                int clicks = cursor.getInt(SourceStats.total_clicks.ordinal());
+                corpora.put(name, clicks);
             }
-            return sources;
+            return corpora;
         } finally {
             cursor.close();
         }
     }
 
-    private ComponentName sourceFromCursor(Cursor cursor) {
-        return ComponentName.unflattenFromString(cursor.getString(SourceStats.component.ordinal()));
-    }
-
     private ContentValues makeShortcutRow(SuggestionCursor suggestion) {
-        ComponentName source = suggestion.getSourceComponentName();
-        Intent intent = suggestion.getSuggestionIntent(mContext, null, KeyEvent.KEYCODE_UNKNOWN, null);
-        String intentAction = intent.getAction();
-        String intentData = intent.getDataString();
-        String intentQuery = intent.getStringExtra(SearchManager.QUERY);
-        String intentExtraData = intent.getStringExtra(SearchManager.EXTRA_DATA_KEY);
+        String intentAction = suggestion.getSuggestionIntentAction();
+        String intentData = suggestion.getSuggestionIntentDataString();
+        String intentQuery = suggestion.getSuggestionQuery();
+        String intentExtraData = suggestion.getSuggestionIntentExtraData();
 
+        ComponentName source = suggestion.getSuggestionSource().getComponentName();
         StringBuilder key = new StringBuilder(source.flattenToShortString());
         key.append("#");
         if (intentData != null) {
@@ -405,9 +402,7 @@
         return cv;
     }
 
-    private void logClick(SuggestionPosition clicked, long now) {
-        SuggestionCursor suggestion = clicked.getSuggestion();
-
+    private void logClick(SuggestionCursor suggestion, long now) {
         if (DBG) {
             Log.d(TAG, "logClicked(" + suggestion + ")");
         }
@@ -419,7 +414,7 @@
 
         // Once the user has clicked on a shortcut, don't bother refreshing
         // (especially if this is a new shortcut)
-        mRefresher.onShortcutRefreshed(clicked.getSuggestion());
+        mRefresher.onShortcutRefreshed(suggestion);
 
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
 
@@ -427,7 +422,7 @@
         // Since intent_key is the primary key, any existing
         // suggestion with the same source+data+action will be replaced
         if (DBG) Log.d(TAG, "Adding shortcut: " + suggestion);
-        ContentValues shortcut = makeShortcutRow(clicked.getSuggestion());
+        ContentValues shortcut = makeShortcutRow(suggestion);
         String intentKey = shortcut.getAsString(Shortcuts.intent_key.name());
         db.replaceOrThrow(Shortcuts.TABLE_NAME, null, shortcut);
 
@@ -440,19 +435,22 @@
             db.insertOrThrow(ClickLog.TABLE_NAME, null, cv);
         }
 
-        // Log click for source
-        {
-            final ContentValues cv = new ContentValues();
-            ComponentName name = suggestion.getSourceComponentName();
-            cv.put(SourceLog.component.name(), name.flattenToString());
-            cv.put(SourceLog.time.name(), now);
-            cv.put(SourceLog.click_count.name(), 1);
-            db.insertOrThrow(SourceLog.TABLE_NAME, null, cv);
-        }
+        // Log click for corpus
+        Corpus corpus = mCorpora.getCorpusForSource(suggestion.getSuggestionSource());
+        logCorpusClick(db, corpus, now);
 
         postSourceEventCleanup(now);
     }
 
+    private void logCorpusClick(SQLiteDatabase db, Corpus corpus, long now) {
+        if (corpus == null) return;
+        ContentValues cv = new ContentValues();
+        cv.put(SourceLog.corpus.name(), corpus.getName());
+        cv.put(SourceLog.time.name(), now);
+        cv.put(SourceLog.click_count.name(), 1);
+        db.insertOrThrow(SourceLog.TABLE_NAME, null, cv);
+    }
+
     /**
      * Execute queries necessary to keep things up to date after inserting into {@link SourceLog}.
      *
@@ -469,12 +467,12 @@
                 + now + " - " + mConfig.getMaxSourceEventAgeMillis() + ";");
 
         // update the source stats
-        final String columns = SourceLog.component + "," +
+        final String columns = SourceLog.corpus + "," +
                 "SUM(" + SourceLog.click_count.fullName + ")";
         db.execSQL("DELETE FROM " + SourceStats.TABLE_NAME);
         db.execSQL("INSERT INTO " + SourceStats.TABLE_NAME  + " "
                 + "SELECT " + columns + " FROM " + SourceLog.TABLE_NAME + " GROUP BY "
-                + SourceLog.component.name());
+                + SourceLog.corpus.name());
     }
 
 // -------------------------- TABLES --------------------------
@@ -541,7 +539,7 @@
      */
     enum SourceLog {
         _id,
-        component,
+        corpus,
         time,
         click_count,
         impression_count;
@@ -573,7 +571,7 @@
      * are reported.
      */
     enum SourceStats {
-        component,
+        corpus,
         total_clicks;
 
         static final String TABLE_NAME = "sourcetotals";
@@ -753,13 +751,13 @@
 
             db.execSQL("CREATE TABLE " + SourceLog.TABLE_NAME + " ( " +
                     SourceLog._id.name() + " INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, " +
-                    SourceLog.component.name() + " TEXT NOT NULL COLLATE UNICODE, " +
+                    SourceLog.corpus.name() + " TEXT NOT NULL COLLATE UNICODE, " +
                     SourceLog.time.name() + " INTEGER, " +
                     SourceLog.click_count + " INTEGER);"
             );
 
             db.execSQL("CREATE TABLE " + SourceStats.TABLE_NAME + " ( " +
-                    SourceStats.component.name() + " TEXT NOT NULL COLLATE UNICODE PRIMARY KEY, " +
+                    SourceStats.corpus.name() + " TEXT NOT NULL COLLATE UNICODE PRIMARY KEY, " +
                     SourceStats.total_clicks + " INTEGER);"
                     );
         }
diff --git a/src/com/android/quicksearchbox/ShouldQueryStrategy.java b/src/com/android/quicksearchbox/ShouldQueryStrategy.java
index 1de96af..5f09b24 100644
--- a/src/com/android/quicksearchbox/ShouldQueryStrategy.java
+++ b/src/com/android/quicksearchbox/ShouldQueryStrategy.java
@@ -16,7 +16,6 @@
 
 package com.android.quicksearchbox;
 
-import android.content.ComponentName;
 import android.util.Log;
 
 import java.util.HashMap;
@@ -36,24 +35,23 @@
     // The last query we've seen
     private String mLastQuery = "";
 
-    // The current implementation keeps a record of those sources that have
-    // returned zero results for some prefix of the current query. mEmptySources
-    // maps from source component name to the length of the query which returned
+    // The current implementation keeps a record of those corpora that have
+    // returned zero results for some prefix of the current query. mEmptyCorpora
+    // maps from corpus to the length of the query which returned
     // zero results.  When a query is shortened (e.g., by deleting characters)
-    // or changed entirely, mEmptySources is pruned (in updateQuery)
-    private final HashMap<ComponentName, Integer> mEmptySources
-            = new HashMap<ComponentName, Integer>();
+    // or changed entirely, mEmptyCorpora is pruned (in updateQuery)
+    private final HashMap<Corpus, Integer> mEmptyCorpora
+            = new HashMap<Corpus, Integer>();
 
     /**
      * Returns whether we should query the given source for the given query.
      */
-    public synchronized boolean shouldQuerySource(Source source, String query) {
+    public synchronized boolean shouldQueryCorpus(Corpus corpus, String query) {
         updateQuery(query);
-        if (query.length() >= source.getQueryThreshold()) {
-            ComponentName sourceName = source.getComponentName();
-            if (!source.queryAfterZeroResults() && mEmptySources.containsKey(sourceName)) {
-                if (DBG) Log.i(TAG, "Not querying " + sourceName + ", returned 0 after "
-                        + mEmptySources.get(sourceName));
+        if (query.length() >= corpus.getQueryThreshold()) {
+            if (!corpus.queryAfterZeroResults() && mEmptyCorpora.containsKey(corpus)) {
+                if (DBG) Log.i(TAG, "Not querying " + corpus + ", returned 0 after "
+                        + mEmptyCorpora.get(corpus));
                 return false;
             }
             return true;
@@ -64,12 +62,12 @@
     /**
      * Called to notify ShouldQueryStrategy when a source reports no results for a query.
      */
-    public synchronized void onZeroResults(ComponentName source, String query) {
-        if (DBG) Log.i(TAG, source + " returned 0 results for " + query);
+    public synchronized void onZeroResults(Corpus corpus, String query) {
+        if (DBG) Log.i(TAG, corpus + " returned 0 results for " + query);
         // Make sure this result is actually for a prefix of the current query.
         if (mLastQuery.startsWith(query)) {
             // TODO: Don't bother if queryAfterZeroResults is true
-            mEmptySources.put(source, query.length());
+            mEmptyCorpora.put(corpus, query.length());
         }
     }
 
@@ -80,7 +78,7 @@
         } else if (mLastQuery.startsWith(query)) {
             // This is a widening of the last query: clear out any sources
             // that reported zero results after this query.
-            Iterator<Map.Entry<ComponentName, Integer>> iter = mEmptySources.entrySet().iterator();
+            Iterator<Map.Entry<Corpus, Integer>> iter = mEmptyCorpora.entrySet().iterator();
             while (iter.hasNext()) {
                 if (iter.next().getValue() > query.length()) {
                     iter.remove();
@@ -88,7 +86,7 @@
             }
         } else {
             // This is a completely different query, clear everything.
-            mEmptySources.clear();
+            mEmptyCorpora.clear();
         }
     }
 }
diff --git a/src/com/android/quicksearchbox/SingleSourceSuggestionsProvider.java b/src/com/android/quicksearchbox/SingleCorpusSuggestionsProvider.java
similarity index 74%
rename from src/com/android/quicksearchbox/SingleSourceSuggestionsProvider.java
rename to src/com/android/quicksearchbox/SingleCorpusSuggestionsProvider.java
index 5da2f28..c2c235c 100644
--- a/src/com/android/quicksearchbox/SingleSourceSuggestionsProvider.java
+++ b/src/com/android/quicksearchbox/SingleCorpusSuggestionsProvider.java
@@ -21,30 +21,28 @@
 import java.util.ArrayList;
 
 /**
- * A suggestions provider that gets suggestions from a single source.
+ * A suggestions provider that gets suggestions from a single corpus.
  */
-public class SingleSourceSuggestionsProvider extends AbstractSuggestionsProvider {
+public class SingleCorpusSuggestionsProvider extends AbstractSuggestionsProvider {
 
-    private final Source mSource;
-
-    private final ArrayList<Source> mSources;
+    private final ArrayList<Corpus> mCorpora;
 
     private final ShortcutRepository mShortcutRepo;
 
-    public SingleSourceSuggestionsProvider(Config config, Source source,
+    public SingleCorpusSuggestionsProvider(Config config, Corpus corpus,
             SourceTaskExecutor queryExecutor,
             Handler publishThread,
             Promoter promoter,
             ShortcutRepository shortcutRepo) {
         super(config, queryExecutor, publishThread, promoter);
-        mSource = source;
-        mSources = new ArrayList<Source>(1);
-        mSources.add(source);
+        mCorpora = new ArrayList<Corpus>(1);
+        mCorpora.add(corpus);
         mShortcutRepo = shortcutRepo;
     }
 
-    public ArrayList<Source> getOrderedSources() {
-        return mSources;
+    @Override
+    public ArrayList<Corpus> getOrderedCorpora() {
+        return mCorpora;
     }
 
     @Override
diff --git a/src/com/android/quicksearchbox/SingleSourceCorpus.java b/src/com/android/quicksearchbox/SingleSourceCorpus.java
new file mode 100644
index 0000000..fd5d353
--- /dev/null
+++ b/src/com/android/quicksearchbox/SingleSourceCorpus.java
@@ -0,0 +1,84 @@
+/*
+ * 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.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+
+/**
+ * A corpus that uses a single source.
+ */
+public class SingleSourceCorpus extends AbstractCorpus {
+
+    private final Source mSource;
+
+    public SingleSourceCorpus(Source source) {
+        mSource = source;
+    }
+
+    public Drawable getCorpusIcon() {
+        return mSource.getSourceIcon();
+    }
+
+    public Uri getCorpusIconUri() {
+        return mSource.getSourceIconUri();
+    }
+
+    public CharSequence getLabel() {
+        return mSource.getLabel();
+    }
+
+    public CharSequence getSettingsDescription() {
+        return mSource.getSettingsDescription();
+    }
+
+    public CorpusResult getSuggestions(String query, int queryLimit) {
+        SourceResult sourceResult = mSource.getSuggestions(query, queryLimit);
+        return new SingleSourceCorpusResult(this, query, sourceResult);
+    }
+
+    public String getName() {
+        return mSource.getFlattenedComponentName();
+    }
+
+    public boolean queryAfterZeroResults() {
+        return mSource.queryAfterZeroResults();
+    }
+
+    public int getQueryThreshold() {
+        return mSource.getQueryThreshold();
+    }
+
+    public boolean voiceSearchEnabled() {
+        return mSource.voiceSearchEnabled();
+    }
+
+    public Intent createSearchIntent(String query, Bundle appData) {
+        return mSource.createSearchIntent(query, appData);
+    }
+
+    public Intent createVoiceSearchIntent(Bundle appData) {
+        return mSource.createVoiceSearchIntent(appData);
+    }
+
+    public boolean isWebCorpus() {
+        return false;
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/SingleSourceCorpusResult.java b/src/com/android/quicksearchbox/SingleSourceCorpusResult.java
new file mode 100644
index 0000000..28bc4c7
--- /dev/null
+++ b/src/com/android/quicksearchbox/SingleSourceCorpusResult.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+
+/**
+ * A CorpusResult backed by a single SourceResult.
+ */
+public class SingleSourceCorpusResult extends SuggestionCursorWrapper implements CorpusResult {
+
+    private final Corpus mCorpus;
+
+    public SingleSourceCorpusResult(Corpus corpus, String userQuery, SuggestionCursor cursor) {
+        super(userQuery, cursor);
+        mCorpus = corpus;
+    }
+
+    public Corpus getCorpus() {
+        return mCorpus;
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/Source.java b/src/com/android/quicksearchbox/Source.java
index 3a1056c..c322187 100644
--- a/src/com/android/quicksearchbox/Source.java
+++ b/src/com/android/quicksearchbox/Source.java
@@ -17,8 +17,10 @@
 package com.android.quicksearchbox;
 
 import android.content.ComponentName;
+import android.content.Intent;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.Bundle;
 
 /**
  * Interface for suggestion sources.
@@ -97,6 +99,12 @@
      */
     boolean queryAfterZeroResults();
 
+    boolean voiceSearchEnabled();
+
+    Intent createSearchIntent(String query, Bundle appData);
+
+    Intent createVoiceSearchIntent(Bundle appData);
+
     /**
      * Gets suggestions from this source.
      *
@@ -104,7 +112,7 @@
      * @param queryLimit An advisory maximum number of results that the source should return.
      * @return The suggestion results.
      */
-    SuggestionCursor getSuggestions(String query, int queryLimit);
+    SourceResult getSuggestions(String query, int queryLimit);
 
     /**
      * Updates a shorcut.
diff --git a/src/com/android/quicksearchbox/SourceLookup.java b/src/com/android/quicksearchbox/SourceLookup.java
deleted file mode 100644
index 2a5f31a..0000000
--- a/src/com/android/quicksearchbox/SourceLookup.java
+++ /dev/null
@@ -1,69 +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.content.ComponentName;
-
-import java.util.ArrayList;
-import java.util.Collection;
-
-/**
- * Defines operations for looking up information about a {@link Source}.
- */
-public interface SourceLookup {
-
-    /**
-     * Gets a suggestion source (or the current web search source) by component name.
-     *
-     * @return A source, or {@code null} if the source was not found or
-     *         {@code componentName} was null.
-     */
-    Source getSourceByComponentName(ComponentName componentName);
-
-    /**
-     * Returns the web search source.
-     *
-     * @return <code>null</code> only if there is no web search source available.
-     */
-    Source getWebSearchSource();
-
-    /**
-     * Checks whether web suggestions are enabled.
-     */
-    boolean areWebSuggestionsEnabled();
-
-    /**
-     * Checks if we trust the given source not to be spammy.
-     */
-    boolean isTrustedSource(Source source);
-
-    /**
-     * Checks if the source is enabled.
-     */
-    boolean isEnabledSource(Source source);
-
-    /**
-     * Gets all suggestion sources. This does not include any web search sources.
-     *
-     * @return A list of suggestion sources, including sources that are not enabled.
-     *         Callers must not modify the returned collection.
-     */
-    Collection<Source> getSources();
-
-    ArrayList<Source> getEnabledSources();
-
-}
diff --git a/src/com/android/quicksearchbox/SourceResult.java b/src/com/android/quicksearchbox/SourceResult.java
index fce01fe..20ea48f 100644
--- a/src/com/android/quicksearchbox/SourceResult.java
+++ b/src/com/android/quicksearchbox/SourceResult.java
@@ -16,47 +16,11 @@
 
 package com.android.quicksearchbox;
 
-import android.database.Cursor;
-
 /**
  * The result of getting suggestions from a single source.
- *
- * This class is similar to a cursor, in that it is moved to a suggestion, and then
- * the suggestion info can be read out.
- *
  */
-public class SourceResult extends CursorBackedSuggestionCursor {
+public interface SourceResult extends SuggestionCursor {
 
-    /** The suggestion source. */
-    private final Source mSource;
+    Source getSource();
 
-    /**
-     * Creates a result for a failed or canceled query.
-     */
-    public SourceResult(Source source, String userQuery) {
-        this(source, userQuery, null);
-    }
-
-    /**
-     * Creates a new source result.
-     *
-     * @param source The suggestion source. Must be non-null.
-     * @param cursor The cursor containing the suggestions. May be null.
-     */
-    public SourceResult(Source source, String userQuery, Cursor cursor) {
-        super(userQuery, cursor);
-        if (source == null) {
-            throw new NullPointerException("source is null");
-        }
-        mSource = source;
-    }
-
-    protected Source getSource() {
-        return mSource;
-    }
-
-    @Override
-    public String toString() {
-        return "SourceResult{source=" + mSource + ",query=" + getUserQuery() + "}";
-    }
 }
diff --git a/src/com/android/quicksearchbox/SourceTask.java b/src/com/android/quicksearchbox/SourceTask.java
index 86325ba..cb8b507 100644
--- a/src/com/android/quicksearchbox/SourceTask.java
+++ b/src/com/android/quicksearchbox/SourceTask.java
@@ -22,6 +22,4 @@
  */
 public interface SourceTask extends Runnable {
 
-    Source getSource();
-
 }
diff --git a/src/com/android/quicksearchbox/Sources.java b/src/com/android/quicksearchbox/Sources.java
deleted file mode 100644
index 34a8a13..0000000
--- a/src/com/android/quicksearchbox/Sources.java
+++ /dev/null
@@ -1,347 +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.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.SharedPreferences;
-import android.content.pm.PackageManager.NameNotFoundException;
-import android.database.ContentObserver;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.provider.Settings;
-import android.util.Log;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-
-/**
- * Maintains the list of all suggestion sources.
- */
-public class Sources implements SourceLookup {
-
-    // set to true to enable the more verbose debug logging for this file
-    private static final boolean DBG = false;
-    private static final String TAG = "QSB.Sources";
-
-    // Name of the preferences file used to store suggestion source preferences
-    public static final String PREFERENCES_NAME = "SuggestionSources";
-
-    // The key for the preference that holds the selected web search source
-    public static final String WEB_SEARCH_SOURCE_PREF = "web_search_source";
-
-    private static final int MSG_UPDATE_SOURCES = 0;
-
-    // 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 Context mContext;
-    private final Config mConfig;
-    private final SourceFactory mSourceFactory;
-    private final SearchManager mSearchManager;
-    private final SharedPreferences mPreferences;
-    private boolean mLoaded;
-
-    // Runs source updates
-    private final UpdateHandler mHandler;
-
-    // All available suggestion sources.
-    private HashMap<ComponentName,Source> mSources;
-
-    // The web search source to use. This is the source selected in the preferences,
-    // or the default source if no source has been selected.
-    private Source mSelectedWebSearchSource;
-
-    // All enabled suggestion sources. This does not include the web search source.
-    private ArrayList<Source> mEnabledSources;
-
-    // Updates the inclusion of the web search provider.
-    private ShowWebSuggestionsSettingChangeObserver mShowWebSuggestionsSettingChangeObserver;
-
-    /**
-     *
-     * @param context Used for looking up source information etc.
-     */
-    public Sources(Context context, Config config, SourceFactory sourceFactory) {
-        mContext = context;
-        mConfig = config;
-        mSourceFactory = sourceFactory;
-        mSearchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
-        mPreferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
-        mLoaded = false;
-        HandlerThread t = new HandlerThread("Sources.UpdateThread",
-                android.os.Process.THREAD_PRIORITY_BACKGROUND);
-        t.start();
-        mHandler = new UpdateHandler(t.getLooper());
-    }
-
-    /**
-     * Gets all suggestion sources. This does not include any web search sources.
-     *
-     * @return A list of suggestion sources, including sources that are not enabled.
-     *         Callers must not modify the returned list.
-     */
-    public synchronized Collection<Source> getSources() {
-        if (!mLoaded) {
-            throw new IllegalStateException("getSources(): sources not loaded.");
-        }
-        return mSources.values();
-    }
-
-    /** {@inheritDoc} */
-    public synchronized Source getSourceByComponentName(ComponentName componentName) {
-        Source source = mSources.get(componentName);
-        
-        // If the source was not found, back off to check the web source in case it's that.
-        if (source == null) {
-            if (mSelectedWebSearchSource != null &&
-                    mSelectedWebSearchSource.getComponentName().equals(componentName)) {
-                source = mSelectedWebSearchSource;
-            }
-        }
-        return source;
-    }
-
-    /**
-     * Gets all enabled suggestion sources.
-     *
-     * @return All enabled suggestion sources (does not include the web search source).
-     *         Callers must not modify the returned list.
-     */
-    public synchronized ArrayList<Source> getEnabledSources() {
-        if (!mLoaded) {
-            throw new IllegalStateException("getEnabledSources(): sources not loaded.");
-        }
-        return mEnabledSources;
-    }
-
-    /** {@inheritDoc} */
-    public synchronized Source getWebSearchSource() {
-        if (!mLoaded) {
-            throw new IllegalStateException("getSelectedWebSearchSource(): sources not loaded.");
-        }
-        return mSelectedWebSearchSource;
-    }
-
-    public boolean areWebSuggestionsEnabled() {
-        return (Settings.System.getInt(mContext.getContentResolver(),
-                Settings.System.SHOW_WEB_SUGGESTIONS,
-                1 /* default on until user actually changes it */) == 1);
-    }
-
-    /**
-     * Gets the preference key of the preference for whether the given source
-     * is enabled. The preference is stored in the {@link #PREFERENCES_NAME}
-     * preferences file.
-     */
-    public static String getSourceEnabledPreference(Source source) {
-        return "enable_source_" + source.getFlattenedComponentName();
-    }
-
-    // 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.
-                scheduleUpdateSources();
-            }
-        }
-    };
-
-    /* package */ void scheduleUpdateSources() {
-        if (DBG) Log.d(TAG, "scheduleUpdateSources()");
-        mHandler.sendEmptyMessageDelayed(MSG_UPDATE_SOURCES, UPDATE_SOURCES_DELAY_MILLIS);
-    }
-
-    private class UpdateHandler extends Handler {
-
-        public UpdateHandler(Looper looper) {
-            super(looper);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            switch (msg.what) {
-                case MSG_UPDATE_SOURCES:
-                    // Remove any duplicate update messages
-                    removeMessages(MSG_UPDATE_SOURCES);
-                    updateSources();
-                    break;
-            }
-        }
-    }
-
-    /**
-     * After calling, clients must call {@link #close()} when done with this object.
-     */
-    public synchronized void load() {
-        if (mLoaded) {
-            throw new IllegalStateException("load(): Already loaded.");
-        }
-
-        // Listen for searchables changes.
-        mContext.registerReceiver(mBroadcastReceiver,
-                new IntentFilter(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED));
-
-        // Listen for search preference changes.
-        mContext.registerReceiver(mBroadcastReceiver,
-                new IntentFilter(SearchManager.INTENT_ACTION_SEARCH_SETTINGS_CHANGED));
-        
-        mShowWebSuggestionsSettingChangeObserver =
-                new ShowWebSuggestionsSettingChangeObserver(mHandler);
-        mContext.getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(Settings.System.SHOW_WEB_SUGGESTIONS),
-                true,
-                mShowWebSuggestionsSettingChangeObserver);
-
-        // update list of sources
-        updateSources();
-
-        mLoaded = true;
-    }
-
-    /**
-     * Releases all resources used by this object. It is possible to call
-     * {@link #load()} again after calling this method.
-     */
-    public synchronized void close() {
-        if (!mLoaded) {
-            throw new IllegalStateException("close(): Not loaded.");
-        }
-        mContext.unregisterReceiver(mBroadcastReceiver);
-        mContext.getContentResolver().unregisterContentObserver(
-                mShowWebSuggestionsSettingChangeObserver);
-
-        mSources = null;
-        mSelectedWebSearchSource = null;
-        mEnabledSources = null;
-        mLoaded = false;
-    }
-
-    /**
-     * Loads the list of suggestion sources. This method is package private so that
-     * it can be called efficiently from inner classes.
-     */
-    /* package */ synchronized void updateSources() {
-        if (DBG) Log.d(TAG, "updateSources()");
-        mSources = new HashMap<ComponentName,Source>();
-        addExternalSources();
-
-        mEnabledSources = findEnabledSources();
-        mSelectedWebSearchSource = findWebSearchSource();
-    }
-
-    private void addExternalSources() {
-        ArrayList<Source> trusted = new ArrayList<Source>();
-        ArrayList<Source> untrusted = new ArrayList<Source>();
-        for (SearchableInfo searchable : mSearchManager.getSearchablesInGlobalSearch()) {
-            Source source = mSourceFactory.createSource(searchable);
-            if (source != null) {
-                if (DBG) Log.d(TAG, "Created source " + source);
-                if (isTrustedSource(source)) {
-                    trusted.add(source);
-                } else {
-                    untrusted.add(source);
-                }
-            }
-        }
-        for (Source s : trusted) {
-            addSource(s);
-        }
-        for (Source s : untrusted) {
-            addSource(s);
-        }
-    }
-
-    private void addSource(Source source) {
-        if (DBG) Log.d(TAG, "Adding source: " + source);
-        Source old = mSources.put(source.getComponentName(), source);
-        if (old != null) {
-            Log.w(TAG, "Replaced source " + old + " for " + source.getComponentName());
-        }
-    }
-
-    /**
-     * Computes the list of enabled suggestion sources.
-     */
-    private ArrayList<Source> findEnabledSources() {
-        ArrayList<Source> enabledSources = new ArrayList<Source>();
-        for (Source source : mSources.values()) {
-            if (isEnabledSource(source)) {
-                if (DBG) Log.d(TAG, "Adding enabled source " + source);
-                enabledSources.add(source);
-            }
-        }
-        return enabledSources;
-    }
-
-    public synchronized boolean isEnabledSource(Source source) {
-        if (source == null) return false;
-        boolean defaultEnabled = isTrustedSource(source);
-        if (mPreferences == null) {
-            Log.w(TAG, "Search preferences " + PREFERENCES_NAME + " not found.");
-            return true;
-        }
-        String sourceEnabledPref = getSourceEnabledPreference(source);
-        return mPreferences.getBoolean(sourceEnabledPref, defaultEnabled);
-    }
-
-    public synchronized boolean isTrustedSource(Source source) {
-        if (source == null) return false;
-        String packageName = source.getComponentName().getPackageName();
-        return mConfig.isTrustedSource(packageName);
-    }
-
-    /**
-     * Finds the selected web search source.
-     */
-    private Source findWebSearchSource() {
-        return mSourceFactory.createWebSearchSource();
-    }
-
-    /**
-     * ContentObserver which updates the list of enabled sources to include or exclude
-     * the web search provider depending on the state of the
-     * {@link Settings.System#SHOW_WEB_SUGGESTIONS} setting.
-     */
-    private class ShowWebSuggestionsSettingChangeObserver extends ContentObserver {
-        public ShowWebSuggestionsSettingChangeObserver(Handler handler) {
-            super(handler);
-        }
-
-        @Override
-        public void onChange(boolean selfChange) {
-            scheduleUpdateSources();
-        }
-    }
-}
diff --git a/src/com/android/quicksearchbox/SuggestionCursor.java b/src/com/android/quicksearchbox/SuggestionCursor.java
index 9c8740e..27912b7 100644
--- a/src/com/android/quicksearchbox/SuggestionCursor.java
+++ b/src/com/android/quicksearchbox/SuggestionCursor.java
@@ -16,85 +16,15 @@
 
 package com.android.quicksearchbox;
 
-import android.content.ComponentName;
-import android.content.Context;
-import android.content.Intent;
 import android.database.DataSetObserver;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Bundle;
 
+
+/**
+ * A sequence of suggestions, with a current position.
+ */
 public interface SuggestionCursor {
 
     /**
-     * The user query that returned these suggestions.
-     */
-    String getUserQuery();
-
-    /**
-     * Gets the component name of the source that produced this result.
-     */
-    ComponentName getSourceComponentName();
-
-    /**
-     * Gets the string that will be logged for this suggestion when logging
-     * suggestion clicks etc.
-     */
-    String getLogName();
-
-    /**
-     * Gets the localized, human-readable label for the source that produced this result.
-     */
-    CharSequence getSourceLabel();
-
-    /**
-     * Gets the icon for the source that produced this result.
-     */
-    Drawable getSourceIcon();
-
-    /**
-     * Gets the icon URI for the source that produced this result.
-     */
-    Uri getSourceIconUri();
-
-    /**
-     * Gets an icon by ID. Used for getting the icons returned by {@link #getSuggestionIcon1()}
-     * and {@link #getSuggestionIcon2()}.
-     */
-    Drawable getIcon(String iconId);
-
-    /**
-     * Gets the URI for an icon.
-     */
-    Uri getIconUri(String iconId);
-
-    /**
-     * Checks whether this result represents a failed suggestion query.
-     */
-    boolean isFailed();
-
-    /**
-     * Closes this suggestion result. 
-     */
-    void close();
-
-    /**
-     * Register an observer that is called when changes happen to the contents
-     * of this cursor's data set.
-     *
-     * @param observer the object that gets notified when the data set changes.
-     */
-    public void registerDataSetObserver(DataSetObserver observer);
-
-    /**
-     * Unregister an observer that has previously been registered with this cursor
-     * via {@link #registerDataSetObserver(DataSetObserver)}
-     *
-     * @param observer the object to unregister.
-     */
-    public void unregisterDataSetObserver(DataSetObserver observer);
-
-    /**
      * Gets the number of suggestions in this result.
      *
      * @return The number of suggestions, or {@code 0} if this result represents a failed query.
@@ -115,9 +45,34 @@
     int getPosition();
 
     /**
-     * Gets the text to put in the search box when the current suggestion is selected.
+     * Frees any resources used by this cursor.
      */
-    String getSuggestionDisplayQuery();
+    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);
+
+    /**
+     * Gets the source that produced the current suggestion.
+     */
+    Source getSuggestionSource();
+
+    /**
+     * Gets the query that the user typed to get this suggestion.
+     */
+    String getUserQuery();
 
     /**
      * Gets the shortcut ID of the current suggestion.
@@ -150,27 +105,21 @@
     /**
      * Gets the left-hand-side icon for the current suggestion.
      *
-     * @return A string that can be passed to {@link #getIcon()}.
+     * @return A string that can be passed to {@link Source#getIcon(String)}.
      */
     String getSuggestionIcon1();
 
     /**
      * Gets the right-hand-side icon for the current suggestion.
      *
-     * @return A string that can be passed to {@link #getIcon()}.
+     * @return A string that can be passed to {@link Source#getIcon(String)}.
      */
     String getSuggestionIcon2();
 
     /**
-     * Gets the intent that this suggestion launches.
-     *
-     * @param context Used for resolving the intent target.
-     * @param actionKey
-     * @param actionMsg
-     * @return
+     * Gets the intent action for the current suggestion.
      */
-    Intent getSuggestionIntent(Context context, Bundle appSearchData,
-            int actionKey, String actionMsg);
+    String getSuggestionIntentAction();
 
     /**
      * Gets the extra data associated with this suggestion's intent.
@@ -183,6 +132,13 @@
     String getSuggestionIntentDataString();
 
     /**
+     * Gets the data associated with this suggestion's intent.
+     */
+    String getSuggestionQuery();
+
+    String getSuggestionDisplayQuery();
+
+    /**
      * Gets a unique key that identifies this suggestion. This is used to avoid
      * duplicate suggestions in the promoted list. This key should be based on
      * the intent of the suggestion.
@@ -190,30 +146,7 @@
     String getSuggestionKey();
 
     /**
-     * Gets the first suggestion text line as styled text.
+     * Gets the suggestion log type for the current suggestion.
      */
-    CharSequence getSuggestionFormattedText1();
-
-    /**
-     * Gets the second suggestion text line as styled text.
-     */
-    CharSequence getSuggestionFormattedText2();
-
-    /**
-     * Gets the first suggestion icon.
-     */
-    Drawable getSuggestionDrawableIcon1();
-
-    /**
-     * Gets the second suggestion icon.
-     */
-    Drawable getSuggestionDrawableIcon2();
-
-    /**
-     * Gets the action message for a key code for the current suggestion.
-     *
-     * @param keyCode Key code, see {@link android.view.KeyEvent}.
-     * @return The action message for the key, or {@code null} if there is none.
-     */
-    String getActionKeyMsg(int keyCode);
+    String getSuggestionLogType();
 }
diff --git a/src/com/android/quicksearchbox/SuggestionCursorWrapper.java b/src/com/android/quicksearchbox/SuggestionCursorWrapper.java
new file mode 100644
index 0000000..8abeca0
--- /dev/null
+++ b/src/com/android/quicksearchbox/SuggestionCursorWrapper.java
@@ -0,0 +1,70 @@
+/*
+ * 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.DataSetObserver;
+
+/**
+ * A suggestion cursor that delegates all methods to another SuggestionCursor.
+ */
+public class SuggestionCursorWrapper extends AbstractSuggestionCursorWrapper {
+
+    private final SuggestionCursor mCursor;
+
+    public SuggestionCursorWrapper(String userQuery, SuggestionCursor cursor) {
+        super(userQuery);
+        mCursor = cursor;
+    }
+
+    public void close() {
+        if (mCursor != null) {
+            mCursor.close();
+        }
+    }
+
+    public int getCount() {
+        return mCursor == null ? 0 : mCursor.getCount();
+    }
+
+    public int getPosition() {
+        return mCursor == null ? 0 : mCursor.getPosition();
+    }
+
+    public void moveTo(int pos) {
+        if (mCursor != null) {
+            mCursor.moveTo(pos);
+        }
+    }
+
+    public void registerDataSetObserver(DataSetObserver observer) {
+        if (mCursor != null) {
+            mCursor.registerDataSetObserver(observer);
+        }
+    }
+
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        if (mCursor != null) {
+            mCursor.unregisterDataSetObserver(observer);
+        }
+    }
+
+    @Override
+    protected SuggestionCursor current() {
+        return mCursor;
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/SuggestionPosition.java b/src/com/android/quicksearchbox/SuggestionPosition.java
index 44e56a5..3f514b2 100644
--- a/src/com/android/quicksearchbox/SuggestionPosition.java
+++ b/src/com/android/quicksearchbox/SuggestionPosition.java
@@ -39,7 +39,7 @@
     /**
      * Gets the suggestion cursor, moved to point to the right suggestion.
      */
-    public SuggestionCursor getSuggestion() {
+    protected SuggestionCursor current() {
         mCursor.moveTo(mPosition);
         return mCursor;
     }
diff --git a/src/com/android/quicksearchbox/Suggestions.java b/src/com/android/quicksearchbox/Suggestions.java
index 535eeb9..5fa4759 100644
--- a/src/com/android/quicksearchbox/Suggestions.java
+++ b/src/com/android/quicksearchbox/Suggestions.java
@@ -16,7 +16,6 @@
 
 package com.android.quicksearchbox;
 
-import android.content.ComponentName;
 import android.database.DataSetObservable;
 import android.database.DataSetObserver;
 import android.util.Log;
@@ -37,7 +36,7 @@
     private final String mQuery;
 
     /** The number of sources that are expected to report. */
-    private final int mExpectedSourceCount;
+    private final int mExpectedCorpusCount;
 
     /**
      * The observers that want notifications of changes to the published suggestions.
@@ -50,13 +49,13 @@
      * in the order that they were published.
      * This object may only be accessed on the UI thread.
      * */
-    private final ArrayList<SuggestionCursor> mSourceResults;
+    private final ArrayList<CorpusResult> mCorpusResults;
 
     /**
      * All {@link SuggestionCursor} objects that have been published so far.
      * This object may only be accessed on the UI thread.
      * */
-    private final HashMap<ComponentName,SuggestionCursor> mSourceResultsBySource;
+    private final HashMap<Corpus,CorpusResult> mResultsByCorpus;
 
     private SuggestionCursor mShortcuts;
 
@@ -72,16 +71,16 @@
     /**
      * Creates a new empty Suggestions.
      *
-     * @param expectedSourceCount The number of sources that are expected to report.
+     * @param expectedCorpusCount The number of sources that are expected to report.
      */
     public Suggestions(Promoter promoter, int maxPromoted,
-            String query, int expectedSourceCount) {
+            String query, int expectedCorpusCount) {
         mPromoter = promoter;
         mMaxPromoted = maxPromoted;
         mQuery = query;
-        mExpectedSourceCount = expectedSourceCount;
-        mSourceResults = new ArrayList<SuggestionCursor>(mExpectedSourceCount);
-        mSourceResultsBySource = new HashMap<ComponentName,SuggestionCursor>(mExpectedSourceCount);
+        mExpectedCorpusCount = expectedCorpusCount;
+        mCorpusResults = new ArrayList<CorpusResult>(mExpectedCorpusCount);
+        mResultsByCorpus = new HashMap<Corpus,CorpusResult>(mExpectedCorpusCount);
         mPromoted = null;  // will be set by updatePromoted()
     }
 
@@ -93,7 +92,7 @@
      * Gets the number of sources that are expected to report.
      */
     public int getExpectedSourceCount() {
-        return mExpectedSourceCount;
+        return mExpectedCorpusCount;
     }
 
     /**
@@ -143,11 +142,11 @@
             mShortcuts.close();
             mShortcuts = null;
         }
-        for (SuggestionCursor result : mSourceResults) {
+        for (CorpusResult result : mCorpusResults) {
             result.close();
         }
-        mSourceResults.clear();
-        mSourceResultsBySource.clear();
+        mCorpusResults.clear();
+        mResultsByCorpus.clear();
     }
 
     public boolean isClosed() {
@@ -168,7 +167,7 @@
      */
     public boolean isDone() {
         // TODO: Handle early completion because we have all the results we want.
-        return mSourceResults.size() >= mExpectedSourceCount;
+        return mCorpusResults.size() >= mExpectedCorpusCount;
     }
 
     /**
@@ -186,22 +185,20 @@
     }
 
     /**
-     * Adds a source result. Must be called on the UI thread, or before this
+     * Adds a corpus result. Must be called on the UI thread, or before this
      * object is seen by the UI thread.
-     *
-     * @param sourceResult The source result.
      */
-    public void addSourceResult(SuggestionCursor sourceResult) {
+    public void addCorpusResult(CorpusResult corpusResult) {
         if (mClosed) {
-            sourceResult.close();
+            corpusResult.close();
             return;
         }
-        if (!mQuery.equals(sourceResult.getUserQuery())) {
+        if (!mQuery.equals(corpusResult.getUserQuery())) {
           throw new IllegalArgumentException("Got result for wrong query: "
-                + mQuery + " != " + sourceResult.getUserQuery());
+                + mQuery + " != " + corpusResult.getUserQuery());
         }
-        mSourceResults.add(sourceResult);
-        mSourceResultsBySource.put(sourceResult.getSourceComponentName(), sourceResult);
+        mCorpusResults.add(corpusResult);
+        mResultsByCorpus.put(corpusResult.getCorpus(), corpusResult);
         mPromoted = null;
         notifyDataSetChanged();
     }
@@ -211,40 +208,22 @@
         if (mPromoter == null) {
             return;
         }
-        mPromoter.pickPromoted(mShortcuts, mSourceResults, mMaxPromoted, mPromoted);
+        mPromoter.pickPromoted(mShortcuts, mCorpusResults, mMaxPromoted, mPromoted);
     }
 
     /**
-     * Gets a given source result.
+     * Gets a given corpus result.
      * Must be called on the UI thread, or before this object is seen by the UI thread.
      *
-     * @param sourcePos 
-     * @return The source result at the given position.
-     * @throws IndexOutOfBoundsException If {@code sourcePos < 0} or
-     *         {@code sourcePos >= getSourceCount()}.
-     */
-    public SuggestionCursor getSourceResult(int sourcePos) {
-        if (mClosed) {
-            throw new IllegalStateException("Called getSourceResult(" + sourcePos
-                + ") when closed.");
-        }
-        return mSourceResults.get(sourcePos);
-    }
-
-    /**
-     * Gets a given source result.
-     * Must be called on the UI thread, or before this object is seen by the UI thread.
-     *
-     * @param source Source name.
+     * @param corpus corpus
      * @return The source result for the given source. {@code null} if the source has not
      *         yet returned.
      */
-    public SuggestionCursor getSourceResult(ComponentName source) {
+    public CorpusResult getCorpusResult(Corpus corpus) {
         if (mClosed) {
-            throw new IllegalStateException("Called getSourceResult(" + source
-                + ") when closed.");
+            throw new IllegalStateException("getCorpusResult(" + corpus + ") when closed.");
         }
-        return mSourceResultsBySource.get(source);
+        return mResultsByCorpus.get(corpus);
     }
 
     /**
@@ -255,7 +234,7 @@
         if (mClosed) {
             throw new IllegalStateException("Called getSourceCount() when closed.");
         }
-        return mSourceResults == null ? 0 : mSourceResults.size();
+        return mCorpusResults == null ? 0 : mCorpusResults.size();
     }
 
     private class MyShortcutsObserver extends DataSetObserver {
diff --git a/src/com/android/quicksearchbox/SuggestionsProvider.java b/src/com/android/quicksearchbox/SuggestionsProvider.java
index 588b3ca..55eefd9 100644
--- a/src/com/android/quicksearchbox/SuggestionsProvider.java
+++ b/src/com/android/quicksearchbox/SuggestionsProvider.java
@@ -28,7 +28,7 @@
      */
     Suggestions getSuggestions(String query);
 
-    ArrayList<Source> getOrderedSources();
+    ArrayList<Corpus> getOrderedCorpora();
 
     void close();
 }
diff --git a/src/com/android/quicksearchbox/WebCorpus.java b/src/com/android/quicksearchbox/WebCorpus.java
new file mode 100644
index 0000000..8f72bbc
--- /dev/null
+++ b/src/com/android/quicksearchbox/WebCorpus.java
@@ -0,0 +1,211 @@
+/*
+ * 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 com.android.common.Patterns;
+
+import android.app.SearchManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.speech.RecognizerIntent;
+import android.util.Log;
+import android.webkit.URLUtil;
+
+/**
+ * The web search source.
+ */
+public class WebCorpus extends AbstractCorpus {
+
+    private static final boolean DBG = true;
+    private static final String TAG = "QSB.WebSource";
+
+    private static final String WEB_CORPUS_NAME = "web";
+
+    private final Context mContext;
+
+    private final Source mWebSearchSource;
+    private final Source mBrowserSource;
+
+    public WebCorpus(Context context, Source webSearchSource, Source browserSource) {
+        mContext = context;
+        mWebSearchSource = webSearchSource;
+        mBrowserSource = browserSource;
+    }
+
+    protected Context getContext() {
+        return mContext;
+    }
+
+    public CharSequence getLabel() {
+        return getContext().getText(R.string.corpus_label_web);
+    }
+
+    public Intent createSearchIntent(String query, Bundle appData) {
+        return createWebIntent(query, appData);
+    }
+
+    public static Intent createWebIntent(String query, Bundle appData) {
+        return Patterns.WEB_URL.matcher(query).matches()
+                ? 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);
+        }
+        // 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 url) {
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.addCategory(Intent.CATEGORY_BROWSABLE);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        url = URLUtil.guessUrl(url);
+        intent.setData(Uri.parse(url));
+        return intent;
+    }
+
+    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;
+    }
+
+    public Drawable getCorpusIcon() {
+        return getContext().getResources().getDrawable(R.drawable.corpus_icon_web);
+    }
+
+    public Uri getCorpusIconUri() {
+        int resourceId = R.drawable.corpus_icon_web;
+        return new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+                .authority(getContext().getPackageName())
+                .appendEncodedPath(String.valueOf(resourceId))
+                .build();
+    }
+
+    public String getName() {
+        return WEB_CORPUS_NAME;
+    }
+
+    public int getQueryThreshold() {
+        return 0;
+    }
+
+    public boolean queryAfterZeroResults() {
+        return true;
+    }
+
+    public boolean voiceSearchEnabled() {
+        return true;
+    }
+
+    public boolean isWebCorpus() {
+        return true;
+    }
+
+    public CharSequence getSettingsDescription() {
+        return getContext().getText(R.string.corpus_description_web);
+    }
+
+    public CorpusResult getSuggestions(String query, int queryLimit) {
+        // TODO: Should run web and browser queries in parallel
+        SuggestionCursor webCursor = null;
+        try {
+            webCursor = mWebSearchSource.getSuggestions(query, queryLimit);
+        } catch (RuntimeException ex) {
+            Log.e(TAG, "Error querying web search source", ex);
+        }
+        SuggestionCursor browserCursor = null;
+        try {
+            browserCursor = mBrowserSource.getSuggestions(query, queryLimit);
+        } catch (RuntimeException ex) {
+            Log.e(TAG, "Error querying browser search source", ex);
+        }
+
+        WebResult c = new WebResult(query, webCursor, browserCursor);
+        if (DBG) Log.d(TAG, "Returning " + c.getCount() + " suggestions");
+        return c;
+    }
+
+    private class WebResult extends ListSuggestionCursor implements CorpusResult {
+
+        private SuggestionCursor mWebCursor;
+
+        private SuggestionCursor mBrowserCursor;
+
+        public WebResult(String userQuery, SuggestionCursor webCursor,
+                SuggestionCursor browserCursor) {
+            super(userQuery);
+            mWebCursor = webCursor;
+            mBrowserCursor = browserCursor;
+
+            if (mBrowserCursor != null && mBrowserCursor.getCount() > 0) {
+                if (DBG) Log.d(TAG, "Adding browser suggestion");
+                add(new SuggestionPosition(mBrowserCursor, 0));
+            }
+
+            if (mWebCursor != null) {
+                int count = mWebCursor.getCount();
+                for (int i = 0; i < count; i++) {
+                    if (DBG) Log.d(TAG, "Adding web suggestion");
+                    add(new SuggestionPosition(mWebCursor, i));
+                }
+            }
+        }
+
+        public Corpus getCorpus() {
+            return WebCorpus.this;
+        }
+
+        @Override
+        public void close() {
+            super.close();
+            if (mWebCursor != null) {
+                mWebCursor.close();
+            }
+            if (mBrowserCursor != null) {
+                mBrowserCursor.close();
+            }
+        }
+    }
+}
diff --git a/src/com/android/quicksearchbox/WebSource.java b/src/com/android/quicksearchbox/WebSource.java
deleted file mode 100644
index 71b0e40..0000000
--- a/src/com/android/quicksearchbox/WebSource.java
+++ /dev/null
@@ -1,47 +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.SearchableInfo;
-import android.content.Context;
-import android.content.pm.PackageManager.NameNotFoundException;
-
-/**
- * The web search source.
- */
-public class WebSource extends SearchableSource {
-
-    private static final boolean DBG = true;
-    private static final String TAG = "QSB.WebSource";
-
-    private static final String WEB_SOURCE_LOG_NAME = "web";
-
-    public WebSource(Context context, SearchableInfo searchable) throws NameNotFoundException {
-        super(context, searchable);
-    }
-
-    @Override
-    public String getLogName() {
-        return WEB_SOURCE_LOG_NAME;
-    }
-
-    @Override
-    public boolean isWebSuggestionSource() {
-        return true;
-    }
-
-}
diff --git a/src/com/android/quicksearchbox/ui/SourcesAdapter.java b/src/com/android/quicksearchbox/ui/CorporaAdapter.java
similarity index 64%
rename from src/com/android/quicksearchbox/ui/SourcesAdapter.java
rename to src/com/android/quicksearchbox/ui/CorporaAdapter.java
index 2286826..26e5dea 100644
--- a/src/com/android/quicksearchbox/ui/SourcesAdapter.java
+++ b/src/com/android/quicksearchbox/ui/CorporaAdapter.java
@@ -16,8 +16,9 @@
 
 package com.android.quicksearchbox.ui;
 
-import com.android.quicksearchbox.Source;
-import com.android.quicksearchbox.SuggestionsProvider;
+import com.android.quicksearchbox.Corpora;
+import com.android.quicksearchbox.Corpus;
+import com.android.quicksearchbox.CorpusRanker;
 
 import android.view.View;
 import android.view.ViewGroup;
@@ -28,33 +29,27 @@
 /**
  * Adapter for showing a list of sources in the source selection activity.
  */
-public class SourcesAdapter extends BaseAdapter {
+public class CorporaAdapter extends BaseAdapter {
 
     private final SuggestionViewFactory mViewFactory;
 
-    private final SuggestionsProvider mProvider;
+    private ArrayList<Corpus> mRankedEnabledCorpora;
 
-    private ArrayList<Source> mEnabledSources;
-
-    public SourcesAdapter(SuggestionViewFactory viewFactory, SuggestionsProvider provider) {
+    public CorporaAdapter(SuggestionViewFactory viewFactory, Corpora corpora,
+            CorpusRanker ranker) {
         mViewFactory = viewFactory;
-        mProvider = provider;
-        updateSources();
-    }
-
-    private void updateSources() {
-        mEnabledSources = mProvider.getOrderedSources();
+        mRankedEnabledCorpora = ranker.rankCorpora(corpora.getEnabledCorpora());
     }
 
     public int getCount() {
-        return 1 + mEnabledSources.size();
+        return 1 + mRankedEnabledCorpora.size();
     }
 
-    public Source getItem(int position) {
+    public Corpus getItem(int position) {
         if (position == 0) {
             return null;
         } else {
-            return mEnabledSources.get(position - 1);
+            return mRankedEnabledCorpora.get(position - 1);
         }
     }
 
@@ -63,17 +58,17 @@
     }
 
     public View getView(int position, View convertView, ViewGroup parent) {
-        SourceView view = (SourceView) convertView;
+        CorpusView view = (CorpusView) convertView;
         if (view == null) {
             view = mViewFactory.createSourceView(parent);
         }
-        Source source = getItem(position);
-        if (source == null) {
+        Corpus corpus = getItem(position);
+        if (corpus == null) {
             view.setIcon(mViewFactory.getGlobalSearchIcon());
             view.setLabel(mViewFactory.getGlobalSearchLabel());
         } else {
-            view.setIcon(source.getSourceIcon());
-            view.setLabel(source.getLabel());
+            view.setIcon(corpus.getCorpusIcon());
+            view.setLabel(corpus.getLabel());
         }
         return view;
     }
diff --git a/src/com/android/quicksearchbox/ui/CorpusIndicator.java b/src/com/android/quicksearchbox/ui/CorpusIndicator.java
new file mode 100644
index 0000000..bababe9
--- /dev/null
+++ b/src/com/android/quicksearchbox/ui/CorpusIndicator.java
@@ -0,0 +1,60 @@
+/*
+ * 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.ui;
+
+import com.android.quicksearchbox.R;
+
+import android.graphics.drawable.Drawable;
+import android.view.View;
+import android.widget.ImageButton;
+
+/**
+ * Utilities for setting up the corpus indicator.
+ */
+public class CorpusIndicator {
+
+    public static final int ICON_VIEW_ID = R.id.search_source_selector_icon;
+
+    private final View mView;
+
+    private final ImageButton mIconView;
+
+    public CorpusIndicator(View view) {
+        mView = view;
+        mIconView = (ImageButton) view.findViewById(ICON_VIEW_ID);
+    }
+
+    /**
+     * Sets the icon displayed in the search source selector.
+     */
+    public void setSourceIcon(Drawable icon) {
+        mIconView.setImageDrawable(icon);
+    }
+
+    public void setVisibility(int visibility) {
+        mView.setVisibility(visibility);
+    }
+
+    public void setOnKeyListener(View.OnKeyListener listener) {
+        mIconView.setOnKeyListener(listener);
+    }
+
+    public void setOnClickListener(View.OnClickListener listener) {
+        mIconView.setOnClickListener(listener);
+    }
+
+}
diff --git a/src/com/android/quicksearchbox/ui/SourceView.java b/src/com/android/quicksearchbox/ui/CorpusView.java
similarity index 82%
rename from src/com/android/quicksearchbox/ui/SourceView.java
rename to src/com/android/quicksearchbox/ui/CorpusView.java
index e9a8128..0a291af 100644
--- a/src/com/android/quicksearchbox/ui/SourceView.java
+++ b/src/com/android/quicksearchbox/ui/CorpusView.java
@@ -27,21 +27,18 @@
 
 
 /**
- * A source in the source selection list.
+ * A corpus in the corpus selection list.
  */
-public class SourceView extends RelativeLayout {
-
-    private static final boolean DBG = true;
-    private static final String TAG = "QSB.SourceView";
+public class CorpusView extends RelativeLayout {
 
     private ImageView mIcon;
     private TextView mLabel;
 
-    public SourceView(Context context, AttributeSet attrs) {
+    public CorpusView(Context context, AttributeSet attrs) {
         super(context, attrs);
     }
 
-    public SourceView(Context context) {
+    public CorpusView(Context context) {
         super(context);
     }
 
diff --git a/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java b/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java
index 977b744..f7a6d1a 100644
--- a/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java
+++ b/src/com/android/quicksearchbox/ui/DefaultSuggestionView.java
@@ -17,11 +17,12 @@
 package com.android.quicksearchbox.ui;
 
 import com.android.quicksearchbox.R;
+import com.android.quicksearchbox.Source;
 import com.android.quicksearchbox.SuggestionCursor;
-import com.android.quicksearchbox.SuggestionPosition;
 
 import android.content.Context;
 import android.graphics.drawable.Drawable;
+import android.text.Html;
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -40,15 +41,6 @@
     private static final boolean DBG = false;
     private static final String TAG = "QSB.SuggestionView";
 
-    /**
-     * The cursor that contains the current suggestion.
-     */
-    private SuggestionCursor mCursor;
-    /**
-     * The position within the cursor of the current suggestion.
-     */
-    private int mPos;
-
     private TextView mText1;
     private TextView mText2;
     private ImageView mIcon1;
@@ -75,23 +67,12 @@
         mIcon2 = (ImageView) findViewById(R.id.icon2);
     }
 
-    /**
-     * Gets the suggestion that this view is showing.
-     */
-    public SuggestionPosition getSuggestionPosition() {
-        if (mCursor == null) {
-            throw new IllegalStateException("No cursor in SuggestionView");
-        }
-        return new SuggestionPosition(mCursor, mPos);
-    }
-
     public void bindAsSuggestion(SuggestionCursor suggestion) {
-        mCursor = suggestion;
-        mPos = suggestion.getPosition();
-        CharSequence text1 = suggestion.getSuggestionFormattedText1();
-        CharSequence text2 = suggestion.getSuggestionFormattedText2();
-        Drawable icon1 = suggestion.getSuggestionDrawableIcon1();
-        Drawable icon2 = suggestion.getSuggestionDrawableIcon2();
+        String format = suggestion.getSuggestionFormat();
+        CharSequence text1 = formatText(suggestion.getSuggestionText1(), format);
+        CharSequence text2 = formatText(suggestion.getSuggestionText2(), format);
+        Drawable icon1 = getSuggestionDrawableIcon1(suggestion);
+        Drawable icon2 = getSuggestionDrawableIcon2(suggestion);
         if (DBG) {
             Log.d(TAG, "bindAsSuggestion(), text1=" + text1 + ",text2=" + text2
                     + ",icon1=" + icon1 + ",icon2=" + icon2);
@@ -102,6 +83,36 @@
         setIcon2(icon2);
     }
 
+    public Drawable getSuggestionDrawableIcon1(SuggestionCursor suggestion) {
+        Source source = suggestion.getSuggestionSource();
+        String icon1Id = suggestion.getSuggestionIcon1();
+        Drawable icon1 = source.getIcon(icon1Id);
+        return icon1 == null ? source.getSourceIcon() : icon1;
+    }
+
+    public Drawable getSuggestionDrawableIcon2(SuggestionCursor suggestion) {
+        Source source = suggestion.getSuggestionSource();
+        return source.getIcon(suggestion.getSuggestionIcon2());
+    }
+
+    private CharSequence formatText(String str, String format) {
+        boolean isHtml = "html".equals(format);
+        if (isHtml && looksLikeHtml(str)) {
+            return Html.fromHtml(str);
+        } else {
+            return str;
+        }
+    }
+
+    private boolean looksLikeHtml(String str) {
+        if (TextUtils.isEmpty(str)) return false;
+        for (int i = str.length() - 1; i >= 0; i--) {
+            char c = str.charAt(i);
+            if (c == '>' || c == '&') return true;
+        }
+        return false;
+    }
+
     /**
      * Sets the first text line.
      */
diff --git a/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.java b/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.java
index 29c01f7..161c9de 100644
--- a/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.java
+++ b/src/com/android/quicksearchbox/ui/DelayingSuggestionsAdapter.java
@@ -39,11 +39,13 @@
         super(viewFactory);
     }
 
+    @Override
     public void close() {
         setPendingSuggestions(null);
         super.close();
     }
 
+    @Override
     public void setSuggestions(Suggestions suggestions) {
         if (suggestions == null) {
             super.setSuggestions(null);
@@ -66,7 +68,7 @@
      */
     private boolean shouldPublish(Suggestions suggestions) {
         if (suggestions.isDone()) return true;
-        SuggestionCursor cursor = getSourceCursor(suggestions, getSource());
+        SuggestionCursor cursor = getCorpusCursor(suggestions, getCorpus());
         return cursor != null && cursor.getCount() > 0;
     }
 
diff --git a/src/com/android/quicksearchbox/ui/SearchSourceSelector.java b/src/com/android/quicksearchbox/ui/SearchSourceSelector.java
deleted file mode 100644
index c99b8fc..0000000
--- a/src/com/android/quicksearchbox/ui/SearchSourceSelector.java
+++ /dev/null
@@ -1,110 +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.ui;
-
-import com.android.quicksearchbox.R;
-
-import android.app.SearchManager;
-import android.content.ActivityNotFoundException;
-import android.content.ComponentName;
-import android.content.Intent;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.util.Log;
-import android.view.View;
-import android.widget.ImageButton;
-
-import java.util.List;
-
-/**
- * Utilities for setting up the search source selector.
- *
- * They should keep the same look and feel as much as possible,
- * but only the intent details must absolutely stay in sync.
- *
- * @hide
- */
-public class SearchSourceSelector {
-
-    private static final String TAG = "SearchSourceSelector";
-
-    private static final String SCHEME_COMPONENT = "android.component";
-
-    public static final int ICON_VIEW_ID = R.id.search_source_selector_icon;
-
-    private final View mView;
-
-    private final ImageButton mIconView;
-
-    public SearchSourceSelector(View view) {
-        mView = view;
-        mIconView = (ImageButton) view.findViewById(ICON_VIEW_ID);
-    }
-
-    /**
-     * Sets the icon displayed in the search source selector.
-     */
-    public void setSourceIcon(Drawable icon) {
-        mIconView.setImageDrawable(icon);
-    }
-
-    public void setVisibility(int visibility) {
-        mView.setVisibility(visibility);
-    }
-
-    public static ComponentName getSource(Intent intent) {
-        return uriToComponentName(intent.getData());
-    }
-
-    public static void setSource(Intent intent, ComponentName source) {
-        if (source != null) {
-            intent.setData(componentNameToUri(source));
-        }
-    }
-
-    private static Uri componentNameToUri(ComponentName name) {
-        if (name == null) return null;
-        return new Uri.Builder()
-                .scheme(SCHEME_COMPONENT)
-                .authority(name.getPackageName())
-                .path(name.getClassName())
-                .build();
-    }
-
-    private static ComponentName uriToComponentName(Uri uri) {
-        if (uri == null) return null;
-        if (!SCHEME_COMPONENT.equals(uri.getScheme())) return null;
-        String pkg = uri.getAuthority();
-        List<String> path = uri.getPathSegments();
-        if (path == null || path.isEmpty()) return null;
-        String cls = path.get(0);
-        if (TextUtils.isEmpty(pkg) || TextUtils.isEmpty(cls)) return null;
-        return new ComponentName(pkg, cls);
-    }
-
-    public void setOnKeyListener(View.OnKeyListener listener) {
-        mIconView.setOnKeyListener(listener);
-    }
-
-    public void setOnClickListener(View.OnClickListener listener) {
-        mIconView.setOnClickListener(listener);
-    }
-
-}
diff --git a/src/com/android/quicksearchbox/ui/SuggestionClickListener.java b/src/com/android/quicksearchbox/ui/SuggestionClickListener.java
index a26beda..9cc3b10 100644
--- a/src/com/android/quicksearchbox/ui/SuggestionClickListener.java
+++ b/src/com/android/quicksearchbox/ui/SuggestionClickListener.java
@@ -16,12 +16,20 @@
 
 package com.android.quicksearchbox.ui;
 
-import com.android.quicksearchbox.SuggestionPosition;
-
 /**
  * Listener interface for clicks on suggestions.
  */
 public interface SuggestionClickListener {
-    void onSuggestionClicked(SuggestionPosition suggestion);
-    boolean onSuggestionLongClicked(SuggestionPosition suggestion);
+    /**
+     * 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);
 }
diff --git a/src/com/android/quicksearchbox/ui/SuggestionSelectionListener.java b/src/com/android/quicksearchbox/ui/SuggestionSelectionListener.java
index 8b3ab67..91f6e54 100644
--- a/src/com/android/quicksearchbox/ui/SuggestionSelectionListener.java
+++ b/src/com/android/quicksearchbox/ui/SuggestionSelectionListener.java
@@ -16,17 +16,20 @@
 
 package com.android.quicksearchbox.ui;
 
-import com.android.quicksearchbox.SuggestionPosition;
 
 /**
  * Listener interface for suggestion selection.
  */
 public interface SuggestionSelectionListener {
     /**
-     * Called when the suggestion selection changes.
+     * Called when a suggestion is selected
      *
-     * @param suggestion The new selected suggestion, or {@code null} if
-     *        no suggestion is now selected.
+     * @param position Position of the new selected suggestion.
      */
-    void onSelectionChanged(SuggestionPosition suggestion);
+    void onSuggestionSelected(int position);
+
+    /**
+     * Called when the selection changed so that no suggestion is selected.
+     */
+    void onNothingSelected();
 }
diff --git a/src/com/android/quicksearchbox/ui/SuggestionView.java b/src/com/android/quicksearchbox/ui/SuggestionView.java
index 8ab3d08..e2a114a 100644
--- a/src/com/android/quicksearchbox/ui/SuggestionView.java
+++ b/src/com/android/quicksearchbox/ui/SuggestionView.java
@@ -17,7 +17,6 @@
 package com.android.quicksearchbox.ui;
 
 import com.android.quicksearchbox.SuggestionCursor;
-import com.android.quicksearchbox.SuggestionPosition;
 
 /**
  * Interface to be implemented by any view appearing in the list of suggestions.
@@ -28,8 +27,4 @@
      */
     void bindAsSuggestion(SuggestionCursor suggestion);
 
-    /**
-     * Gets the SuggestionPosition associated with this view.
-     */
-    SuggestionPosition getSuggestionPosition();
 }
diff --git a/src/com/android/quicksearchbox/ui/SuggestionViewFactory.java b/src/com/android/quicksearchbox/ui/SuggestionViewFactory.java
index 5772236..9fc538f 100644
--- a/src/com/android/quicksearchbox/ui/SuggestionViewFactory.java
+++ b/src/com/android/quicksearchbox/ui/SuggestionViewFactory.java
@@ -46,7 +46,7 @@
      */
     SuggestionView getSuggestionView(int viewType, View convertView, ViewGroup parentViewType);
 
-    SourceView createSourceView(ViewGroup parentViewType);
+    CorpusView createSourceView(ViewGroup parentViewType);
 
     String getGlobalSearchLabel();
 
diff --git a/src/com/android/quicksearchbox/ui/SuggestionViewInflater.java b/src/com/android/quicksearchbox/ui/SuggestionViewInflater.java
index cb86464..e986674 100644
--- a/src/com/android/quicksearchbox/ui/SuggestionViewInflater.java
+++ b/src/com/android/quicksearchbox/ui/SuggestionViewInflater.java
@@ -37,7 +37,7 @@
     private static final String TAG = "QSB.SuggestionViewInflater";
 
     // The suggestion view classes that may be returned by this factory.
-    private static final Class[] SUGGESTION_VIEW_CLASSES = {
+    private static final Class<?>[] SUGGESTION_VIEW_CLASSES = {
             DefaultSuggestionView.class,
             ContactSuggestionView.class,
     };
@@ -79,26 +79,26 @@
         return (SuggestionView) convertView;
     }
 
-    public SourceView createSourceView(ViewGroup parentViewType) {
+    public CorpusView createSourceView(ViewGroup parentViewType) {
         if (DBG) Log.d(TAG, "createSourceView()");
-        SourceView view = (SourceView)
-                getInflater().inflate(R.layout.source_list_item, parentViewType, false);
+        CorpusView view = (CorpusView)
+                getInflater().inflate(R.layout.corpus_grid_item, parentViewType, false);
         return view;
     }
 
     public String getGlobalSearchLabel() {
-        return mContext.getString(R.string.global_search_label);
+        return mContext.getString(R.string.corpus_label_global);
     }
 
     public Drawable getGlobalSearchIcon() {
-        return mContext.getResources().getDrawable(R.drawable.global_search_source);
+        return mContext.getResources().getDrawable(R.drawable.corpus_icon_global);
     }
 
     public Uri getGlobalSearchIconUri() {
         return new Uri.Builder()
                 .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
                 .authority(mContext.getPackageName())
-                .appendEncodedPath(String.valueOf(R.drawable.global_search_source))
+                .appendEncodedPath(String.valueOf(R.drawable.corpus_icon_global))
                 .build();
     }
 
diff --git a/src/com/android/quicksearchbox/ui/SuggestionsAdapter.java b/src/com/android/quicksearchbox/ui/SuggestionsAdapter.java
index 6b37cf9..e9af263 100644
--- a/src/com/android/quicksearchbox/ui/SuggestionsAdapter.java
+++ b/src/com/android/quicksearchbox/ui/SuggestionsAdapter.java
@@ -16,11 +16,11 @@
 
 package com.android.quicksearchbox.ui;
 
+import com.android.quicksearchbox.Corpus;
 import com.android.quicksearchbox.SuggestionCursor;
 import com.android.quicksearchbox.SuggestionPosition;
 import com.android.quicksearchbox.Suggestions;
 
-import android.content.ComponentName;
 import android.database.DataSetObserver;
 import android.util.Log;
 import android.view.View;
@@ -41,7 +41,7 @@
 
     private SuggestionCursor mCursor;
 
-    private ComponentName mSource = null;
+    private Corpus mCorpus = null;
 
     private Suggestions mSuggestions;
 
@@ -57,7 +57,7 @@
 
     public void close() {
         setSuggestions(null);
-        mSource = null;
+        mCorpus = null;
         mClosed = true;
     }
 
@@ -93,18 +93,15 @@
     /**
      * Gets the source whose results are displayed.
      */
-    public ComponentName getSource() {
-        return mSource;
+    public Corpus getCorpus() {
+        return mCorpus;
     }
 
     /**
      * Sets the source whose results are displayed.
-     *
-     * @param source The name of a source, or {@code null} to show
-     *        the promoted results.
      */
-    public void setSource(ComponentName source) {
-        mSource = source;
+    public void setCorpus(Corpus corpus) {
+        mCorpus = corpus;
         onSuggestionsChanged();
     }
 
@@ -121,10 +118,12 @@
         return position;
     }
 
+    @Override
     public int getViewTypeCount() {
         return mViewFactory.getSuggestionViewTypeCount();
     }
 
+    @Override
     public int getItemViewType(int position) {
         if (mCursor == null) {
             return 0;
@@ -146,7 +145,7 @@
 
     protected void onSuggestionsChanged() {
         if (DBG) Log.d(TAG, "onSuggestionsChanged(), mSuggestions=" + mSuggestions);
-        SuggestionCursor cursor = getSourceCursor(mSuggestions, mSource);
+        SuggestionCursor cursor = getCorpusCursor(mSuggestions, mCorpus);
         changeCursor(cursor);
     }
 
@@ -161,10 +160,10 @@
     /**
      * Gets the cursor for the given source.
      */
-    protected SuggestionCursor getSourceCursor(Suggestions suggestions, ComponentName source) {
+    protected SuggestionCursor getCorpusCursor(Suggestions suggestions, Corpus corpus) {
         if (suggestions == null) return null;
-        if (source == null) return suggestions.getPromoted();
-        return suggestions.getSourceResult(source);
+        if (corpus == null) return suggestions.getPromoted();
+        return suggestions.getCorpusResult(corpus);
     }
 
     /**
diff --git a/src/com/android/quicksearchbox/ui/SuggestionsView.java b/src/com/android/quicksearchbox/ui/SuggestionsView.java
index ac0db8b..dad393e 100644
--- a/src/com/android/quicksearchbox/ui/SuggestionsView.java
+++ b/src/com/android/quicksearchbox/ui/SuggestionsView.java
@@ -101,9 +101,8 @@
         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
             if (DBG) Log.d(TAG, "onItemClick(" + position + ")");
             SuggestionView suggestionView = (SuggestionView) view;
-            SuggestionPosition suggestion = suggestionView.getSuggestionPosition();
             if (mSuggestionClickListener != null) {
-                mSuggestionClickListener.onSuggestionClicked(suggestion);
+                mSuggestionClickListener.onSuggestionClicked(position);
             }
         }
     }
@@ -112,9 +111,8 @@
         public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
             if (DBG) Log.d(TAG, "onItemLongClick(" + position + ")");
             SuggestionView suggestionView = (SuggestionView) view;
-            SuggestionPosition suggestion = suggestionView.getSuggestionPosition();
             if (mSuggestionClickListener != null) {
-                return mSuggestionClickListener.onSuggestionLongClicked(suggestion);
+                return mSuggestionClickListener.onSuggestionLongClicked(position);
             }
             return false;
         }
@@ -124,16 +122,15 @@
         public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
             if (DBG) Log.d(TAG, "onItemSelected(" + position + ")");
             SuggestionView suggestionView = (SuggestionView) view;
-            SuggestionPosition suggestion = suggestionView.getSuggestionPosition();
             if (mSuggestionSelectionListener != null) {
-                mSuggestionSelectionListener.onSelectionChanged(suggestion);
+                mSuggestionSelectionListener.onSuggestionSelected(position);
             }
         }
 
         public void onNothingSelected(AdapterView<?> parent) {
             if (DBG) Log.d(TAG, "onNothingSelected()");
             if (mSuggestionSelectionListener != null) {
-                mSuggestionSelectionListener.onSelectionChanged(null);
+                mSuggestionSelectionListener.onNothingSelected();
             }
         }
     }