Add user-selected search providers to browser

The lists of search providers are taken from Chrome.

Change-Id: I7af6dc1258950d1fc5cf86013f8be9f3c5db0f1a
diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java
index 5e55789..6f47788 100644
--- a/src/com/android/browser/BrowserActivity.java
+++ b/src/com/android/browser/BrowserActivity.java
@@ -111,6 +111,7 @@
 import android.accounts.OperationCanceledException;
 import android.accounts.AccountManagerCallback;
 
+import com.android.browser.search.SearchEngine;
 import com.android.common.Search;
 import com.android.common.speech.LoggingEvents;
 
@@ -619,17 +620,9 @@
             }
         }.execute();
 
-        Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
-        intent.addCategory(Intent.CATEGORY_DEFAULT);
-        intent.putExtra(SearchManager.QUERY, url);
-        if (appData != null) {
-            intent.putExtra(SearchManager.APP_DATA, appData);
-        }
-        if (extraData != null) {
-            intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
-        }
-        intent.putExtra(Browser.EXTRA_APPLICATION_ID, getPackageName());
-        startActivity(intent);
+        SearchEngine searchEngine = mSettings.getSearchEngine();
+        if (searchEngine == null) return false;
+        searchEngine.startSearch(this, url, appData, extraData);
 
         return true;
     }
diff --git a/src/com/android/browser/BrowserPreferencesPage.java b/src/com/android/browser/BrowserPreferencesPage.java
index 6426b99..9af66f1 100644
--- a/src/com/android/browser/BrowserPreferencesPage.java
+++ b/src/com/android/browser/BrowserPreferencesPage.java
@@ -16,24 +16,19 @@
 
 package com.android.browser;
 
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.Vector;
-
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Bundle;
 import android.preference.EditTextPreference;
-import android.preference.ListPreference;
 import android.preference.Preference;
 import android.preference.PreferenceActivity;
 import android.preference.PreferenceScreen;
-import android.util.Log;
 import android.webkit.GeolocationPermissions;
 import android.webkit.ValueCallback;
 import android.webkit.WebStorage;
-import android.webkit.WebView;
+
+import java.util.Map;
+import java.util.Set;
 
 public class BrowserPreferencesPage extends PreferenceActivity
         implements Preference.OnPreferenceChangeListener {
@@ -119,6 +114,7 @@
 
         // sync the shared preferences back to BrowserSettings
         BrowserSettings.getInstance().syncSharedPreferences(
+                getApplicationContext(),
                 getPreferenceScreen().getSharedPreferences());
     }
 
diff --git a/src/com/android/browser/BrowserProvider.java b/src/com/android/browser/BrowserProvider.java
index bf1f9d5..96745e5 100644
--- a/src/com/android/browser/BrowserProvider.java
+++ b/src/com/android/browser/BrowserProvider.java
@@ -16,10 +16,10 @@
 
 package com.android.browser;
 
+import com.android.browser.search.SearchEngine;
+
 import android.app.SearchManager;
-import android.app.SearchableInfo;
 import android.app.backup.BackupManager;
-import android.content.ComponentName;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -27,28 +27,21 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
-import android.content.UriMatcher;
 import android.content.SharedPreferences.Editor;
-import android.content.pm.PackageManager;
-import android.content.pm.ResolveInfo;
+import android.content.UriMatcher;
 import android.database.AbstractCursor;
-import android.database.ContentObserver;
 import android.database.Cursor;
 import android.database.sqlite.SQLiteDatabase;
 import android.database.sqlite.SQLiteOpenHelper;
 import android.net.Uri;
-import android.os.Handler;
 import android.os.Process;
 import android.preference.PreferenceManager;
 import android.provider.Browser;
-import android.provider.Settings;
 import android.provider.Browser.BookmarkColumns;
 import android.speech.RecognizerResultsIntent;
 import android.text.TextUtils;
 import android.util.Log;
 import android.util.Patterns;
-import android.util.TypedValue;
-
 
 import java.io.File;
 import java.io.FilenameFilter;
@@ -165,7 +158,7 @@
     // optionally a trailing slash, all matched as separate groups.
     private static final Pattern STRIP_URL_PATTERN = Pattern.compile("^(http://)(.*?)(/$)?");
 
-    private SearchManager mSearchManager;
+    private BrowserSettings mSettings;
 
     public BrowserProvider() {
     }
@@ -366,59 +359,10 @@
                 ed.commit();
             }
         }
-        mSearchManager = (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
-        mShowWebSuggestionsSettingChangeObserver
-            = new ShowWebSuggestionsSettingChangeObserver();
-        context.getContentResolver().registerContentObserver(
-                Settings.System.getUriFor(
-                        Settings.System.SHOW_WEB_SUGGESTIONS),
-                true, mShowWebSuggestionsSettingChangeObserver);
-        updateShowWebSuggestions();
+        mSettings = BrowserSettings.getInstance();
         return true;
     }
 
-    /**
-     * This Observer will ensure that if the user changes the system
-     * setting of whether to display web suggestions, we will
-     * change accordingly.
-     */
-    /* package */ class ShowWebSuggestionsSettingChangeObserver
-            extends ContentObserver {
-        public ShowWebSuggestionsSettingChangeObserver() {
-            super(new Handler());
-        }
-
-        @Override
-        public void onChange(boolean selfChange) {
-            updateShowWebSuggestions();
-        }
-    }
-
-    private ShowWebSuggestionsSettingChangeObserver
-            mShowWebSuggestionsSettingChangeObserver;
-
-    // If non-null, then the system is set to show web suggestions,
-    // and this is the SearchableInfo to use to get them.
-    private SearchableInfo mSearchableInfo;
-
-    /**
-     * Check the system settings to see whether web suggestions are
-     * allowed.  If so, store the SearchableInfo to grab suggestions
-     * while the user is typing.
-     */
-    private void updateShowWebSuggestions() {
-        mSearchableInfo = null;
-        Context context = getContext();
-        if (Settings.System.getInt(context.getContentResolver(),
-                Settings.System.SHOW_WEB_SUGGESTIONS,
-                1 /* default on */) == 1) {
-            ComponentName webSearchComponent = mSearchManager.getWebSearchActivity();
-            if (webSearchComponent != null) {
-                mSearchableInfo = mSearchManager.getSearchableInfo(webSearchComponent);
-            }
-        }
-    }
-
     private void fixPicasaBookmark() {
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
         Cursor cursor = db.rawQuery("SELECT _id FROM bookmarks WHERE " +
@@ -875,12 +819,15 @@
                     || Patterns.WEB_URL.matcher(selectionArgs[0]).matches()) {
                 return new MySuggestionCursor(c, null, "");
             } else {
-                // get Google suggest if there is still space in the list
+                // get search suggestions if there is still space in the list
                 if (myArgs != null && myArgs.length > 1
-                        && mSearchableInfo != null
+                        && mSettings.getShowSearchSuggestions()
                         && c.getCount() < (MAX_SUGGESTION_SHORT_ENTRIES - 1)) {
-                    Cursor sc = mSearchManager.getSuggestions(mSearchableInfo, selectionArgs[0]);
-                    return new MySuggestionCursor(c, sc, selectionArgs[0]);
+                    SearchEngine searchEngine = mSettings.getSearchEngine();
+                    if (searchEngine != null && searchEngine.supportsSuggestions()) {
+                        Cursor sc = searchEngine.getSuggestions(getContext(), selectionArgs[0]);
+                        return new MySuggestionCursor(c, sc, selectionArgs[0]);
+                    }
                 }
                 return new MySuggestionCursor(c, null, selectionArgs[0]);
             }
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
index 51b4eaa..6263eb3 100644
--- a/src/com/android/browser/BrowserSettings.java
+++ b/src/com/android/browser/BrowserSettings.java
@@ -17,14 +17,22 @@
 
 package com.android.browser;
 
+import com.android.browser.search.SearchEngine;
+import com.android.browser.search.SearchEngines;
+
 import android.app.ActivityManager;
+import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.pm.ActivityInfo;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.Editor;
+import android.database.ContentObserver;
+import android.os.Handler;
 import android.preference.PreferenceActivity;
 import android.preference.PreferenceScreen;
+import android.provider.Settings;
+import android.util.Log;
 import android.webkit.CookieManager;
 import android.webkit.GeolocationPermissions;
 import android.webkit.ValueCallback;
@@ -71,6 +79,8 @@
     private boolean openInBackground;
     private String defaultTextEncodingName;
     private String homeUrl = "";
+    private SearchEngine searchEngine;
+    private boolean showSearchSuggestions;
     private boolean autoFitPage;
     private boolean landscapeOnly;
     private boolean loadsPageInOverviewMode;
@@ -121,6 +131,8 @@
     public final static String PREF_CLEAR_COOKIES = "privacy_clear_cookies";
     public final static String PREF_CLEAR_HISTORY = "privacy_clear_history";
     public final static String PREF_HOMEPAGE = "homepage";
+    public final static String PREF_SEARCH_ENGINE = "search_engine";
+    public final static String PREF_SHOW_SEARCH_SUGGESTIONS = "show_search_suggestions";
     public final static String PREF_CLEAR_FORM_DATA =
             "privacy_clear_form_data";
     public final static String PREF_CLEAR_PASSWORDS =
@@ -234,7 +246,7 @@
      *            stored in this BrowserSettings object. This will update all
      *            observers of this object.
      */
-    public void loadFromDb(Context ctx) {
+    public void loadFromDb(final Context ctx) {
         SharedPreferences p =
                 PreferenceManager.getDefaultSharedPreferences(ctx);
         // Set the default value for the Application Caches path.
@@ -266,17 +278,41 @@
             pageCacheCapacity = 1;
         }
 
-        // Load the defaults from the xml
+        final ContentResolver cr = ctx.getContentResolver();
+        cr.registerContentObserver(
+                Settings.System.getUriFor(Settings.System.SHOW_WEB_SUGGESTIONS), false,
+                new ContentObserver(new Handler()) {
+                        @Override
+                        public void onChange(boolean selfChange) {
+                            SharedPreferences p =
+                                    PreferenceManager.getDefaultSharedPreferences(ctx);
+                            updateShowWebSuggestions(cr, p);
+                        }
+                });
+        updateShowWebSuggestions(cr, p);
+
+    // Load the defaults from the xml
         // This call is TOO SLOW, need to manually keep the defaults
         // in sync
         //PreferenceManager.setDefaultValues(ctx, R.xml.browser_preferences);
-        syncSharedPreferences(p);
+        syncSharedPreferences(ctx, p);
     }
 
-    /* package */ void syncSharedPreferences(SharedPreferences p) {
+    /* package */ void syncSharedPreferences(Context ctx, SharedPreferences p) {
 
         homeUrl =
             p.getString(PREF_HOMEPAGE, homeUrl);
+        String searchEngineName = p.getString(PREF_SEARCH_ENGINE, null);
+        if (searchEngine == null || !searchEngine.getName().equals(searchEngineName)) {
+            if (searchEngine != null) {
+                searchEngine.close();
+            }
+            searchEngine = SearchEngines.get(ctx, searchEngineName);
+        }
+        Log.i(TAG, "Selected search engine: " + searchEngine);
+        showSearchSuggestions = p.getBoolean(PREF_SHOW_SEARCH_SUGGESTIONS, true);
+        // Persist to system settings
+        saveShowWebSuggestions(ctx.getContentResolver());
 
         loadsImagesAutomatically = p.getBoolean("load_images",
                 loadsImagesAutomatically);
@@ -365,10 +401,30 @@
         update();
     }
 
+    private void saveShowWebSuggestions(ContentResolver cr) {
+        int value = showSearchSuggestions ? 1 : 0;
+        Settings.System.putInt(cr, Settings.System.SHOW_WEB_SUGGESTIONS, value);
+    }
+
+    private void updateShowWebSuggestions(ContentResolver cr, SharedPreferences p) {
+        showSearchSuggestions =
+                Settings.System.getInt(cr,
+                        Settings.System.SHOW_WEB_SUGGESTIONS, 1) == 1;
+        p.edit().putBoolean(PREF_SHOW_SEARCH_SUGGESTIONS, showSearchSuggestions).commit();
+    }
+
     public String getHomePage() {
         return homeUrl;
     }
 
+    public SearchEngine getSearchEngine() {
+        return searchEngine;
+    }
+
+    public boolean getShowSearchSuggestions() {
+        return showSearchSuggestions;
+    }
+
     public String getJsFlags() {
         return jsFlags;
     }
diff --git a/src/com/android/browser/search/DefaultSearchEngine.java b/src/com/android/browser/search/DefaultSearchEngine.java
new file mode 100644
index 0000000..42d274d
--- /dev/null
+++ b/src/com/android/browser/search/DefaultSearchEngine.java
@@ -0,0 +1,118 @@
+/*
+ * 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.browser.search;
+
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.text.TextUtils;
+import android.util.Log;
+
+public class DefaultSearchEngine implements SearchEngine {
+
+    private static final String TAG = "DefaultSearchEngine";
+
+    private final SearchableInfo mSearchable;
+
+    private final CharSequence mLabel;
+
+    private DefaultSearchEngine(Context context, SearchableInfo searchable) {
+        mSearchable = searchable;
+        mLabel = loadLabel(context, mSearchable.getSearchActivity());
+    }
+
+    public static DefaultSearchEngine create(Context context) {
+        SearchManager searchManager =
+                (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
+        ComponentName name = searchManager.getWebSearchActivity();
+        if (name == null) return null;
+        SearchableInfo searchable = searchManager.getSearchableInfo(name);
+        if (searchable == null) return null;
+        return new DefaultSearchEngine(context, searchable);
+    }
+
+    private CharSequence loadLabel(Context context, ComponentName activityName) {
+        PackageManager pm = context.getPackageManager();
+        try {
+            ActivityInfo ai = pm.getActivityInfo(activityName, 0);
+            return ai.loadLabel(pm);
+        } catch (PackageManager.NameNotFoundException ex) {
+            Log.e(TAG, "Web search activity not found: " + activityName);
+            return null;
+        }
+    }
+
+    public String getName() {
+        String packageName = mSearchable.getSearchActivity().getPackageName();
+        // Use "google" as name to avoid showing Google twice (app + OpenSearch)
+        if ("com.google.android.googlequicksearchbox".equals(packageName)) {
+            return "google";
+        } else if ("com.android.quicksearchbox".equals(packageName)) {
+            return "google";
+        } else {
+            return packageName;
+        }
+    }
+
+    public CharSequence getLabel() {
+        return mLabel;
+    }
+
+    public void startSearch(Context context, String query, Bundle appData, String extraData) {
+        try {
+            Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
+            intent.addCategory(Intent.CATEGORY_DEFAULT);
+            intent.putExtra(SearchManager.QUERY, query);
+            if (appData != null) {
+                intent.putExtra(SearchManager.APP_DATA, appData);
+            }
+            if (extraData != null) {
+                intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+            }
+            intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
+            context.startActivity(intent);
+        } catch (ActivityNotFoundException ex) {
+            Log.e(TAG, "Web search activity not found: " + mSearchable.getSearchActivity());
+        }
+    }
+
+    public Cursor getSuggestions(Context context, String query) {
+        SearchManager searchManager =
+                (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
+        return searchManager.getSuggestions(mSearchable, query);
+    }
+
+    public boolean supportsSuggestions() {
+        return !TextUtils.isEmpty(mSearchable.getSuggestAuthority());
+    }
+
+    public void close() {
+    }
+
+    @Override
+    public String toString() {
+        return "ActivitySearchEngine{" + mSearchable + "}";
+    }
+
+}
diff --git a/src/com/android/browser/search/OpenSearchSearchEngine.java b/src/com/android/browser/search/OpenSearchSearchEngine.java
new file mode 100644
index 0000000..e78a93c
--- /dev/null
+++ b/src/com/android/browser/search/OpenSearchSearchEngine.java
@@ -0,0 +1,295 @@
+/*
+ * 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.browser.search;
+
+import com.android.browser.R;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.params.HttpParams;
+import org.apache.http.util.EntityUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import android.app.SearchManager;
+import android.content.Context;
+import android.content.Intent;
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.http.AndroidHttpClient;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Provides search suggestions, if any, for a given web search provider.
+ */
+public class OpenSearchSearchEngine implements SearchEngine {
+
+    private static final String TAG = "OpenSearchSearchEngine";
+
+    private static final String USER_AGENT = "Android/1.0";
+    private static final int HTTP_TIMEOUT_MS = 1000;
+
+    // TODO: this should be defined somewhere
+    private static final String HTTP_TIMEOUT = "http.connection-manager.timeout";
+
+    // Indices of the columns in the below arrays.
+    private static final int COLUMN_INDEX_ID = 0;
+    private static final int COLUMN_INDEX_QUERY = 1;
+    private static final int COLUMN_INDEX_ICON = 2;
+    private static final int COLUMN_INDEX_TEXT_1 = 3;
+    private static final int COLUMN_INDEX_TEXT_2 = 4;
+
+    // The suggestion columns used. If you are adding a new entry to these arrays make sure to
+    // update the list of indices declared above.
+    private static final String[] COLUMNS = new String[] {
+        "_id",
+        SearchManager.SUGGEST_COLUMN_QUERY,
+        SearchManager.SUGGEST_COLUMN_ICON_1,
+        SearchManager.SUGGEST_COLUMN_TEXT_1,
+        SearchManager.SUGGEST_COLUMN_TEXT_2,
+    };
+
+    private static final String[] COLUMNS_WITHOUT_DESCRIPTION = new String[] {
+        "_id",
+        SearchManager.SUGGEST_COLUMN_QUERY,
+        SearchManager.SUGGEST_COLUMN_ICON_1,
+        SearchManager.SUGGEST_COLUMN_TEXT_1,
+    };
+
+    private final SearchEngineInfo mSearchEngineInfo;
+
+    private final AndroidHttpClient mHttpClient;
+
+    public OpenSearchSearchEngine(Context context, SearchEngineInfo searchEngineInfo) {
+        mSearchEngineInfo = searchEngineInfo;
+        mHttpClient = AndroidHttpClient.newInstance(USER_AGENT);
+        HttpParams params = mHttpClient.getParams();
+        params.setLongParameter(HTTP_TIMEOUT, HTTP_TIMEOUT_MS);
+    }
+
+    public String getName() {
+        return mSearchEngineInfo.getName();
+    }
+
+    public CharSequence getLabel() {
+        return mSearchEngineInfo.getLabel();
+    }
+
+    public void startSearch(Context context, String query, Bundle appData, String extraData) {
+        String uri = mSearchEngineInfo.getSearchUriForQuery(query);
+        if (uri == null) {
+            Log.e(TAG, "Unable to get search URI for " + mSearchEngineInfo);
+        } else {
+            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
+            // Make sure the intent goes to the Browser itself
+            intent.setPackage(context.getPackageName());
+            intent.addCategory(Intent.CATEGORY_DEFAULT);
+            intent.putExtra(SearchManager.QUERY, query);
+            if (appData != null) {
+                intent.putExtra(SearchManager.APP_DATA, appData);
+            }
+            if (extraData != null) {
+                intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+            }
+            intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
+            context.startActivity(intent);
+        }
+    }
+
+    /**
+     * Queries for a given search term and returns a cursor containing
+     * suggestions ordered by best match.
+     */
+    public Cursor getSuggestions(Context context, String query) {
+        if (TextUtils.isEmpty(query)) {
+            return null;
+        }
+        if (!isNetworkConnected(context)) {
+            Log.i(TAG, "Not connected to network.");
+            return null;
+        }
+
+        String suggestUri = mSearchEngineInfo.getSuggestUriForQuery(query);
+        if (TextUtils.isEmpty(suggestUri)) {
+            // No suggest URI available for this engine
+            return null;
+        }
+
+        try {
+            String content = readUrl(suggestUri);
+            if (content == null) return null;
+            /* The data format is a JSON array with items being regular strings or JSON arrays
+             * themselves. We are interested in the second and third elements, both of which
+             * should be JSON arrays. The second element/array contains the suggestions and the
+             * third element contains the descriptions. Some search engines don't support
+             * suggestion descriptions so the third element is optional.
+             */
+            JSONArray results = new JSONArray(content);
+            JSONArray suggestions = results.getJSONArray(1);
+            JSONArray descriptions = null;
+            if (results.length() > 2) {
+                descriptions = results.getJSONArray(2);
+                // Some search engines given an empty array "[]" for descriptions instead of
+                // not including it in the response.
+                if (descriptions.length() == 0) {
+                    descriptions = null;
+                }
+            }
+            return new SuggestionsCursor(suggestions, descriptions);
+        } catch (JSONException e) {
+            Log.w(TAG, "Error", e);
+        }
+        return null;
+    }
+
+    /**
+     * Executes a GET request and returns the response content.
+     *
+     * @param url Request URI.
+     * @param requestHeaders Request headers.
+     * @return The response content. This is the empty string if the response
+     *         contained no content.
+     */
+    public String readUrl(String url) {
+        try {
+            HttpGet method = new HttpGet(url);
+            HttpResponse response = mHttpClient.execute(method);
+            if (response.getStatusLine().getStatusCode() == 200) {
+                return EntityUtils.toString(response.getEntity());
+            } else {
+                Log.i(TAG, "Suggestion request failed");
+                return null;
+            }
+        } catch (IOException e) {
+            Log.w(TAG, "Error", e);
+            return null;
+        }
+    }
+
+    public boolean supportsSuggestions() {
+        return mSearchEngineInfo.supportsSuggestions();
+    }
+
+    public void close() {
+        mHttpClient.close();
+    }
+
+    private boolean isNetworkConnected(Context context) {
+        NetworkInfo networkInfo = getActiveNetworkInfo(context);
+        return networkInfo != null && networkInfo.isConnected();
+    }
+
+    private NetworkInfo getActiveNetworkInfo(Context context) {
+        ConnectivityManager connectivity =
+                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (connectivity == null) {
+            return null;
+        }
+        return connectivity.getActiveNetworkInfo();
+    }
+
+    private static class SuggestionsCursor extends AbstractCursor {
+
+        private final JSONArray mSuggestions;
+
+        private final JSONArray mDescriptions;
+
+        public SuggestionsCursor(JSONArray suggestions, JSONArray descriptions) {
+            mSuggestions = suggestions;
+            mDescriptions = descriptions;
+        }
+
+        @Override
+        public int getCount() {
+            return mSuggestions.length();
+        }
+
+        @Override
+        public String[] getColumnNames() {
+            return (mDescriptions != null ? COLUMNS : COLUMNS_WITHOUT_DESCRIPTION);
+        }
+
+        @Override
+        public String getString(int column) {
+            if (mPos != -1) {
+                if ((column == COLUMN_INDEX_QUERY) || (column == COLUMN_INDEX_TEXT_1)) {
+                    try {
+                        return mSuggestions.getString(mPos);
+                    } catch (JSONException e) {
+                        Log.w(TAG, "Error", e);
+                    }
+                } else if (column == COLUMN_INDEX_TEXT_2) {
+                    try {
+                        return mDescriptions.getString(mPos);
+                    } catch (JSONException e) {
+                        Log.w(TAG, "Error", e);
+                    }
+                } else if (column == COLUMN_INDEX_ICON) {
+                    return String.valueOf(R.drawable.magnifying_glass);
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public double getDouble(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public float getFloat(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int getInt(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public long getLong(int column) {
+            if (column == COLUMN_INDEX_ID) {
+                return mPos;        // use row# as the _Id
+            }
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public short getShort(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isNull(int column) {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "OpenSearchSearchEngine{" + mSearchEngineInfo + "}";
+    }
+
+}
diff --git a/src/com/android/browser/search/SearchEngine.java b/src/com/android/browser/search/SearchEngine.java
new file mode 100644
index 0000000..3d24d2e
--- /dev/null
+++ b/src/com/android/browser/search/SearchEngine.java
@@ -0,0 +1,57 @@
+/*
+ * 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.browser.search;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+
+/**
+ * Interface for search engines.
+ */
+public interface SearchEngine {
+
+    /**
+     * Gets the unique name of this search engine.
+     */
+    public String getName();
+
+    /**
+     * Gets the human-readable name of this search engine.
+     */
+    public CharSequence getLabel();
+
+    /**
+     * Starts a search.
+     */
+    public void startSearch(Context context, String query, Bundle appData, String extraData);
+
+    /**
+     * Gets search suggestions.
+     */
+    public Cursor getSuggestions(Context context, String query);
+
+    /**
+     * Checks whether this search engine supports search suggestions.
+     */
+    public boolean supportsSuggestions();
+
+    /**
+     * Closes this search engine.
+     */
+    public void close();
+
+}
diff --git a/src/com/android/browser/search/SearchEngineInfo.java b/src/com/android/browser/search/SearchEngineInfo.java
new file mode 100644
index 0000000..6f0b1d5
--- /dev/null
+++ b/src/com/android/browser/search/SearchEngineInfo.java
@@ -0,0 +1,169 @@
+/*
+ * 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.browser.search;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.net.URLEncoder;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Loads and holds data for a given web search engine.
+ */
+public class SearchEngineInfo {
+
+    private static String TAG = "SearchEngineInfo";
+
+    // The fields of a search engine data array, defined in the same order as they appear in the
+    // all_search_engines.xml file.
+    // If you are adding/removing to this list, remember to update NUM_FIELDS below.
+    private static final int FIELD_LABEL = 0;
+    private static final int FIELD_KEYWORD = 1;
+    private static final int FIELD_FAVICON_URI = 2;
+    private static final int FIELD_SEARCH_URI = 3;
+    private static final int FIELD_ENCODING = 4;
+    private static final int FIELD_SUGGEST_URI = 5;
+    private static final int NUM_FIELDS = 6;
+
+    // The OpenSearch URI template parameters that we support.
+    private static final String PARAMETER_LANGUAGE = "{language}";
+    private static final String PARAMETER_SEARCH_TERMS = "{searchTerms}";
+    private static final String PARAMETER_INPUT_ENCODING = "{inputEncoding}";
+
+    private final String mName;
+
+    // The array of strings defining this search engine. The array values are in the same order as
+    // the above enumeration definition.
+    private final String[] mSearchEngineData;
+
+    /**
+     * @throws IllegalArgumentException If the name does not refer to a valid search engine
+     */
+    public SearchEngineInfo(Context context, String name) throws IllegalArgumentException {
+        mName = name;
+
+        Resources res = context.getResources();
+        int id_data = res.getIdentifier(name, "array", context.getPackageName());
+        mSearchEngineData = res.getStringArray(id_data);
+
+        if (mSearchEngineData == null) {
+            throw new IllegalArgumentException("No data found for " + name);
+        }
+        if (mSearchEngineData.length != NUM_FIELDS) {
+                throw new IllegalArgumentException(
+                        name + " has invalid number of fields - " + mSearchEngineData.length);
+        }
+        if (TextUtils.isEmpty(mSearchEngineData[FIELD_SEARCH_URI])) {
+            throw new IllegalArgumentException(name + " has an empty search URI");
+        }
+
+        // Add the current language/country information to the URIs.
+        Locale locale = context.getResources().getConfiguration().locale;
+        StringBuilder language = new StringBuilder(locale.getLanguage());
+        if (!TextUtils.isEmpty(locale.getCountry())) {
+            language.append('-');
+            language.append(locale.getCountry());
+        }
+
+        String language_str = language.toString();
+        mSearchEngineData[FIELD_SEARCH_URI] =
+                mSearchEngineData[FIELD_SEARCH_URI].replace(PARAMETER_LANGUAGE, language_str);
+        mSearchEngineData[FIELD_SUGGEST_URI] =
+                mSearchEngineData[FIELD_SUGGEST_URI].replace(PARAMETER_LANGUAGE, language_str);
+
+        // Default to UTF-8 if not specified.
+        String enc = mSearchEngineData[FIELD_ENCODING];
+        if (TextUtils.isEmpty(enc)) {
+            enc = "UTF-8";
+            mSearchEngineData[FIELD_ENCODING] = enc;
+        }
+
+        // Add the input encoding method to the URI.
+        mSearchEngineData[FIELD_SEARCH_URI] =
+                mSearchEngineData[FIELD_SEARCH_URI].replace(PARAMETER_INPUT_ENCODING, enc);
+        mSearchEngineData[FIELD_SUGGEST_URI] =
+                mSearchEngineData[FIELD_SUGGEST_URI].replace(PARAMETER_INPUT_ENCODING, enc);
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public String getLabel() {
+        return mSearchEngineData[FIELD_LABEL];
+    }
+
+    /**
+     * Returns the URI for launching a web search with the given query (or null if there was no
+     * data available for this search engine).
+     */
+    public String getSearchUriForQuery(String query) {
+        return getFormattedUri(searchUri(), query);
+    }
+
+    /**
+     * Returns the URI for retrieving web search suggestions for the given query (or null if there
+     * was no data available for this search engine).
+     */
+    public String getSuggestUriForQuery(String query) {
+        return getFormattedUri(suggestUri(), query);
+    }
+
+    public boolean supportsSuggestions() {
+        return !TextUtils.isEmpty(suggestUri());
+    }
+
+    public String faviconUri() {
+        return mSearchEngineData[FIELD_FAVICON_URI];
+    }
+
+    private String suggestUri() {
+        return mSearchEngineData[FIELD_SUGGEST_URI];
+    }
+
+    private String searchUri() {
+        return mSearchEngineData[FIELD_SEARCH_URI];
+    }
+
+    /**
+     * Formats a launchable uri out of the template uri by replacing the template parameters with
+     * actual values.
+     */
+    private String getFormattedUri(String templateUri, String query) {
+        if (TextUtils.isEmpty(templateUri)) {
+            return null;
+        }
+
+        // Encode the query terms in the requested encoding (and fallback to UTF-8 if not).
+        String enc = mSearchEngineData[FIELD_ENCODING];
+        try {
+            return templateUri.replace(PARAMETER_SEARCH_TERMS, URLEncoder.encode(query, enc));
+        } catch (java.io.UnsupportedEncodingException e) {
+            Log.e(TAG, "Exception occured when encoding query " + query + " to " + enc);
+            return null;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "SearchEngineInfo{" + Arrays.toString(mSearchEngineData) + "}";
+    }
+
+}
diff --git a/src/com/android/browser/search/SearchEnginePreference.java b/src/com/android/browser/search/SearchEnginePreference.java
new file mode 100644
index 0000000..18ce495
--- /dev/null
+++ b/src/com/android/browser/search/SearchEnginePreference.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.browser.search;
+
+import com.android.browser.R;
+
+import android.app.SearchManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+class SearchEnginePreference extends ListPreference {
+
+    private static final String TAG = "SearchEnginePreference";
+
+    public SearchEnginePreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        ArrayList<CharSequence> entryValues = new ArrayList<CharSequence>();
+        ArrayList<CharSequence> entries = new ArrayList<CharSequence>();
+
+        SearchEngine defaultSearchEngine = SearchEngines.getDefaultSearchEngine(context);
+        String defaultSearchEngineName = null;
+        if (defaultSearchEngine != null) {
+            defaultSearchEngineName = defaultSearchEngine.getName();
+            entryValues.add(defaultSearchEngineName);
+            entries.add(defaultSearchEngine.getLabel());
+        }
+        for (SearchEngineInfo searchEngineInfo : SearchEngines.getSearchEngineInfos(context)) {
+            String name = searchEngineInfo.getName();
+            // Skip entry with same name as default provider
+            if (!name.equals(defaultSearchEngineName)) {
+                entryValues.add(name);
+                entries.add(searchEngineInfo.getLabel());
+            }
+        }
+
+        setEntryValues(entryValues.toArray(new CharSequence[entryValues.size()]));
+        setEntries(entries.toArray(new CharSequence[entries.size()]));
+    }
+
+}
diff --git a/src/com/android/browser/search/SearchEngines.java b/src/com/android/browser/search/SearchEngines.java
new file mode 100644
index 0000000..62690e7
--- /dev/null
+++ b/src/com/android/browser/search/SearchEngines.java
@@ -0,0 +1,73 @@
+/*
+ * 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.browser.search;
+
+import com.android.browser.R;
+
+import android.app.SearchManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SearchEngines {
+
+    private static final String TAG = "SearchEngines";
+
+    public static SearchEngine getDefaultSearchEngine(Context context) {
+        return DefaultSearchEngine.create(context);
+    }
+
+    public static List<SearchEngineInfo> getSearchEngineInfos(Context context) {
+        ArrayList<SearchEngineInfo> searchEngineInfos = new ArrayList<SearchEngineInfo>();
+        Resources res = context.getResources();
+        String[] searchEngines = res.getStringArray(R.array.search_engines);
+        for (int i = 0; i < searchEngines.length; i++) {
+            String name = searchEngines[i];
+            SearchEngineInfo info = new SearchEngineInfo(context, name);
+            searchEngineInfos.add(info);
+        }
+        return searchEngineInfos;
+    }
+
+    public static SearchEngine get(Context context, String name) {
+        // TODO: cache
+        SearchEngine defaultSearchEngine = getDefaultSearchEngine(context);
+        if (TextUtils.isEmpty(name)
+                || (defaultSearchEngine != null && name.equals(defaultSearchEngine.getName()))) {
+            return defaultSearchEngine;
+        }
+        SearchEngineInfo searchEngineInfo = getSearchEngineInfo(context, name);
+        if (searchEngineInfo == null) return defaultSearchEngine;
+        return new OpenSearchSearchEngine(context, searchEngineInfo);
+    }
+
+    private static SearchEngineInfo getSearchEngineInfo(Context context, String name) {
+        try {
+            return new SearchEngineInfo(context, name);
+        } catch (IllegalArgumentException exception) {
+            Log.e(TAG, "Cannot load search engine " + name, exception);
+            return null;
+        }
+    }
+
+}