Bug 892113 - Dialog to manage installed search providers. r=liuche
authorChris Kitching <ckitching@mozilla.com>
Mon, 05 Aug 2013 08:39:29 -0700
changeset 141368 7bbaba1b712f689d37217b2c7eea02520c3e8315
parent 141367 ad0ae007aa9e03cd74e9005cd6652e544139b3b5
child 141369 b92db71dc525ce3db360b2727818ca7b8a1e1aab
push id2046
push usercliu@mozilla.com
push dateMon, 05 Aug 2013 15:39:56 +0000
treeherderfx-team@b92db71dc525 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersliuche
bugs892113
milestone25.0a1
Bug 892113 - Dialog to manage installed search providers. r=liuche
mobile/android/base/Makefile.in
mobile/android/base/awesomebar/AllPagesTab.java
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/preferences/SearchEnginePreference.java
mobile/android/base/preferences/SearchPreferenceCategory.java
mobile/android/base/strings.xml.in
mobile/android/chrome/content/aboutAddons.js
mobile/android/chrome/content/aboutAddons.xhtml
mobile/android/chrome/content/browser.js
mobile/android/locales/en-US/chrome/aboutAddons.dtd
mobile/android/locales/en-US/chrome/aboutAddons.properties
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -229,16 +229,17 @@ FENNEC_JAVA_FILES = \
   menu/GeckoMenuItem.java \
   menu/GeckoSubMenu.java \
   menu/MenuItemActionBar.java \
   menu/MenuItemActionView.java \
   menu/MenuItemDefault.java \
   menu/MenuPanel.java \
   menu/MenuPopup.java \
   preferences/SearchPreferenceCategory.java \
+  preferences/SearchEnginePreference.java \
   widget/AboutHome.java \
   widget/AboutHomeView.java \
   widget/AboutHomeSection.java \
   widget/ActivityChooserModel.java \
   widget/AddonsSection.java \
   widget/ButtonToast.java \
   widget/ArrowPopup.java \
   widget/DateTimePicker.java \
--- a/mobile/android/base/awesomebar/AllPagesTab.java
+++ b/mobile/android/base/awesomebar/AllPagesTab.java
@@ -85,17 +85,17 @@ public class AllPagesTab extends Awesome
     private static final int MESSAGE_UPDATE_FAVICONS = 2;
     private static final int DELAY_SHOW_THUMBNAILS = 550;
 
     public AllPagesTab(Context context) {
         super(context);
         mSearchEngines = new ArrayList<SearchEngine>();
 
         registerEventListener("SearchEngines:Data");
-        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null));
+        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null));
 
         mHandler = new AllPagesHandler();
     }
 
     @Override
     public boolean onBackPressed() {
         return false;
     }
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -139,16 +139,20 @@ size. -->
 <!ENTITY pref_private_data_offlineApps "Offline website data">
 <!ENTITY pref_private_data_siteSettings2 "Site settings">
 <!ENTITY pref_private_data_downloadFiles "Downloaded files">
 
 
 <!ENTITY pref_about_firefox "About &brandShortName;">
 <!ENTITY pref_vendor_faqs "FAQs">
 <!ENTITY pref_vendor_feedback "Give feedback">
+<!ENTITY pref_search_set_default "Set as default">
+<!ENTITY pref_search_default "Default">
+<!ENTITY pref_search_remove "Remove">
+<!ENTITY pref_search_last_toast "You can\'t remove or disable your last search engine.">
 
 <!ENTITY datareporting_notification_title "&brandShortName; stats &amp; data">
 <!ENTITY datareporting_notification_action_long "Choose what information to share">
 <!ENTITY datareporting_notification_action "Choose what to share">
 <!ENTITY datareporting_notification_summary "To improve your experience, &brandShortName; automatically sends some information to &vendorShortName;.">
 <!ENTITY datareporting_notification_summary_short "To improve your experience, &brandShortName;…">
 <!ENTITY datareporting_notification_ticker_text "&datareporting_notification_title;: &datareporting_notification_action_long;">
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/preferences/SearchEnginePreference.java
@@ -0,0 +1,229 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.preferences;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.os.Build;
+import android.preference.Preference;
+import android.text.SpannableString;
+import android.util.Log;
+import android.widget.TextView;
+import android.widget.Toast;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.util.ThreadUtils;
+
+/**
+ * Represents an element in the list of search engines on the preferences menu.
+ */
+public class SearchEnginePreference extends Preference {
+    private static final String LOGTAG = "SearchEnginePreference";
+
+    // Dimensions, in dp, of the icon to display for this engine.
+    public static int sIconSize;
+
+    // Indices in button array of the AlertDialog of the three buttons.
+    public static final int INDEX_SET_DEFAULT_BUTTON = 0;
+    public static final int INDEX_REMOVE_BUTTON = 1;
+
+    // Cache label to avoid repeated use of the resource system.
+    public final String LABEL_IS_DEFAULT;
+
+    // Specifies if this engine is configured as the default search engine.
+    private boolean mIsDefaultEngine;
+    // Specifies if this engine is one of the ones bundled with the app, which cannot be deleted.
+    private boolean mIsImmutableEngine;
+
+    // Dialog element labels.
+    private String[] mDialogItems;
+
+    // The popup displayed when this element is tapped.
+    private AlertDialog mDialog;
+
+    private final SearchPreferenceCategory mParentCategory;
+
+    /**
+     * Create a preference object to represent a search engine that is attached to category
+     * containingCategory.
+     * @param context The activity context we operate under.
+     * @param parentCategory The PreferenceCategory this object exists within.
+     * @see this.setSearchEngine
+     */
+    public SearchEnginePreference(Context context, SearchPreferenceCategory parentCategory) {
+        super(context);
+        mParentCategory = parentCategory;
+
+        Resources res = getContext().getResources();
+
+        // Fetch the icon dimensions from the resource file.
+        sIconSize = res.getDimensionPixelSize(R.dimen.searchpreferences_icon_size);
+        setOnPreferenceClickListener(new OnPreferenceClickListener() {
+            @Override
+            public boolean onPreferenceClick(Preference preference) {
+                SearchEnginePreference sPref = (SearchEnginePreference) preference;
+                sPref.showDialog();
+
+                return true;
+            }
+        });
+
+        // Fetch this resource now, instead of every time we ever want to relabel a button.
+        LABEL_IS_DEFAULT = res.getString(R.string.pref_search_default);
+
+        // Set up default dialog items.
+        mDialogItems = new String[] { res.getString(R.string.pref_search_set_default),
+                                      res.getString(R.string.pref_search_remove) };
+    }
+
+    /**
+     * Configure this Preference object from the Gecko search engine JSON object.
+     * @param geckoEngineJSON The Gecko-formatted JSON object representing the search engine.
+     * @throws JSONException If the JSONObject is invalid.
+     */
+    public void setSearchEngineFromJSON(JSONObject geckoEngineJSON) throws JSONException {
+        final String engineName = geckoEngineJSON.getString("name");
+        SpannableString titleSpannable = new SpannableString(engineName);
+        mIsImmutableEngine = geckoEngineJSON.getBoolean("immutable");
+
+        if (mIsImmutableEngine) {
+            // Delete the "Remove" option from the menu.
+            mDialogItems = new String[] { getContext().getResources().getString(R.string.pref_search_set_default) };
+        }
+        setTitle(titleSpannable);
+
+        // setIcon is only available on Honeycomb and up.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            // Create a drawable from the iconURI and assign it to this Preference for display.
+            String iconURI = geckoEngineJSON.getString("iconURI");
+            Bitmap iconBitmap = BitmapUtils.getBitmapFromDataURI(iconURI);
+            Bitmap scaledIconBitmap = Bitmap.createScaledBitmap(iconBitmap, sIconSize, sIconSize, false);
+            BitmapDrawable drawable = new BitmapDrawable(scaledIconBitmap);
+            setIcon(drawable);
+        }
+    }
+
+    /**
+     * Set if this object's UI should show that this is the default engine.
+     * @param isDefault Flag indicating if this represents the default engine.
+     */
+    public void setIsDefaultEngine(boolean isDefault) {
+        mIsDefaultEngine = isDefault;
+        if (isDefault) {
+            setOrder(0);
+            setSummary(LABEL_IS_DEFAULT);
+        } else {
+            setOrder(1);
+            setSummary("");
+        }
+    }
+
+    /**
+     * Display the AlertDialog providing options to reconfigure this search engine. Sets an event
+     * listener to disable buttons in the dialog as appropriate after they have been constructed by
+     * Android.
+     * @see this.configureShownDialog
+     * @see this.hideDialog
+     */
+    public void showDialog() {
+        // If we are the only engine left, then we are the default engine, and none of the options
+        // on this menu can do anything.
+        if (mParentCategory.getPreferenceCount() == 1) {
+            ThreadUtils.postToUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    Toast.makeText(getContext(), R.string.pref_search_last_toast, Toast.LENGTH_SHORT).show();
+                }
+            });
+            return;
+        }
+
+        // If we are both default and immutable, we have no enabled items to show on the menu - abort.
+        if (mIsDefaultEngine && mIsImmutableEngine) {
+            return;
+        }
+
+        final AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+        builder.setTitle(getTitle().toString());
+        builder.setItems(mDialogItems, new DialogInterface.OnClickListener() {
+            // Forward the various events that we care about to the container class for handling.
+            @Override
+            public void onClick(DialogInterface dialog, int indexClicked) {
+                hideDialog();
+                switch (indexClicked) {
+                    case INDEX_SET_DEFAULT_BUTTON:
+                        mParentCategory.setDefault(SearchEnginePreference.this);
+                        break;
+                    case INDEX_REMOVE_BUTTON:
+                        mParentCategory.uninstall(SearchEnginePreference.this);
+                        break;
+                    default:
+                        Log.w(LOGTAG, "Selected index out of range.");
+                        break;
+                }
+            }
+        });
+
+        // Copy the icon, if any, from this object to the prompt we produce.
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
+            builder.setIcon(getIcon());
+        }
+
+        // We have to construct the dialog itself on the UI thread.
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                mDialog = builder.create();
+                mDialog.setOnShowListener(new DialogInterface.OnShowListener() {
+                    // Called when the dialog is shown (so we're finally able to manipulate button enabledness).
+                    @Override
+                    public void onShow(DialogInterface dialog) {
+                        configureShownDialog();
+                    }
+                });
+                mDialog.show();
+            }
+        });
+    }
+
+    /**
+     * Hide the dialog we previously created, if any.
+     */
+    public void hideDialog() {
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                // Null check so we can chain engine-mutating methods up in SearchPreferenceCategory
+                // without consequence.
+                if (mDialog != null && mDialog.isShowing()) {
+                    mDialog.dismiss();
+                }
+            }
+        });
+    }
+
+    /**
+     * Disables buttons in the shown AlertDialog as required. The button elements are not created
+     * until after we call show, so this method has to be called from the onShowListener above.
+     * @see this.showDialog
+     */
+    private void configureShownDialog() {
+        // If we are the default engine, disable the "Set as default" button.
+        TextView defaultButton = (TextView) mDialog.getListView().getChildAt(INDEX_SET_DEFAULT_BUTTON);
+        // Disable "Set as default" button if we are already the default.
+        if (mIsDefaultEngine) {
+            defaultButton.setEnabled(false);
+            // Failure to unregister this listener leads to tapping the button dismissing the dialog
+            // without doing anything.
+            defaultButton.setOnClickListener(null);
+        }
+    }
+}
--- a/mobile/android/base/preferences/SearchPreferenceCategory.java
+++ b/mobile/android/base/preferences/SearchPreferenceCategory.java
@@ -1,89 +1,162 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
 package org.mozilla.gecko.preferences;
 
 import android.content.Context;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.os.Build;
 import android.preference.Preference;
 import android.preference.PreferenceCategory;
 import android.util.AttributeSet;
 import android.util.Log;
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
-import org.mozilla.gecko.R;
-import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.util.GeckoEventListener;
 
 public class SearchPreferenceCategory extends PreferenceCategory implements GeckoEventListener {
     public static final String LOGTAG = "SearchPrefCategory";
 
-    private static int sIconSize;
+    private SearchEnginePreference mDefaultEngineReference;
+
+    // These seemingly redundant constructors are mandated by the Android system, else it fails to
+    // inflate this object.
 
     public SearchPreferenceCategory(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
-        init();
     }
+
     public SearchPreferenceCategory(Context context, AttributeSet attrs) {
         super(context, attrs);
-        init();
     }
 
     public SearchPreferenceCategory(Context context) {
         super(context);
-        init();
-    }
-
-    private void init() {
-        sIconSize = getContext().getResources().getDimensionPixelSize(R.dimen.searchpreferences_icon_size);
     }
 
     @Override
     protected void onAttachedToActivity() {
         super.onAttachedToActivity();
 
-        // Request list of search engines from Gecko
+        // Ensures default engine remains at top of list.
+        setOrderingAsAdded(false);
+
+        // Request list of search engines from Gecko.
         GeckoAppShell.registerEventListener("SearchEngines:Data", this);
         GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null));
     }
 
     @Override
     public void handleMessage(String event, final JSONObject data) {
         if (event.equals("SearchEngines:Data")) {
+            // Parse engines array from JSON. The first element in the array is the default engine.
             JSONArray engines;
+            JSONObject defaultEngine;
+            final String defaultEngineName;
             try {
                 engines = data.getJSONArray("searchEngines");
+                if (engines.length() == 0) {
+                    return;
+                }
+                defaultEngine = engines.getJSONObject(0);
+                defaultEngineName = defaultEngine.getString("name");
             } catch (JSONException e) {
                 Log.e(LOGTAG, "Unable to decode search engine data from Gecko.", e);
                 return;
             }
 
             // Create an element in this PreferenceCategory for each engine.
             for (int i = 0; i < engines.length(); i++) {
                 try {
                     JSONObject engineJSON = engines.getJSONObject(i);
                     final String engineName = engineJSON.getString("name");
 
-                    Preference engine = new Preference(getContext());
-                    engine.setTitle(engineName);
-                    engine.setKey(engineName);
+                    SearchEnginePreference enginePreference = new SearchEnginePreference(getContext(), this);
+                    enginePreference.setSearchEngineFromJSON(engineJSON);
+                    if (engineName.equals(defaultEngineName)) {
+                        // We set this here, not in setSearchEngineFromJSON, because it allows us to
+                        // keep a reference  to the default engine to use when the AlertDialog
+                        // callbacks are used.
+                        enginePreference.setIsDefaultEngine(true);
+                        mDefaultEngineReference = enginePreference;
+                    }
 
-                    // The setIcon feature is not available prior to API 11.
-                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
-                        String iconURI = engineJSON.getString("iconURI");
-                        Bitmap iconBitmap = BitmapUtils.getBitmapFromDataURI(iconURI);
-                        Bitmap scaledIconBitmap = Bitmap.createScaledBitmap(iconBitmap, sIconSize, sIconSize, false);
-                        BitmapDrawable drawable = new BitmapDrawable(scaledIconBitmap);
-                        engine.setIcon(drawable);
-                    }
-                    addPreference(engine);
-                    // TODO: Bug 892113 - Add event listener here for tapping on each element. Produce a dialog to provide options.
+                    enginePreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
+                        @Override
+                        public boolean onPreferenceClick(Preference preference) {
+                            SearchEnginePreference sPref = (SearchEnginePreference) preference;
+                            // Display the configuration dialog associated with the tapped engine.
+                            sPref.showDialog();
+                            return true;
+                        }
+                    });
+
+                    addPreference(enginePreference);
                 } catch (JSONException e) {
                     Log.e(LOGTAG, "JSONException parsing engine at index " + i, e);
                 }
             }
         }
+
+        // We are no longer interested in this event from Gecko, as we do not request it again with
+        // this instance.
+        GeckoAppShell.unregisterEventListener("SearchEngines:Data", this);
+    }
+
+    /**
+     * Set the default engine to any available engine. Used if the current default is removed or
+     * disabled.
+     */
+    private void setFallbackDefaultEngine() {
+        if (getPreferenceCount() > 0) {
+            SearchEnginePreference aEngine = (SearchEnginePreference) getPreference(0);
+            setDefault(aEngine);
+        }
+    }
+
+    /**
+     * Helper method to send a particular event string to Gecko with an associated engine name.
+     * @param event The type of event to send.
+     * @param engine The engine to which the event relates.
+     */
+    private void sendGeckoEngineEvent(String event, SearchEnginePreference engine) {
+        JSONObject json = new JSONObject();
+        try {
+            json.put("engine", engine.getTitle());
+        } catch (JSONException e) {
+            Log.e(LOGTAG, "JSONException creating search engine configuration change message for Gecko.", e);
+            return;
+        }
+        GeckoAppShell.notifyGeckoOfEvent(GeckoEvent.createBroadcastEvent(event, json.toString()));
+    }
+
+    // Methods called by tapping items on the submenus for each search engine are below.
+
+    /**
+     * Removes the given engine from the set of available engines.
+     * @param engine The engine to remove.
+     */
+    public void uninstall(SearchEnginePreference engine) {
+        removePreference(engine);
+        if (engine == mDefaultEngineReference) {
+            // If they're deleting their default engine, get them a new default engine.
+            setFallbackDefaultEngine();
+        }
+
+        sendGeckoEngineEvent("SearchEngines:Remove", engine);
+    }
+
+    /**
+     * Sets the given engine as the current default engine.
+     * @param engine The intended new default engine.
+     */
+    public void setDefault(SearchEnginePreference engine) {
+        engine.setIsDefaultEngine(true);
+        mDefaultEngineReference.setIsDefaultEngine(false);
+        mDefaultEngineReference = engine;
+
+        sendGeckoEngineEvent("SearchEngines:SetDefault", engine);
     }
 }
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -146,16 +146,22 @@
   <string name="pref_update_autodownload_disabled">&pref_update_autodownload_disabled;</string>
   <string name="pref_update_autodownload_enabled">&pref_update_autodownload_enabled;</string>
 
 
   <string name="pref_about_firefox">&pref_about_firefox;</string>
   <string name="pref_vendor_faqs">&pref_vendor_faqs;</string>
   <string name="pref_vendor_feedback">&pref_vendor_feedback;</string>
 
+  <!-- Strings used in default search provider config preferences menu -->
+  <string name="pref_search_set_default">&pref_search_set_default;</string>
+  <string name="pref_search_default">&pref_search_default;</string>
+  <string name="pref_search_remove">&pref_search_remove;</string>
+  <string name="pref_search_last_toast">&pref_search_last_toast;</string>
+
   <string name="datareporting_notification_title">&datareporting_notification_title;</string>
   <string name="datareporting_notification_action_long">&datareporting_notification_action_long;</string>
   <string name="datareporting_notification_action">&datareporting_notification_action;</string>
   <string name="datareporting_notification_summary">&datareporting_notification_summary;</string>
   <string name="datareporting_notification_summary_short">&datareporting_notification_summary_short;</string>
   <string name="datareporting_notification_ticker_text">&datareporting_notification_ticker_text;</string>
 
   <string name="datareporting_telemetry_title">&datareporting_telemetry_title;</string>
--- a/mobile/android/chrome/content/aboutAddons.js
+++ b/mobile/android/chrome/content/aboutAddons.js
@@ -56,58 +56,46 @@ var ContextMenus = {
     if (enabled) {
       document.getElementById("contextmenu-enable").setAttribute("hidden", "true");
       document.getElementById("contextmenu-disable").removeAttribute("hidden");
     } else {
       document.getElementById("contextmenu-enable").removeAttribute("hidden");
       document.getElementById("contextmenu-disable").setAttribute("hidden", "true");
     }
 
-    // Only show the "Set as Default" menuitem for enabled non-default search engines.
-    if (addon.type == "search" && enabled && addon.id != Services.search.defaultEngine.name) {
-      document.getElementById("contextmenu-default").removeAttribute("hidden");
-    } else {
-      document.getElementById("contextmenu-default").setAttribute("hidden", "true");
-    }
+    document.getElementById("contextmenu-default").setAttribute("hidden", "true");
   },
 
   enable: function(event) {
     Addons.setEnabled(true, this.target.addon);
     this.target = null;
   },
   
   disable: function (event) {
     Addons.setEnabled(false, this.target.addon);
     this.target = null;
   },
   
   uninstall: function (event) {
     Addons.uninstall(this.target.addon);
     this.target = null;
-  },
-
-  setDefaultSearch: function(event) {
-    Addons.setDefaultSearch(this.target.addon);
-    this.target = null;
   }
 }
 
 function init() {
   window.addEventListener("popstate", onPopState, false);
-  Services.obs.addObserver(Addons, "browser-search-engine-modified", false);
 
   AddonManager.addInstallListener(Addons);
   AddonManager.addAddonListener(Addons);
   Addons.getAddons();
   showList();
   ContextMenus.init();
 }
 
 function uninit() {
-  Services.obs.removeObserver(Addons, "browser-search-engine-modified");
   AddonManager.removeInstallListener(Addons);
   AddonManager.removeAddonListener(Addons);
 }
 
 function openLink(aElement) {
   try {
     let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
     let url = formatter.formatURLPref(aElement.getAttribute("pref"));
@@ -356,26 +344,17 @@ var Addons = {
 
     let uninstallBtn = document.getElementById("uninstall-btn");
     if (addon.scope == AddonManager.SCOPE_APPLICATION)
       uninstallBtn.setAttribute("disabled", "true");
     else
       uninstallBtn.removeAttribute("disabled");
 
     let defaultButton = document.getElementById("default-btn");
-    if (addon.type == "search") {
-      if (addon.id == Services.search.defaultEngine.name)
-        defaultButton.setAttribute("disabled", "true");
-      else
-        defaultButton.removeAttribute("disabled");
-
-      defaultButton.removeAttribute("hidden");
-    } else {
-      defaultButton.setAttribute("hidden", "true");
-    }
+    defaultButton.setAttribute("hidden", "true");
 
     let box = document.querySelector("#addons-details > .addon-item .options-box");
     box.innerHTML = "";
 
     // Retrieve the extensions preferences
     try {
       let optionsURL = aListItem.getAttribute("optionsURL");
       let xhr = new XMLHttpRequest();
@@ -419,20 +398,17 @@ var Addons = {
     let detailItem = document.querySelector("#addons-details > .addon-item");
     let addon = aAddon || detailItem.addon;
     if (!addon)
       return;
 
     let listItem = this._getElementForAddon(addon.id);
 
     let opType;
-    if (addon.type == "search") {
-      addon.engine.hidden = !aValue;
-      opType = aValue ? "needs-enable" : "needs-disable";
-    } else if (addon.type == "theme") {
+    if (addon.type == "theme") {
       if (aValue) {
         // We can have only one theme enabled, so disable the current one if any
         let list = document.getElementById("addons-list");
         let item = list.firstElementChild;
         while (item) {
           if (item.addon && (item.addon.type == "theme") && (item.addon.isActive)) {
             item.addon.userDisabled = true;
             item.setAttribute("isDisabled", true);
@@ -487,40 +463,31 @@ var Addons = {
     let detailItem = document.querySelector("#addons-details > .addon-item");
 
     let addon = aAddon || detailItem.addon;
     if (!addon)
       return;
 
     let listItem = this._getElementForAddon(addon.id);
 
-    if (addon.type == "search") {
-      // Make sure the engine isn't hidden before removing it, to make sure it's
-      // visible if the user later re-adds it (works around bug 341833)
-      addon.engine.hidden = false;
-      Services.search.removeEngine(addon.engine);
-      // the search-engine-modified observer will take care of updating the list
-      history.back();
-    } else {
-      addon.uninstall();
-      if (addon.pendingOperations & AddonManager.PENDING_UNINSTALL) {
-        this.showRestart();
+    addon.uninstall();
+    if (addon.pendingOperations & AddonManager.PENDING_UNINSTALL) {
+      this.showRestart();
 
-        // A disabled addon doesn't need a restart so it has no pending ops and
-        // can't be cancelled
-        let opType = this._getOpTypeForOperations(addon.pendingOperations);
-        if (!addon.isActive && opType == "")
-          opType = "needs-uninstall";
+      // A disabled addon doesn't need a restart so it has no pending ops and
+      // can't be cancelled
+      let opType = this._getOpTypeForOperations(addon.pendingOperations);
+      if (!addon.isActive && opType == "")
+        opType = "needs-uninstall";
 
-        detailItem.setAttribute("opType", opType);
-        listItem.setAttribute("opType", opType);
-      } else {
-        list.removeChild(listItem);
-        history.back();
-      }
+      detailItem.setAttribute("opType", opType);
+      listItem.setAttribute("opType", opType);
+    } else {
+      list.removeChild(listItem);
+      history.back();
     }
   },
 
   cancelUninstall: function ev_cancelUninstall() {
     let detailItem = document.querySelector("#addons-details > .addon-item");
     let addon = detailItem.addon;
     if (!addon)
       return;
@@ -530,30 +497,16 @@ var Addons = {
 
     let opType = this._getOpTypeForOperations(addon.pendingOperations);
     detailItem.setAttribute("opType", opType);
 
     let listItem = this._getElementForAddon(addon.id);
     listItem.setAttribute("opType", opType);
   },
 
-  setDefaultSearch: function setDefaultSearch(aAddon) {
-    let addon = aAddon || document.querySelector("#addons-details > .addon-item").addon;
-    if (addon.type != "search")
-      return;
-
-    let engine = Services.search.getEngineByName(addon.id);
-
-    // Move the new default search engine to the top of the search engine list.
-    Services.search.moveEngine(engine, 0);
-    Services.search.defaultEngine = engine;
-
-    document.getElementById("default-btn").setAttribute("disabled", "true");
-  },
-
   showRestart: function showRestart() {
     this._restartCount++;
     gChromeWin.XPInstallObserver.showRestartPrompt();
   },
 
   hideRestart: function hideRestart() {
     this._restartCount--;
     if (this._restartCount == 0)
@@ -585,28 +538,16 @@ var Addons = {
       element = this._createItemForAddon(aAddon);
       list.insertBefore(element, list.firstElementChild);
     }
 
     if (needsRestart)
       element.setAttribute("opType", "needs-restart");
   },
 
-  observe: function observe(aSubject, aTopic, aData) {
-    if (aTopic == "browser-search-engine-modified") {
-      switch (aData) {
-        case "engine-added":
-        case "engine-removed":
-        case "engine-changed":
-          this.getAddons();
-          break;
-      }
-    }
-  },
-
   onInstallFailed: function(aInstall) {
   },
 
   onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {
   },
 
   onDownloadFailed: function(aInstall) {
   },
--- a/mobile/android/chrome/content/aboutAddons.xhtml
+++ b/mobile/android/chrome/content/aboutAddons.xhtml
@@ -52,17 +52,16 @@
   </style>
 </head>
 
 <body dir="&locale.dir;" onload="init();" onunload="uninit();">
   <menu type="context" id="addonmenu">
     <menuitem id="contextmenu-enable" label="&addonAction.enable;" onclick="ContextMenus.enable();"></menuitem>
     <menuitem id="contextmenu-disable" label="&addonAction.disable;" onclick="ContextMenus.disable();"></menuitem>
     <menuitem id="contextmenu-uninstall" label="&addonAction.uninstall;" onclick="ContextMenus.uninstall();"></menuitem>
-    <menuitem id="contextmenu-default" label="&addonAction.setDefault;" onclick="ContextMenus.setDefaultSearch();"></menuitem>
   </menu>
 
   <div id="addons-header" class="header">
     <div>&aboutAddons.header2;</div>
     <div id="header-button" role="button" pref="extensions.getAddons.browseAddons" onclick="openLink(this);"/>
   </div>
   <div id="addons-list" class="list" style="display: none;">
   </div>
@@ -79,17 +78,16 @@
         <div class="description-full"></div>
       </div>
       <div class="status status-uninstalled show-on-uninstall"></div>
       <div class="buttons">
         <button id="enable-btn" class="show-on-disable hide-on-enable hide-on-uninstall" onclick="Addons.enable();">&addonAction.enable;</button>
         <button id="disable-btn" class="show-on-enable hide-on-disable hide-on-uninstall" onclick="Addons.disable();">&addonAction.disable;</button>
         <button id="uninstall-btn" class="hide-on-uninstall" onclick="Addons.uninstall();">&addonAction.uninstall;</button>
         <button id="cancel-btn" class="show-on-uninstall" onclick="Addons.cancelUninstall();">&addonAction.undo;</button>
-        <button id="default-btn" class="show-on-enable hide-on-disable hide-on-uninstall" onclick="Addons.setDefaultSearch();">&addonAction.setDefault;</button>
       </div>
       <div class="options-header">&aboutAddons.options;</div>
       <div class="options-box"></div>
     </div>
   </div>
 
   <script type="application/javascript;version=1.8" src="chrome://browser/content/aboutAddons.js"></script>
 </body>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -6416,68 +6416,124 @@ OverscrollController.prototype = {
 
 var SearchEngines = {
   _contextMenuId: null,
   PREF_SUGGEST_ENABLED: "browser.search.suggest.enabled",
   PREF_SUGGEST_PROMPTED: "browser.search.suggest.prompted",
 
   init: function init() {
     Services.obs.addObserver(this, "SearchEngines:Get", false);
+    Services.obs.addObserver(this, "SearchEngines:GetVisible", false);
+    Services.obs.addObserver(this, "SearchEngines:SetDefault", false);
+    Services.obs.addObserver(this, "SearchEngines:Remove", false);
     let contextName = Strings.browser.GetStringFromName("contextmenu.addSearchEngine");
     let filter = {
       matches: function (aElement) {
         return (aElement.form && NativeWindow.contextmenus.textContext.matches(aElement));
       }
     };
     this._contextMenuId = NativeWindow.contextmenus.add(contextName, filter, this.addEngine);
   },
 
   uninit: function uninit() {
     Services.obs.removeObserver(this, "SearchEngines:Get");
+    Services.obs.removeObserver(this, "SearchEngines:GetVisible");
+    Services.obs.removeObserver(this, "SearchEngines:SetDefault");
+    Services.obs.removeObserver(this, "SearchEngines:Remove");
     if (this._contextMenuId != null)
       NativeWindow.contextmenus.remove(this._contextMenuId);
   },
 
-  _handleSearchEnginesGet: function _handleSearchEnginesGet(rv) {
+  // Fetch list of search engines. all ? All engines : Visible engines only.
+  _handleSearchEnginesGet: function _handleSearchEnginesGet(rv, all) {
     if (!Components.isSuccessCode(rv)) {
       Cu.reportError("Could not initialize search service, bailing out.");
       return;
     }
-    let engineData = Services.search.getVisibleEngines({});
+    let engineData;
+    if (all) {
+      engineData = Services.search.getEngines({});
+    } else {
+      engineData = Services.search.getVisibleEngines({});
+    }
+
+    // These engines are the bundled ones - they may not be uninstalled.
+    let immutableEngines = Services.search.getDefaultEngines();
+
     let searchEngines = engineData.map(function (engine) {
       return {
         name: engine.name,
         identifier: engine.identifier,
-        iconURI: (engine.iconURI ? engine.iconURI.spec : null)
+        iconURI: (engine.iconURI ? engine.iconURI.spec : null),
+        hidden: engine.hidden,
+        immutable: immutableEngines.indexOf(engine) != -1
       };
     });
 
     let suggestTemplate = null;
     let suggestEngine = null;
     let engine = this.getSuggestionEngine();
     if (engine != null) {
       suggestEngine = engine.name;
       suggestTemplate = engine.getSubmission("__searchTerms__", "application/x-suggestions+json").uri.spec;
     }
 
+
+    // By convention, the currently configured default engine is at position zero in searchEngines.
     sendMessageToJava({
       type: "SearchEngines:Data",
       searchEngines: searchEngines,
       suggest: {
         engine: suggestEngine,
         template: suggestTemplate,
         enabled: Services.prefs.getBoolPref(this.PREF_SUGGEST_ENABLED),
         prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED)
       }
     });
   },
 
+  _handleSearchEnginesGetAll: function _handleSearchEnginesGetAll(rv) {
+    this._handleSearchEnginesGet(rv, true);
+  },
+  _handleSearchEnginesGetVisible: function _handleSearchEnginesGetVisible(rv) {
+    this._handleSearchEnginesGet(rv, false)
+  },
+
+  // Helper method to extract the engine name from a JSON. Simplifies the observe function.
+  _extractEngineFromJSON: function _extractEngineFromJSON(aData) {
+    let data = JSON.parse(aData);
+    return Services.search.getEngineByName(data.engine);
+  },
+
   observe: function observe(aSubject, aTopic, aData) {
-    if (aTopic == "SearchEngines:Get") {
-      Services.search.init(this._handleSearchEnginesGet.bind(this));
+    let engine;
+    switch(aTopic) {
+      case "SearchEngines:GetVisible":
+        Services.search.init(this._handleSearchEnginesGetVisible.bind(this));
+        break;
+      case "SearchEngines:Get":
+        // Return a list of all engines, including "Hidden" ones.
+        Services.search.init(this._handleSearchEnginesGetAll.bind(this));
+        break;
+      case "SearchEngines:SetDefault":
+        engine = this._extractEngineFromJSON(aData);
+        // Move the new default search engine to the top of the search engine list.
+        Services.search.moveEngine(engine, 0);
+        Services.search.defaultEngine = engine;
+        break;
+      case "SearchEngines:Remove":
+        // Make sure the engine isn't hidden before removing it, to make sure it's
+        // visible if the user later re-adds it (works around bug 341833)
+        engine = this._extractEngineFromJSON(aData);
+        engine.hidden = false;
+        Services.search.removeEngine(engine);
+        break;
+      default:
+        dump("Unexpected message type observed: " + aTopic);
+        break;
     }
   },
 
   getSuggestionEngine: function () {
     let engines = [ Services.search.currentEngine,
                     Services.search.defaultEngine ];
 
     for (let i = 0; i < engines.length; i++) {
--- a/mobile/android/locales/en-US/chrome/aboutAddons.dtd
+++ b/mobile/android/locales/en-US/chrome/aboutAddons.dtd
@@ -5,9 +5,8 @@
 <!ENTITY aboutAddons.title2                     "Add-ons">
 <!ENTITY aboutAddons.header2                    "Your Add-ons">
 <!ENTITY aboutAddons.options                    "Options">
 
 <!ENTITY addonAction.enable                     "Enable">
 <!ENTITY addonAction.disable                    "Disable">
 <!ENTITY addonAction.uninstall                  "Uninstall">
 <!ENTITY addonAction.undo                       "Undo">
-<!ENTITY addonAction.setDefault                 "Set as Default">
--- a/mobile/android/locales/en-US/chrome/aboutAddons.properties
+++ b/mobile/android/locales/en-US/chrome/aboutAddons.properties
@@ -1,14 +1,11 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
-addonsSearchEngine.description=Integrated Search
-
 addonType.extension=Extension
 addonType.theme=Theme
 addonType.locale=Locale
-addonType.search=Search
 
 addonStatus.uninstalled=%S will be uninstalled after restart.
 
 addons.browseAll=Browse all Firefox Add-ons