Bug 892113 - Dialog to manage installed search providers. r=liuche
authorChris Kitching <ckitching@mozilla.com>
Mon, 05 Aug 2013 08:39:29 -0700
changeset 141398 7bbaba1b712f689d37217b2c7eea02520c3e8315
parent 141367 ad0ae007aa9e03cd74e9005cd6652e544139b3b5
child 141399 b92db71dc525ce3db360b2727818ca7b8a1e1aab
push id25058
push userryanvm@gmail.com
push dateTue, 06 Aug 2013 00:00:00 +0000
treeherdermozilla-central@ff6bb97c0c74 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersliuche
bugs892113
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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