Bug 696532 - Basic add-on manager support [r=wesj]
authorMark Finkle <mfinkle@mozilla.com>
Tue, 22 Nov 2011 01:12:59 -0500
changeset 83574 8bb653dcddc9278bf45a8c47a3637f8440615cbf
parent 83573 5f5f3bb411ff1d1a292882c7af2a57cb8786f3d1
child 83575 c26b7a14e5bd15ed873e270a456b7f1226fdd036
push id519
push userakeybl@mozilla.com
push dateWed, 01 Feb 2012 00:38:35 +0000
treeherdermozilla-beta@788ea1ef610b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerswesj
bugs696532
milestone11.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 696532 - Basic add-on manager support [r=wesj]
mobile/android/chrome/content/aboutAddons.xhtml
mobile/android/chrome/content/browser.js
mobile/android/chrome/jar.mn
mobile/android/locales/en-US/chrome/aboutAddons.dtd
mobile/android/locales/en-US/chrome/aboutAddons.properties
mobile/android/locales/jar.mn
mobile/android/themes/core/aboutAddons.css
mobile/android/themes/core/jar.mn
new file mode 100644
--- /dev/null
+++ b/mobile/android/chrome/content/aboutAddons.xhtml
@@ -0,0 +1,463 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
+  "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd" [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+%brandDTD;
+<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd" >
+%globalDTD;
+<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutAddons.dtd" >
+%aboutDTD;
+]>
+
+<!-- ***** BEGIN LICENSE BLOCK *****
+   - Version: MPL 1.1/GPL 2.0/LGPL 2.1
+   -
+   - The contents of this file are subject to the Mozilla Public License Version
+   - 1.1 (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.mozilla.org/MPL/
+   -
+   - Software distributed under the License is distributed on an "AS IS" basis,
+   - WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+   - for the specific language governing rights and limitations under the
+   - License.
+   -
+   - The Original Code is Mozilla Communicator client code, released
+   - March 31, 1998.
+   -
+   - The Initial Developer of the Original Code is
+   - Netscape Communications Corporation.
+   - Portions created by the Initial Developer are Copyright (C) 1998-1999
+   - the Initial Developer. All Rights Reserved.
+   -
+   - Contributor(s):
+   -   Mark Finkle <mfinkle@mozilla.com>
+   -
+   - Alternatively, the contents of this file may be used under the terms of
+   - either the GNU General Public License Version 2 or later (the "GPL"), or
+   - the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+   - in which case the provisions of the GPL or the LGPL are applicable instead
+   - of those above. If you wish to allow use of your version of this file only
+   - under the terms of either the GPL or the LGPL, and not to allow others to
+   - use your version of this file under the terms of the MPL, indicate your
+   - decision by deleting the provisions above and replace them with the notice
+   - and other provisions required by the GPL or the LGPL. If you do not delete
+   - the provisions above, a recipient may use your version of this file under
+   - the terms of any one of the MPL, the GPL or the LGPL.
+   -
+   - ***** END LICENSE BLOCK ***** -->
+
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+  <title>&aboutAddons.title;</title>
+  <meta name="viewport" content="width=480; initial-scale=.6667; user-scalable=0" />
+  <link rel="icon" type="image/png" href="chrome://branding/content/favicon32.png" />
+  <link rel="stylesheet" href="chrome://browser/skin/aboutAddons.css" type="text/css"/>
+  <style>
+    .hide-on-enable,
+    .show-on-error,
+    .show-on-uninstall,
+    .show-on-install,
+    .show-on-restart,
+    div[isDisabled="true"] .hide-on-disable {
+      display: none;
+    }
+    
+    div[error] .show-on-error,
+    div[opType="needs-restart"] .show-on-restart,
+    div[opType="needs-uninstall"] .show-on-uninstall,
+    div[opType="needs-install"] .show-on-install,
+    div[opType="needs-enable"] .show-on-enable,
+    div[opType="needs-disable"] .show-on-disable,
+    div[isDisabled="true"] .show-on-disable {
+      display: -moz-box;
+    }
+    
+    div[opType="needs-restart"] .hide-on-restart,
+    div[opType="needs-uninstall"] .hide-on-uninstall,
+    div[isDisabled="true"][opType="needs-uninstall"],
+    div[opType="needs-install"] .hide-on-install,
+    div[opType="needs-enable"] .hide-on-enable,
+    div[opType="needs-disable"] .hide-on-disable {
+      display: none;
+    }
+  </style>
+</head>
+
+<body dir="&locale.dir;" onload="init();" onunload="uninit();">
+  <div id="addons-header">
+    <div>&aboutAddons.header;</div>
+  </div>
+  <div id="addons-list" style="display: none;">
+  </div>
+
+  <script type="application/javascript;version=1.8"><![CDATA[
+    let Ci = Components.interfaces, Cc = Components.classes, Cu = Components.utils;
+
+    Cu.import("resource://gre/modules/Services.jsm")
+    Cu.import("resource://gre/modules/AddonManager.jsm");
+
+    let gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutAddons.properties");
+
+    let gChromeWin = null;
+    function getChromeWin() {
+      if (!gChromeWin) {
+        gChromeWin = window
+                    .QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIWebNavigation)
+                    .QueryInterface(Ci.nsIDocShellTreeItem)
+                    .rootTreeItem
+                    .QueryInterface(Ci.nsIInterfaceRequestor)
+                    .getInterface(Ci.nsIDOMWindow)
+                    .QueryInterface(Ci.nsIDOMChromeWindow);
+      }
+      return gChromeWin;
+    }
+
+    function init() {
+      AddonManager.addInstallListener(Addons);
+      Addons.getAddons();
+    }
+
+    function uninit() {
+      AddonManager.removeInstallListener(Addons);
+    }
+
+    var Addons = {
+      _createItem: function _createItem(aAddon) {
+        let outer = document.createElement("div");
+        outer.setAttribute("addonID", aAddon.id);
+        outer.className = "addon-item";
+
+        let img = document.createElement("img");
+        img.className = "favicon";
+        img.setAttribute("src", aAddon.iconURL);
+        outer.appendChild(img);
+
+        let inner = document.createElement("div");
+        inner.className = "inner";
+
+        let titlePart = document.createElement("span");
+        titlePart.textContent = aAddon.name;
+        titlePart.className = "title";
+        inner.appendChild(titlePart);
+
+        let versionPart = document.createElement("span");
+        versionPart.textContent = aAddon.version;
+        versionPart.className = "version";
+        inner.appendChild(versionPart);
+
+        if ("description" in aAddon) {
+          let descPart = document.createElement("div");
+          descPart.textContent = aAddon.description;
+          descPart.className = "description";
+          inner.appendChild(descPart);
+        }
+
+        outer.appendChild(inner);
+
+        let buttons = document.createElement("div");
+        buttons.className = "buttons";
+
+        let optionsBtn = document.createElement("button");
+        optionsBtn.className = "options-btn";
+        optionsBtn.textContent = gStringBundle.GetStringFromName("addonAction.options");
+        optionsBtn.setAttribute("disabled", "true"); // TODO (bug 696533)
+        optionsBtn.addEventListener("click", function() {
+          this.toggleOptions(outer);
+        }.bind(this), false)
+        buttons.appendChild(optionsBtn);
+
+        let enableBtn = document.createElement("button");
+        enableBtn.className = "show-on-disable hide-on-enable hide-on-uninstall";
+        enableBtn.textContent = gStringBundle.GetStringFromName("addonAction.enable");
+        if (aAddon.appDisabled)
+          enableBtn.setAttribute("disabled", "true");
+        enableBtn.addEventListener("click", function() {
+          this.enable(outer);
+        }.bind(this), false)
+        buttons.appendChild(enableBtn);
+
+        let disableBtn = document.createElement("button");
+        disableBtn.className = "show-on-enable hide-on-disable hide-on-uninstall";
+        disableBtn.textContent = gStringBundle.GetStringFromName("addonAction.disable");
+        disableBtn.addEventListener("click", function() {
+          this.disable(outer);
+        }.bind(this), false)
+        buttons.appendChild(disableBtn);
+
+        let uninstallBtn = document.createElement("button");
+        uninstallBtn.className = "hide-on-uninstall";
+        uninstallBtn.textContent = gStringBundle.GetStringFromName("addonAction.uninstall");
+        if (aAddon.scope == AddonManager.SCOPE_APPLICATION)
+          uninstallBtn.setAttribute("disabled", "true");
+        uninstallBtn.addEventListener("click", function() {
+          this.uninstall(outer);
+        }.bind(this), false)
+        buttons.appendChild(uninstallBtn);
+
+        let cancelUninstallBtn = document.createElement("button");
+        cancelUninstallBtn.className = "show-on-uninstall";
+        cancelUninstallBtn.textContent = gStringBundle.GetStringFromName("addonAction.cancel");
+        cancelUninstallBtn.addEventListener("click", function() {
+          this.cancelUninstall(outer);
+        }.bind(this), false)
+        buttons.appendChild(cancelUninstallBtn);
+
+        outer.appendChild(buttons);
+        return outer;
+      },
+
+      _createItemForAddon: function _createItemForAddon(aAddon) {
+        let appManaged = (aAddon.scope == AddonManager.SCOPE_APPLICATION);
+        let opType = this._getOpTypeForOperations(aAddon.pendingOperations);
+        let updateable = (aAddon.permissions & AddonManager.PERM_CAN_UPGRADE) > 0;
+        let uninstallable = (aAddon.permissions & AddonManager.PERM_CAN_UNINSTALL) > 0;
+    
+        let blocked = "";
+        switch(aAddon.blocklistState) {
+          case Ci.nsIBlocklistService.STATE_BLOCKED:
+            blocked = "blocked";
+            break;
+          case Ci.nsIBlocklistService.STATE_SOFTBLOCKED:
+            blocked = "softBlocked";
+            break;
+          case Ci.nsIBlocklistService.STATE_OUTDATED:
+            blocked = "outdated";
+            break;
+        }
+
+        let item = this._createItem(aAddon);
+        item.setAttribute("isDisabled", !aAddon.isActive);
+        item.setAttribute("opType", opType);
+        item.setAttribute("updateable", updateable);
+        if (blocked)
+          item.setAttribute("blockedStatus", blocked);
+        item.setAttribute("optionsURL", aAddon.optionsURL || "");
+        item.addon = aAddon;
+
+        return item;
+      },
+
+      _getElementForAddon: function(aKey) {
+        let list = document.getElementById("addons-list");
+        let element = list.querySelector("div[addonID='" + aKey + "']");
+        return element;
+      },
+
+      getAddons: function getAddons() {
+        // Clear all content before filling the addons
+        let list = document.getElementById("addons-list");
+        list.innerHTML = "";
+
+        let self = this;
+        AddonManager.getAddonsByTypes(["extension", "theme", "locale"], function(aAddons) {
+          for (let i=0; i<aAddons.length; i++) {
+            let item = self._createItemForAddon(aAddons[i]);
+            list.appendChild(item);
+          }
+
+          list.style.display = "block";
+          document.getElementById("addons-header").setAttribute("showlist", "true");
+
+          // Load the search engines
+          let defaults = Services.search.getDefaultEngines({ }).map(function (e) e.name);
+          function isDefault(aEngine)
+            defaults.indexOf(aEngine.name) != -1
+
+          let defaultDescription = gStringBundle.GetStringFromName("addonsSearchEngine.description");
+
+          let engines = Services.search.getEngines({ });
+          for (let e = 0; e < engines.length; e++) {
+            let engine = engines[e];
+            let addon = {};
+            addon.id = engine.name;
+            addon.type = "search";
+            addon.name = engine.name;
+            addon.version = "";
+            addon.description = engine.description || defaultDescription;
+            addon.iconURL = engine.iconURI ? engine.iconURI.spec : "";
+            addon.appDisabled = false;
+            addon.scope = isDefault(engine) ? AddonManager.SCOPE_APPLICATION : AddonManager.SCOPE_PROFILE;
+            addon.engine = engine;
+
+            let item = self._createItem(addon);
+            item.setAttribute("isDisabled", engine.hidden);
+            item.setAttribute("updateable", "false");
+            item.setAttribute("opType", "");
+            item.addon = addon;
+            list.appendChild(item);
+          }
+        });
+      },
+
+      _getOpTypeForOperations: function _getOpTypeForOperations(aOperations) {
+        if (aOperations & AddonManager.PENDING_UNINSTALL)
+          return "needs-uninstall";
+        if (aOperations & AddonManager.PENDING_ENABLE)
+          return "needs-enable";
+        if (aOperations & AddonManager.PENDING_DISABLE)
+          return "needs-disable";
+        return "";
+      },
+
+      enable: function enable(aItem) {
+        if (!aItem.addon)
+          return;
+
+        let opType;
+        if (aItem.addon.type == "search") {
+          aItem.setAttribute("isDisabled", false);
+          aItem.addon.engine.hidden = false;
+          opType = "needs-enable";
+        } else if (aItem.addon.type == "theme") {
+          // We can have only one theme enabled, so disable the current one if any
+          let theme = null;
+          let list = document.getElementById("addons-list");
+          let item = list.firstElementChild;
+          while (item) {
+            if (item.addon && (item.addon.type == "theme") && (item.addon.isActive)) {
+              theme = item;
+              break;
+            }
+            item = item.nextSibling;
+          }
+          if (theme)
+            this.disable(theme);
+
+          aItem.addon.userDisabled = false;
+          aItem.setAttribute("isDisabled", false);
+        } else {
+          aItem.addon.userDisabled = false;
+          opType = this._getOpTypeForOperations(aItem.addon.pendingOperations);
+    
+          if (aItem.addon.pendingOperations & AddonManager.PENDING_ENABLE) {
+            this.showRestart();
+          } else {
+            aItem.setAttribute("isDisabled", false);
+            if (aItem.getAttribute("opType") == "needs-disable")
+              this.hideRestart();
+          }
+        }
+    
+        aItem.setAttribute("opType", opType);
+      },
+
+      disable: function disable(aItem) {
+        if (!aItem.addon)
+          return;
+
+        let opType;
+        if (aItem.addon.type == "search") {
+          aItem.setAttribute("isDisabled", true);
+          aItem.addon.engine.hidden = true;
+          opType = "needs-disable";
+        } else if (aItem.addon.type == "theme") {
+          aItem.addon.userDisabled = true;
+          aItem.setAttribute("isDisabled", true);
+        } else if (aItem.addon.type == "locale") {
+          aItem.addon.userDisabled = true;
+          aItem.setAttribute("isDisabled", true);
+        } else {
+          aItem.addon.userDisabled = true;
+          opType = this._getOpTypeForOperations(aItem.addon.pendingOperations);
+    
+          if (aItem.addon.pendingOperations & AddonManager.PENDING_DISABLE) {
+            this.showRestart();
+          } else {
+            aItem.setAttribute("isDisabled", !aItem.addon.isActive);
+            if (aItem.getAttribute("opType") == "needs-enable")
+              this.hideRestart();
+          }
+        }
+    
+        aItem.setAttribute("opType", opType);
+      },
+    
+      uninstall: function uninstall(aItem) {
+        let list = document.getElementById("addons-list");
+        if (!aItem.addon) {
+          list.removeChild(aItem);
+          return;
+        }
+
+        let opType;
+        if (aItem.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)
+          aItem.addon.engine.hidden = false;
+          Services.search.removeEngine(aItem.addon.engine);
+          // the search-engine-modified observer in browser.js will take care of
+          // updating the list
+        } else {
+          aItem.addon.uninstall();
+          opType = this._getOpTypeForOperations(aItem.addon.pendingOperations);
+
+          if (aItem.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
+            if (!aItem.addon.isActive && opType == "")
+              opType = "needs-uninstall";
+
+            aItem.setAttribute("opType", opType);
+          } else {
+            list.removeChild(aItem);
+          }
+        }
+      },
+    
+      cancelUninstall: function ev_cancelUninstall(aItem) {
+        if (!aItem.addon)
+          return;
+
+        aItem.addon.cancelUninstall();
+        this.hideRestart();
+    
+        let opType = this._getOpTypeForOperations(aItem.addon.pendingOperations);
+        aItem.setAttribute("opType", opType);
+      },
+
+      showRestart: function showRestart(aMode) {
+        // TODO (bug 704406)
+      },
+
+      hideRestart: function hideRestart(aMode) {
+        // TODO (bug 704406)
+      },
+
+      onInstallEnded: function(aInstall, aAddon) {
+        let needsRestart = false;
+        if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE))
+          needsRestart = true;
+        else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL)
+          needsRestart = true;
+
+        let list = document.getElementById("addons-list");
+        let element = this._getElementForAddon(aAddon.id);
+        if (!element) {
+          element = this._createItemForAddon(aAddon);
+          list.insertBefore(element, list.firstElementChild);
+        }
+
+        if (needsRestart)
+          element.setAttribute("opType", "needs-restart");
+      },
+
+      onInstallFailed: function(aInstall) {
+      },
+
+      onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {
+      },
+
+      onDownloadFailed: function(aInstall) {
+      },
+
+      onDownloadCancelled: function(aInstall) {
+      }
+    }
+  ]]></script>
+</body>
+</html>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -38,18 +38,18 @@
 "use strict";
 
 let Cc = Components.classes;
 let Ci = Components.interfaces;
 let Cu = Components.utils;
 let Cr = Components.results;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/Services.jsm")
+Cu.import("resource://gre/modules/AddonManager.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "URIFixup",
   "@mozilla.org/docshell/urifixup;1", "nsIURIFixup");
 
 // TODO: Take into account ppi in these units?
 
 // The ratio of velocity that is retained every ms.
 const kPanDeceleration = 0.999;
@@ -161,19 +161,16 @@ var BrowserApp = {
     Services.obs.addObserver(this, "Preferences:Get", false);
     Services.obs.addObserver(this, "Preferences:Set", false);
     Services.obs.addObserver(this, "ScrollTo:FocusedInput", false);
     Services.obs.addObserver(this, "Sanitize:ClearAll", false);
     Services.obs.addObserver(this, "PanZoom:PanZoom", false);
     Services.obs.addObserver(this, "FullScreen:Exit", false);
     Services.obs.addObserver(this, "Viewport:Change", false);
 
-    Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false);
-    Services.obs.addObserver(XPInstallObserver, "addon-install-started", false);
-
     function showFullScreenWarning() {
       NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short");
     }
 
     window.addEventListener("fullscreen", function() {
       sendMessageToJava({
         gecko: {
           type: window.fullScreen ? "ToggleChrome:Show" : "ToggleChrome:Hide"
@@ -187,16 +184,17 @@ var BrowserApp = {
     // When a restricted key is pressed in DOM full-screen mode, we should display
     // the "Press ESC to exit" warning message.
     window.addEventListener("MozShowFullScreenWarning", showFullScreenWarning, true);
 
     NativeWindow.init();
     Downloads.init();
     OfflineApps.init();
     IndexedDB.init();
+    XPInstallObserver.init();
 
     // Init LoginManager
     Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
 
     let uri = "about:support";
     if ("arguments" in window && window.arguments[0])
       uri = window.arguments[0];
 
@@ -240,26 +238,23 @@ var BrowserApp = {
             Services.prefs.setBoolPref("toolkit.telemetry.enabled", false);
           }
         }
       ];
       let brandShortName = Strings.brand.GetStringFromName("brandShortName");
       let message = Strings.browser.formatStringFromName("telemetry.optin.message", [brandShortName], 1);
       NativeWindow.doorhanger.show(message, "telemetry-optin", buttons);
     }
-
   },
 
   shutdown: function shutdown() {
     NativeWindow.uninit();
     OfflineApps.uninit();
     IndexedDB.uninit();
-
-    Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked");
-    Services.obs.removeObserver(XPInstallObserver, "addon-install-started");
+    XPInstallObserver.uninit();
   },
 
   get tabs() {
     return this._tabs;
   },
 
   get selectedTab() {
     return this._selectedTab;
@@ -1996,67 +1991,123 @@ var FormAssistant = {
         aFunction.call(this, child, optionIndex);
         optionIndex++;
       }
     }
   }
 }
 
 var XPInstallObserver = {
+  init: function xpi_init() {
+    Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false);
+    Services.obs.addObserver(XPInstallObserver, "addon-install-started", false);
+
+    AddonManager.addInstallListener(XPInstallObserver);
+  },
+
+  uninit: function xpi_uninit() {
+    Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked");
+    Services.obs.removeObserver(XPInstallObserver, "addon-install-started");
+
+    AddonManager.removeInstallListener(XPInstallObserver);
+  },
+
   observe: function xpi_observer(aSubject, aTopic, aData) {
     switch (aTopic) {
       case "addon-install-started":
         NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsDownloading"), "short");
         break;
       case "addon-install-blocked":
-        dump("XPInstallObserver addon-install-blocked");
         let installInfo = aSubject.QueryInterface(Ci.amIWebInstallInfo);
         let host = installInfo.originatingURI.host;
 
         let brandShortName = Strings.brand.GetStringFromName("brandShortName");
-        let notificationName, buttons, messageString;
+        let notificationName, buttons, message;
         let strings = Strings.browser;
         let enabled = true;
         try {
           enabled = Services.prefs.getBoolPref("xpinstall.enabled");
         }
         catch (e) {}
 
         if (!enabled) {
           notificationName = "xpinstall-disabled";
           if (Services.prefs.prefIsLocked("xpinstall.enabled")) {
-            messageString = strings.GetStringFromName("xpinstallDisabledMessageLocked");
+            message = strings.GetStringFromName("xpinstallDisabledMessageLocked");
             buttons = [];
           } else {
-            messageString = strings.formatStringFromName("xpinstallDisabledMessage2", [brandShortName, host], 2);
+            message = strings.formatStringFromName("xpinstallDisabledMessage2", [brandShortName, host], 2);
             buttons = [{
               label: strings.GetStringFromName("xpinstallDisabledButton"),
               callback: function editPrefs() {
                 Services.prefs.setBoolPref("xpinstall.enabled", true);
                 return false;
               }
             }];
           }
         } else {
           notificationName = "xpinstall";
-          messageString = strings.formatStringFromName("xpinstallPromptWarning2", [brandShortName, host], 2);
+          message = strings.formatStringFromName("xpinstallPromptWarning2", [brandShortName, host], 2);
 
           buttons = [{
             label: strings.GetStringFromName("xpinstallPromptAllowButton"),
             callback: function() {
               // Kick off the install
               installInfo.install();
               return false;
             }
           }];
         }
-        NativeWindow.doorhanger.show(messageString, aTopic, buttons);
+        NativeWindow.doorhanger.show(message, aTopic, buttons);
         break;
     }
-  }
+  },
+
+  onInstallEnded: function(aInstall, aAddon) {
+    let needsRestart = false;
+    if (aInstall.existingAddon && (aInstall.existingAddon.pendingOperations & AddonManager.PENDING_UPGRADE))
+      needsRestart = true;
+    else if (aAddon.pendingOperations & AddonManager.PENDING_INSTALL)
+      needsRestart = true;
+
+    if (needsRestart) {
+      buttons = [{
+        label: Strings.browser.GetStringFromName("notificationRestart.button"),
+        callback: function() {
+          // Notify all windows that an application quit has been requested
+          let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+          Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
+    
+          // If nothing aborted, quit the app
+          if (cancelQuit.data == false) {
+            let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+            appStartup.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
+          }
+        }
+      }];
+
+      let message = Strings.browser.GetStringFromName("notificationRestart.normal");
+      NativeWindow.doorhanger.show(message, "addon-app-restart", buttons, BrowserApp.selectedTab.id, { persistence: -1 });
+    } else {
+      let message = Strings.browser.GetStringFromName("alertAddonsInstalledNoRestart");
+      NativeWindow.toast.show(message, "short");
+    }
+  },
+
+  onInstallFailed: function(aInstall) {
+    NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsFail"), "short");
+  },
+
+  onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {},
+
+  onDownloadFailed: function(aInstall) {
+    this.onInstallFailed(aInstall);
+  },
+
+  onDownloadCancelled: function(aInstall) {}
 };
 
 /**
  * Handler for blocked popups, triggered by DOMUpdatePageReport events in browser.xml
  */
 var PopupBlockerObserver = {
   onUpdatePageReport: function onUpdatePageReport(aEvent) {
     let browser = BrowserApp.selectedBrowser;
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -1,15 +1,16 @@
 #filter substitution
 
 chrome.jar:
 % content browser %content/
 
 * content/about.xhtml                  (content/about.xhtml)
   content/config.xhtml                 (content/config.xhtml)
+  content/aboutAddons.xhtml            (content/aboutAddons.xhtml)
   content/aboutCertError.xhtml         (content/aboutCertError.xhtml)
   content/aboutHome.xhtml              (content/aboutHome.xhtml)
 * content/aboutRights.xhtml            (content/aboutRights.xhtml)
   content/blockedSite.xhtml            (content/blockedSite.xhtml)
   content/languages.properties         (content/languages.properties)
 * content/browser.xul                  (content/browser.xul)
 * content/browser.js                   (content/browser.js)
 * content/bindings.xml                 (content/bindings.xml)
@@ -21,8 +22,9 @@ chrome.jar:
 % content branding %content/branding/
   content/sanitize.js                  (content/sanitize.js)
   content/exceptions.js                (content/exceptions.js)
 * content/downloads.js                 (content/downloads.js)
   content/netError.xhtml               (content/netError.xhtml)
   
 % override chrome://global/content/config.xul chrome://browser/content/config.xhtml
 % override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml
+% override chrome://mozapps/content/extensions/extensions.xul chrome://browser/content/aboutAddons.xhtml
new file mode 100644
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutAddons.dtd
@@ -0,0 +1,2 @@
+<!ENTITY aboutAddons.title                      "Add-ons Manager">
+<!ENTITY aboutAddons.header                     "Add-ons">
new file mode 100644
--- /dev/null
+++ b/mobile/android/locales/en-US/chrome/aboutAddons.properties
@@ -0,0 +1,6 @@
+addonAction.enable=Enable
+addonAction.disable=Disable
+addonAction.uninstall=Uninstall
+addonAction.cancel=Cancel
+addonAction.options=Options
+addonsSearchEngine.description=Integrated Search
--- a/mobile/android/locales/jar.mn
+++ b/mobile/android/locales/jar.mn
@@ -1,13 +1,15 @@
 #filter substitution
 
 @AB_CD@.jar:
 % locale browser @AB_CD@ %locale/@AB_CD@/browser/
   locale/@AB_CD@/browser/about.dtd                (%chrome/about.dtd)
+  locale/@AB_CD@/browser/aboutAddons.dtd          (%chrome/aboutAddons.dtd)
+  locale/@AB_CD@/browser/aboutAddons.properties   (%chrome/aboutAddons.properties)
   locale/@AB_CD@/browser/aboutCertError.dtd       (%chrome/aboutCertError.dtd)
   locale/@AB_CD@/browser/aboutHome.dtd            (%chrome/aboutHome.dtd)
   locale/@AB_CD@/browser/browser.dtd              (%chrome/browser.dtd)
   locale/@AB_CD@/browser/browser.properties       (%chrome/browser.properties)
   locale/@AB_CD@/browser/config.dtd               (%chrome/config.dtd)
   locale/@AB_CD@/browser/config.properties        (%chrome/config.properties)
   locale/@AB_CD@/browser/localepicker.properties  (%chrome/localepicker.properties)
   locale/@AB_CD@/browser/region.properties        (%chrome/region.properties)
new file mode 100644
--- /dev/null
+++ b/mobile/android/themes/core/aboutAddons.css
@@ -0,0 +1,107 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (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.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Corporation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Mark Finkle <mfinkle@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+html {
+  font-size: 24px;
+}
+
+.addons-header {
+  border-bottom: 3px solid black;
+}
+
+.addon-item {
+  border-bottom: 1px solid black;
+  padding: 8px;
+  position: relative;
+}
+
+.addon-item:last-child {
+  border-bottom: 0;
+}
+
+.addon-item:not([optionsURL]) .options-btn {
+  visibility: hidden;
+}
+
+/* Make room for the image */
+.inner {
+  -moz-margin-start: 48px;
+}
+
+.title {
+  color: black;
+}
+
+.version {
+  /* The addon title is not localized, so keep the margin on the left side */
+  margin-left: 12px;
+  font-size: 18px;
+  color: gray;
+}
+
+.description {
+  width: 100%;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+}
+
+.buttons {
+  padding-top: 8px;
+}
+
+body[dir="ltr"] .favicon {
+  left: 12px;
+}
+
+body[dir="ltr"] .favicon {
+  right: 12px;
+}
+
+.favicon {
+  border: none;
+  top: 8;
+  width: 32px;
+  height: 32px;
+  position: absolute;
+}
+
+button {
+  color: black;
+  font-size: 28px !important;
+  padding: 5px;
+}
--- a/mobile/android/themes/core/jar.mn
+++ b/mobile/android/themes/core/jar.mn
@@ -3,16 +3,17 @@
 chrome.jar:
 % skin browser classic/1.0 %skin/ os!=Android
 % skin browser classic/1.0 %skin/ os=Android osversion<2.3
 % skin browser froyo/1.0 %skin/
 # NOTE: If you add a new file here, you'll need to add it to the gingerbread
 # and honeycomb sections at the bottom of this file
   skin/aboutPage.css                        (aboutPage.css)
   skin/about.css                            (about.css)
+  skin/aboutAddons.css                      (aboutAddons.css)
   skin/aboutHome.css                        (aboutHome.css)
 * skin/browser.css                          (browser.css)
 * skin/content.css                          (content.css)
   skin/config.css                           (config.css)
   skin/header.css                           (header.css)
 * skin/platform.css                         (platform.css)
   skin/touchcontrols.css                    (touchcontrols.css)
   skin/netError.css                         (netError.css)
@@ -83,16 +84,17 @@ chrome.jar:
   skin/images/aurora-lightbox-logo.png      (images/aurora-lightbox-logo.png)
   skin/images/aurora-lightbox-close.png     (images/aurora-lightbox-close.png)
 
 chrome.jar:
 % skin browser classic/1.0 %skin/gingerbread/ os=Android osversion=2.3 osversion=2.3.3 osversion=2.3.4 osversion=2.3.5 osversion=2.3.6 osversion=2.3.7
 % skin browser gingerbread/1.0 %skin/gingerbread/
   skin/gingerbread/aboutPage.css                        (aboutPage.css)
   skin/gingerbread/about.css                            (about.css)
+  skin/gingerbread/aboutAddons.css                      (aboutAddons.css)
   skin/gingerbread/aboutHome.css                        (aboutHome.css)
 * skin/gingerbread/browser.css                          (gingerbread/browser.css)
 * skin/gingerbread/content.css                          (gingerbread/content.css)
   skin/gingerbread/config.css                           (config.css)
   skin/gingerbread/header.css                           (header.css)
 * skin/gingerbread/platform.css                         (gingerbread/platform.css)
   skin/gingerbread/touchcontrols.css                    (touchcontrols.css)
   skin/gingerbread/netError.css                         (netError.css)
@@ -160,16 +162,17 @@ chrome.jar:
   skin/gingerbread/images/aurora-lightbox-logo.png      (images/aurora-lightbox-logo.png)
   skin/gingerbread/images/aurora-lightbox-close.png     (images/aurora-lightbox-close.png)
 
 chrome.jar:
 % skin browser classic/1.0 %skin/honeycomb/ os=Android osversion>=3.0
 % skin browser honeycomb/1.0 %skin/honeycomb/
   skin/honeycomb/aboutPage.css                        (aboutPage.css)
   skin/honeycomb/about.css                            (about.css)
+  skin/honeycomb/aboutAddons.css                      (aboutAddons.css)
   skin/honeycomb/aboutHome.css                        (aboutHome.css)
 * skin/honeycomb/browser.css                          (honeycomb/browser.css)
 * skin/honeycomb/content.css                          (content.css)
   skin/honeycomb/config.css                           (config.css)
   skin/honeycomb/header.css                           (header.css)
 * skin/honeycomb/platform.css                         (honeycomb/platform.css)
   skin/honeycomb/touchcontrols.css                    (touchcontrols.css)
   skin/honeycomb/netError.css                         (netError.css)