Merge m-c to m-i
authorPhil Ringnalda <philringnalda@gmail.com>
Sun, 07 Feb 2016 18:51:47 -0800
changeset 283425 2294e44c9057e20860cea871209912bef0722fdc
parent 283424 37d340a97af001402ea9f7e186460d914be768c7 (current diff)
parent 283412 a0d0344ed47a65f5c36802b61b25c0520cec421f (diff)
child 283426 45554120ac30245ec2d4f7a12fc203ed4ca38586
push id29982
push usercbook@mozilla.com
push dateMon, 08 Feb 2016 10:57:27 +0000
treeherdermozilla-central@ac338559876d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone47.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
Merge m-c to m-i
--- a/devtools/client/storage/test/browser.ini
+++ b/devtools/client/storage/test/browser.ini
@@ -9,9 +9,10 @@ support-files =
   storage-unsecured-iframe.html
   storage-updates.html
   head.js
 
 [browser_storage_basic.js]
 [browser_storage_dynamic_updates.js]
 [browser_storage_overflow.js]
 [browser_storage_sidebar.js]
+skip-if = (os == 'win' && os_version == '6.1' && e10s && !debug) # bug 1229272
 [browser_storage_values.js]
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -111,16 +111,19 @@ pref("network.http.spdy.push-allowance",
 pref("network.buffer.cache.count", 24);
 pref("network.buffer.cache.size",  16384);
 
 // predictive actions
 pref("network.predictor.enabled", true);
 pref("network.predictor.max-db-size", 2097152); // bytes
 pref("network.predictor.preserve", 50); // percentage of predictor data to keep when cleaning up
 
+// Use JS mDNS as a fallback
+pref("network.mdns.use_js_fallback", true);
+
 /* history max results display */
 pref("browser.display.history.maxresults", 100);
 
 /* How many times should have passed before the remote tabs list is refreshed */
 pref("browser.display.remotetabs.timeout", 10);
 
 /* session history */
 pref("browser.sessionhistory.max_total_viewers", 1);
@@ -971,8 +974,12 @@ pref("identity.fxaccounts.remote.webchan
 // The remote URL of the Firefox Account profile server.
 pref("identity.fxaccounts.remote.profile.uri", "https://profile.accounts.firefox.com/v1");
 
 // The remote URL of the Firefox Account oauth server.
 pref("identity.fxaccounts.remote.oauth.uri", "https://oauth.accounts.firefox.com/v1");
 
 // Token server used by Firefox Account-authenticated Sync.
 pref("identity.sync.tokenserver.uri", "https://token.services.mozilla.com/1.0/sync/1.5");
+
+// Enable Presentation API
+pref("dom.presentation.enabled", true);
+pref("dom.presentation.discovery.enabled", true);
--- a/mobile/android/chrome/content/CastingApps.js
+++ b/mobile/android/chrome/content/CastingApps.js
@@ -54,16 +54,74 @@ var mediaPlayerDevice = {
       uuid: display.uuid,
       manufacturer: display.manufacturer,
       modelName: display.modelName,
       mirror: display.mirror
     };
   }
 };
 
+var fxOSTVDevice = {
+  id: "app://fling-player.gaiamobile.org",
+  target: "app://fling-player.gaiamobile.org/index.html",
+  factory: function(aService) {
+    Cu.import("resource://gre/modules/PresentationApp.jsm");
+    let request = new window.PresentationRequest(this.target);
+    return new PresentationApp(aService, request);
+  },
+  init: function() {
+    Services.obs.addObserver(this, "presentation-device-change", false);
+    SimpleServiceDiscovery.addExternalDiscovery(this);
+  },
+  observe: function(subject, topic, data) {
+    let device = subject.QueryInterface(Ci.nsIPresentationDevice);
+    let service = this.toService(device);
+    switch (data) {
+      case "add":
+        SimpleServiceDiscovery.addService(service);
+        break;
+      case "update":
+        SimpleServiceDiscovery.updateService(service);
+        break;
+      case "remove":
+        if(SimpleServiceDiscovery.findServiceForID(device.id)) {
+          SimpleServiceDiscovery.removeService(device.id);
+        }
+        break;
+    }
+  },
+  toService: function(device) {
+    return {
+      location: device.id,
+      target: fxOSTVDevice.target,
+      friendlyName: device.name,
+      uuid: device.id,
+      manufacturer: "Firefox OS TV",
+      modelName: "Firefox OS TV",
+    };
+  },
+  startDiscovery: function() {
+    window.navigator.mozPresentationDeviceInfo.forceDiscovery();
+
+    // need to update the lastPing time for known device.
+    window.navigator.mozPresentationDeviceInfo.getAll()
+    .then(function(devices) {
+      for (let device of devices) {
+        let service = fxOSTVDevice.toService(device);
+        SimpleServiceDiscovery.addService(service);
+      }
+    });
+  },
+  stopDiscovery: function() {
+    // do nothing
+  },
+  types: ["video/mp4", "video/webm"],
+  extensions: ["mp4", "webm"],
+};
+
 var CastingApps = {
   _castMenuId: -1,
   mirrorStartMenuId: -1,
   mirrorStopMenuId: -1,
   _blocked: null,
   _bound: null,
   _interval: 120 * 1000, // 120 seconds
 
@@ -74,16 +132,20 @@ var CastingApps = {
 
     // Register targets
     SimpleServiceDiscovery.registerDevice(rokuDevice);
 
     // MediaPlayerDevice will notify us any time the native device list changes.
     mediaPlayerDevice.init();
     SimpleServiceDiscovery.registerDevice(mediaPlayerDevice);
 
+    // Presentation Device will notify us any time the available device list changes.
+    fxOSTVDevice.init();
+    SimpleServiceDiscovery.registerDevice(fxOSTVDevice);
+
     // Search for devices continuously
     SimpleServiceDiscovery.search(this._interval);
 
     this._castMenuId = NativeWindow.contextmenus.add(
       Strings.browser.GetStringFromName("contextmenu.sendToDevice"),
       this.filterCast,
       this.handleContextMenu.bind(this)
     );
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -586,16 +586,17 @@ var BrowserApp = {
     // Notify Java that Gecko has loaded.
     Messaging.sendRequest({ type: "Gecko:Ready" });
 
     this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() {
       BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false);
 
       InitLater(() => Cu.import("resource://gre/modules/NotificationDB.jsm"));
       InitLater(() => Cu.import("resource://gre/modules/Payment.jsm"));
+      InitLater(() => Cu.import("resource://gre/modules/PresentationDeviceInfoManager.jsm"));
 
       InitLater(() => Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""));
       InitLater(() => Messaging.sendRequest({ type: "Gecko:DelayedStartup" }));
 
       if (AppConstants.NIGHTLY_BUILD) {
         InitLater(() => WebcompatReporter.init());
       }
 
--- a/toolkit/components/passwordmgr/content/passwordManager.xul
+++ b/toolkit/components/passwordmgr/content/passwordManager.xul
@@ -18,17 +18,17 @@
 
   <script type="application/javascript" src="chrome://passwordmgr/content/passwordManagerCommon.js"/>
   <script type="application/javascript" src="chrome://passwordmgr/content/passwordManager.js"/>
 
   <stringbundle id="signonBundle"
                 src="chrome://passwordmgr/locale/passwordmgr.properties"/>
 
   <keyset>
-    <key keycode="VK_ESCAPE" oncommand="window.close();"/>
+    <key keycode="VK_ESCAPE" oncommand="escapeKeyHandler();"/>
     <key key="&windowClose.key;" modifiers="accel" oncommand="escapeKeyHandler();"/>
     <key key="&focusSearch1.key;" modifiers="accel" oncommand="FocusFilterBox();"/>
     <key key="&focusSearch2.key;" modifiers="accel" oncommand="FocusFilterBox();"/>
   </keyset>
 
   <popupset id="signonsTreeContextSet">
     <menupopup id="signonsTreeContextMenu"
                onpopupshowing="UpdateContextMenu()">
--- a/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js
+++ b/toolkit/components/passwordmgr/test/browser/browser_passwordmgr_editing.js
@@ -1,10 +1,9 @@
 const { ContentTaskUtils } = Cu.import("resource://testing-common/ContentTaskUtils.jsm", {});
-const TIME_INTERVAL = 500;
 const PWMGR_DLG = "chrome://passwordmgr/content/passwordManager.xul";
 
 var doc;
 var pwmgr;
 var pwmgrdlg;
 var signonsTree;
 
 function addLogin(site, username, password) {
@@ -50,17 +49,17 @@ function* editUsernamePromises(site, old
   let signonsIntro = doc.querySelector("#signonsIntro");
   EventUtils.sendMouseEvent({type: "click"}, signonsIntro, pwmgrdlg);
   yield ContentTaskUtils.waitForCondition(() => !signonsTree.getAttribute("editing"),
                                           "Waiting for editing to stop");
 
   is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login replaced");
   login = Services.logins.findLogins({}, site, "", "")[0];
   is(login.username, newUsername, "Correct username updated");
-  is(getUsername(0), newUsername, "Correct username shown");
+  is(getUsername(0), newUsername, "Correct username shown after the update");
 }
 
 function* editPasswordPromises(site, oldPassword, newPassword) {
   is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login found");
   let login = Services.logins.findLogins({}, site, "", "")[0];
   is(login.password, oldPassword, "Correct password saved");
   is(getPassword(0), oldPassword, "Correct password shown");
 
@@ -72,17 +71,17 @@ function* editPasswordPromises(site, old
   let signonsIntro = doc.querySelector("#signonsIntro");
   EventUtils.sendMouseEvent({type: "click"}, signonsIntro, pwmgrdlg);
   yield ContentTaskUtils.waitForCondition(() => !signonsTree.getAttribute("editing"),
                                           "Waiting for editing to stop");
 
   is(Services.logins.findLogins({}, site, "", "").length, 1, "Correct login replaced");
   login = Services.logins.findLogins({}, site, "", "")[0];
   is(login.password, newPassword, "Correct password updated");
-  is(getPassword(0), newPassword, "Correct password shown");
+  is(getPassword(0), newPassword, "Correct password shown after the update");
 }
 
 add_task(function* test_setup() {
   registerCleanupFunction(function() {
     Services.logins.removeAllLogins();
   });
 
   Services.logins.removeAllLogins();
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -58,16 +58,17 @@ EXTRA_JS_MODULES += [
     'PromiseUtils.jsm',
     'PropertyListUtils.jsm',
     'RemoteController.jsm',
     'RemoteFinder.jsm',
     'RemotePageManager.jsm',
     'RemoteSecurityUI.jsm',
     'RemoteWebProgress.jsm',
     'ResetProfile.jsm',
+    'secondscreen/PresentationApp.jsm',
     'secondscreen/RokuApp.jsm',
     'secondscreen/SimpleServiceDiscovery.jsm',
     'SelectContentHelper.jsm',
     'SelectParentHelper.jsm',
     'Services.jsm',
     'SessionRecorder.jsm',
     'sessionstore/FormData.jsm',
     'sessionstore/ScrollPosition.jsm',
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/secondscreen/PresentationApp.jsm
@@ -0,0 +1,190 @@
+// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["PresentationApp"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "sysInfo", () => {
+  return Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+});
+
+const DEBUG = false;
+
+const STATE_UNINIT = "uninitialized" // RemoteMedia status
+const STATE_STARTED = "started"; // RemoteMedia status
+const STATE_PAUSED = "paused"; // RemoteMedia status
+const STATE_SHUTDOWN = "shutdown"; // RemoteMedia status
+
+function debug(msg) {
+  Services.console.logStringMessage("PresentationApp: " + msg);
+}
+
+// PresentationApp is a wrapper for interacting with a Presentation Receiver Device.
+function PresentationApp(service, request) {
+  this.service = service;
+  this.request = request;
+}
+
+PresentationApp.prototype = {
+  start: function start(callback) {
+    this.request.startWithDevice(this.service.uuid)
+    .then((session) => {
+      this._session = session;
+      if (callback) {
+        callback(true);
+      }
+    }, () => {
+      if (callback) {
+        callback(false);
+      }
+    });
+  },
+
+  stop: function stop(callback) {
+    if (this._session && this._session.state === "connected") {
+      this._session.terminate();
+    }
+
+    delete this._session;
+
+    if (callback) {
+      callback(true);
+    }
+  },
+
+  remoteMedia: function remoteMedia(callback, listener) {
+    if (callback) {
+      if (!this._session) {
+        callback();
+        return;
+      }
+
+      callback(new RemoteMedia(this._session, listener));
+    }
+  }
+}
+
+/* RemoteMedia provides a wrapper for using Presentation API to control Firefox TV app.
+ * The server implementation must be built into the Firefox TV receiver app.
+ * see https://github.com/mozilla-b2g/gaia/tree/master/tv_apps/fling-player
+ */
+function RemoteMedia(session, listener) {
+  this._session = session ;
+  this._listener = listener;
+  this._status = STATE_UNINIT;
+
+  this._session.addEventListener("message", this);
+  this._session.addEventListener("statechange", this);
+
+  if (this._listener && "onRemoteMediaStart" in this._listener) {
+    Services.tm.mainThread.dispatch((function() {
+      this._listener.onRemoteMediaStart(this);
+    }).bind(this), Ci.nsIThread.DISPATCH_NORMAL);
+  }
+}
+
+RemoteMedia.prototype = {
+  _seq: 0,
+
+  handleEvent: function(e) {
+    switch (e.type) {
+      case "message":
+        this._onmessage(e);
+        break;
+      case "statechange":
+        this._onstatechange(e);
+        break;
+    }
+  },
+
+  _onmessage: function(e) {
+    DEBUG && debug("onmessage: " + e.data);
+    if (this.status === STATE_SHUTDOWN) {
+      return;
+    }
+
+    if (e.data.indexOf("stopped") > -1) {
+      if (this.status !== STATE_PAUSED) {
+        this._status = STATE_PAUSED;
+        if (this._listener && "onRemoteMediaStatus" in this._listener) {
+          this._listener.onRemoteMediaStatus(this);
+        }
+      }
+    } else if (e.data.indexOf("playing") > -1) {
+      if (this.status !== STATE_STARTED) {
+        this._status = STATE_STARTED;
+        if (this._listener && "onRemoteMediaStatus" in this._listener) {
+          this._listener.onRemoteMediaStatus(this);
+        }
+      }
+    }
+  },
+
+  _onstatechange: function(e) {
+    DEBUG && debug("onstatechange: " + this._session.state);
+    if (this._session.state !== "connected") {
+      this._status = STATE_SHUTDOWN;
+      if (this._listener && "onRemoteMediaStop" in this._listener) {
+        this._listener.onRemoteMediaStop(this);
+      }
+    }
+  },
+
+  _sendCommand: function(command, data) {
+    let msg = {
+      'type': command,
+      'seq': ++this._seq
+    };
+
+    if (data) {
+      for (var k in data) {
+        msg[k] = data[k];
+      }
+    }
+
+    let raw = JSON.stringify(msg);
+    DEBUG && debug("send command: " + raw);
+
+    this._session.send(raw);
+  },
+
+  shutdown: function shutdown() {
+    DEBUG && debug("RemoteMedia - shutdown");
+    this._sendCommand("close");
+  },
+
+  play: function play() {
+    DEBUG && debug("RemoteMedia - play");
+    this._sendCommand("play");
+  },
+
+  pause: function pause() {
+    DEBUG && debug("RemoteMedia - pause");
+    this._sendCommand("pause");
+  },
+
+  load: function load(data) {
+    DEBUG && debug("RemoteMedia - load: " + data);
+    this._sendCommand("load", { "url": data.source });
+
+    let deviceName;
+    if (Services.appinfo.widgetToolkit == "android") {
+      deviceName = sysInfo.get("device");
+    } else {
+      deviceName = sysInfo.get("host");
+    }
+    this._sendCommand("device-info", { "displayName": deviceName });
+  },
+
+  get status() {
+    return this._status;
+  }
+}
--- a/toolkit/modules/secondscreen/SimpleServiceDiscovery.jsm
+++ b/toolkit/modules/secondscreen/SimpleServiceDiscovery.jsm
@@ -52,16 +52,17 @@ var SimpleServiceDiscovery = {
 
   _devices: new Map(),
   _services: new Map(),
   _searchSocket: null,
   _searchInterval: 0,
   _searchTimestamp: 0,
   _searchTimeout: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
   _searchRepeat: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
+  _discoveryMethods: [],
 
   _forceTrailingSlash: function(aURL) {
     // Cleanup the URL to make it consistent across devices
     try {
       aURL = Services.io.newURI(aURL, null, null).spec;
     } catch(e) {}
     return aURL;
   },
@@ -136,16 +137,19 @@ var SimpleServiceDiscovery = {
     // Update the timestamp so we can use it to clean out stale services the
     // next time we search.
     this._searchTimestamp = Date.now();
 
     // Look for any fixed IP devices. Some routers might be configured to block
     // UDP broadcasts, so this is a way to skip discovery.
     this._searchFixedDevices();
 
+    // Look for any devices via registered external discovery mechanism.
+    this._startExternalDiscovery();
+
     // Perform a UDP broadcast to search for SSDP devices
     let socket = Cc["@mozilla.org/network/udp-socket;1"].createInstance(Ci.nsIUDPSocket);
     try {
       socket.init(SSDP_PORT, false, Services.scriptSecurityManager.getSystemPrincipal());
       socket.joinMulticast(SSDP_ADDRESS);
       socket.asyncListen(this);
     } catch (e) {
       // We were unable to create the broadcast socket. Just return, but don't
@@ -221,16 +225,18 @@ var SimpleServiceDiscovery = {
 
       // Clean out any stale services
       for (let [key, service] of this._services) {
         if (service.lastPing != this._searchTimestamp) {
           this.removeService(service.uuid);
         }
       }
     }
+
+    this._stopExternalDiscovery();
   },
 
   getSupportedExtensions: function() {
     let extensions = [];
     this.services.forEach(function(service) {
         extensions = extensions.concat(service.extensions);
       }, this);
     return extensions.filter(function(extension, pos) {
@@ -404,10 +410,26 @@ var SimpleServiceDiscovery = {
 
   updateService: function(service) {
     if (!this._addService(service)) {
       return;
     }
 
     // Make sure we remember this service is not stale
     this._services.get(service.uuid).lastPing = this._searchTimestamp;
-  }
+  },
+
+  addExternalDiscovery: function(discovery) {
+    this._discoveryMethods.push(discovery);
+  },
+
+  _startExternalDiscovery: function() {
+    for (let discovery of this._discoveryMethods) {
+      discovery.startDiscovery();
+    }
+  },
+
+  _stopExternalDiscovery: function() {
+    for (let discovery of this._discoveryMethods) {
+      discovery.stopDiscovery();
+    }
+  },
 }
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -3038,24 +3038,32 @@ this.AddonManager = {
   SCOPE_APPLICATION: 4,
   // Installed for all users of the computer.
   SCOPE_SYSTEM: 8,
   // Installed temporarily
   SCOPE_TEMPORARY: 16,
   // The combination of all scopes.
   SCOPE_ALL: 31,
 
-  // 1-15 are different built-in views for the add-on type
+  // Add-on type is expected to be displayed in the UI in a list.
   VIEW_TYPE_LIST: "list",
 
+  // Constants describing how add-on types behave.
+
+  // If no add-ons of a type are installed, then the category for that add-on
+  // type should be hidden in the UI.
   TYPE_UI_HIDE_EMPTY: 16,
   // Indicates that this add-on type supports the ask-to-activate state.
   // That is, add-ons of this type can be set to be optionally enabled
   // on a case-by-case basis.
   TYPE_SUPPORTS_ASK_TO_ACTIVATE: 32,
+  // The add-on type natively supports undo for restartless uninstalls.
+  // If this flag is not specified, the UI is expected to handle this via
+  // disabling the add-on, and performing the actual uninstall at a later time.
+  TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL: 64,
 
   // Constants for Addon.applyBackgroundUpdates.
   // Indicates that the Addon should not update automatically.
   AUTOUPDATE_DISABLE: 0,
   // Indicates that the Addon should update automatically only if
   // that's the global default.
   AUTOUPDATE_DEFAULT: 1,
   // Indicates that the Addon should update automatically.
--- a/toolkit/mozapps/extensions/content/extensions.js
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -1713,17 +1713,17 @@ function getAddonsAndInstalls(aType, aCa
 
 function doPendingUninstalls(aListBox) {
   // Uninstalling add-ons can mutate the list so find the add-ons first then
   // uninstall them
   var items = [];
   var listitem = aListBox.firstChild;
   while (listitem) {
     if (listitem.getAttribute("pending") == "uninstall" &&
-        !listitem.isPending("uninstall"))
+        !(listitem.opRequiresRestart("UNINSTALL")))
       items.push(listitem.mAddon);
     listitem = listitem.nextSibling;
   }
 
   for (let addon of items)
     addon.uninstall();
 }
 
--- a/toolkit/mozapps/extensions/content/extensions.xml
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -740,16 +740,26 @@
       <method name="isPending">
         <parameter name="aAction"/>
         <body><![CDATA[
           var action = AddonManager["PENDING_" + aAction.toUpperCase()];
           return !!(this.mAddon.pendingOperations & action);
         ]]></body>
       </method>
 
+      <method name="typeHasFlag">
+        <parameter name="aFlag"/>
+        <body><![CDATA[
+          let flag = AddonManager["TYPE_" + aFlag];
+          let type = AddonManager.addonTypes[this.mAddon.type];
+
+          return !!(type.flags & flag);
+        ]]></body>
+      </method>
+
       <method name="onUninstalled">
         <body><![CDATA[
           this.parentNode.removeChild(this);
         ]]></body>
       </method>
     </implementation>
   </binding>
 
@@ -1301,18 +1311,17 @@
             } else {
               this.removeAttribute("notification");
             }
           }
 
           this._preferencesBtn.hidden = (!this.mAddon.optionsURL) ||
                                         this.mAddon.optionsType == AddonManager.OPTIONS_TYPE_INLINE_INFO;
 
-          let addonType = AddonManager.addonTypes[this.mAddon.type];
-          if (addonType.flags & AddonManager.TYPE_SUPPORTS_ASK_TO_ACTIVATE) {
+          if (this.typeHasFlag("SUPPORTS_ASK_TO_ACTIVATE")) {
             this._enableBtn.disabled = true;
             this._disableBtn.disabled = true;
             this._askToActivateMenuitem.disabled = !this.hasPermission("ask_to_activate");
             this._alwaysActivateMenuitem.disabled = !this.hasPermission("enable");
             this._neverActivateMenuitem.disabled = !this.hasPermission("disable");
             if (!this.mAddon.isActive) {
               this._stateMenulist.selectedItem = this._neverActivateMenuitem;
             } else if (this.mAddon.userDisabled == AddonManager.STATE_ASK_TO_ACTIVATE) {
@@ -1520,29 +1529,31 @@
       <method name="undo">
         <body><![CDATA[
           gViewController.commands["cmd_cancelOperation"].doCommand(this.mAddon);
         ]]></body>
       </method>
 
       <method name="uninstall">
         <body><![CDATA[
-          // If uninstalling does not require a restart then just disable it
-          // and show the undo UI.
-          if (!this.opRequiresRestart("uninstall")) {
+          // If uninstalling does not require a restart and the type doesn't
+          // support undoing of restartless uninstalls, then we fake it by
+          // just disabling it it, and doing the real uninstall later.
+          if (!this.opRequiresRestart("uninstall") &&
+              !this.typeHasFlag("SUPPORTS_UNDO_RESTARTLESS_UNINSTALL")) {
             this.setAttribute("wasDisabled", this.mAddon.userDisabled);
 
             // We must set userDisabled to true first, this will call
             // _updateState which will clear any pending attribute set.
             this.mAddon.userDisabled = true;
 
             // This won't update any other add-on manager views (bug 582002)
             this.setAttribute("pending", "uninstall");
           } else {
-            this.mAddon.uninstall();
+            this.mAddon.uninstall(true);
           }
         ]]></body>
       </method>
 
       <method name="showPreferences">
         <body><![CDATA[
           gViewController.doCommand("cmd_showItemPreferences", this.mAddon);
         ]]></body>
@@ -1768,17 +1779,17 @@
     </content>
 
     <implementation>
       <constructor><![CDATA[
         this._notice.textContent = gStrings.ext.formatStringFromName("uninstallNotice",
                                                                      [this.mAddon.name],
                                                                      1);
 
-        if (!this.isPending("uninstall"))
+        if (!this.opRequiresRestart("uninstall"))
           this._restartBtn.setAttribute("hidden", true);
 
         gEventManager.registerAddonListener(this, this.mAddon.id);
       ]]></constructor>
 
       <destructor><![CDATA[
         gEventManager.unregisterAddonListener(this, this.mAddon.id);
       ]]></destructor>
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -3182,16 +3182,29 @@ this.XPIProvider = {
         if (isDir) {
           // Check if the directory contains an install manifest.
           let manifest = getManifestFileForDir(stageDirEntry);
 
           // If the install manifest doesn't exist uninstall this add-on in this
           // install location.
           if (!manifest) {
             logger.debug("Processing uninstall of " + id + " in " + location.name);
+
+            try {
+              let addonFile = location.getLocationForID(id);
+              let addonToUninstall = syncLoadManifestFromFile(addonFile, location);
+              if (addonToUninstall.bootstrap) {
+                this.callBootstrapMethod(addonToUninstall, addonToUninstall._sourceBundle,
+                                         "uninstall", BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+              }
+            }
+            catch (e) {
+              logger.warn("Failed to call uninstall for " + id, e);
+            }
+
             try {
               location.uninstallAddon(id);
               seenFiles.push(stageDirEntry.leafName);
             }
             catch (e) {
               logger.error("Failed to uninstall add-on " + id + " in " + location.name, e);
             }
             // The file check later will spot the removal and cleanup the database
@@ -4759,44 +4772,61 @@ this.XPIProvider = {
   },
 
   /**
    * Uninstalls an add-on, immediately if possible or marks it as pending
    * uninstall if not.
    *
    * @param  aAddon
    *         The DBAddonInternal to uninstall
+   * @param  aForcePending
+   *         Force this addon into the pending uninstall state, even if
+   *         it isn't marked as requiring a restart (used e.g. while the
+   *         add-on manager is open and offering an "undo" button)
    * @throws if the addon cannot be uninstalled because it is in an install
    *         location that does not allow it
    */
-  uninstallAddon: function(aAddon) {
+  uninstallAddon: function(aAddon, aForcePending) {
     if (!(aAddon.inDatabase))
       throw new Error("Cannot uninstall addon " + aAddon.id + " because it is not installed");
 
     if (aAddon._installLocation.locked)
       throw new Error("Cannot uninstall addon " + aAddon.id
           + " from locked install location " + aAddon._installLocation.name);
 
+    // Inactive add-ons don't require a restart to uninstall
+    let requiresRestart = this.uninstallRequiresRestart(aAddon);
+
+    // if makePending is true, we don't actually apply the uninstall,
+    // we just mark the addon as having a pending uninstall
+    let makePending = aForcePending || requiresRestart;
+
+    if (makePending && aAddon.pendingUninstall)
+      throw new Error("Add-on is already marked to be uninstalled");
+
     aAddon._hasResourceCache.clear();
 
     if (aAddon._updateCheck) {
       logger.debug("Cancel in-progress update check for " + aAddon.id);
       aAddon._updateCheck.cancel();
     }
 
-    // Inactive add-ons don't require a restart to uninstall
-    let requiresRestart = this.uninstallRequiresRestart(aAddon);
-
-    if (requiresRestart) {
-      // We create an empty directory in the staging directory to indicate that
-      // an uninstall is necessary on next startup.
-      let stage = aAddon._installLocation.getStagingDir();
-      stage.append(aAddon.id);
-      if (!stage.exists())
-        stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+    let wasPending = aAddon.pendingUninstall;
+
+    if (makePending) {
+      // We create an empty directory in the staging directory to indicate
+      // that an uninstall is necessary on next startup. Temporary add-ons are
+      // automatically uninstalled on shutdown anyway so there is no need to
+      // do this for them.
+      if (aAddon._installLocation.name != KEY_APP_TEMPORARY) {
+        let stage = aAddon._installLocation.getStagingDir();
+        stage.append(aAddon.id);
+        if (!stage.exists())
+          stage.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+      }
 
       XPIDatabase.setAddonProperties(aAddon, {
         pendingUninstall: true
       });
       Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
       let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
       if (xpiState) {
         xpiState.enabled = false;
@@ -4806,18 +4836,26 @@ this.XPIProvider = {
       }
     }
 
     // If the add-on is not visible then there is no need to notify listeners.
     if (!aAddon.visible)
       return;
 
     let wrapper = aAddon.wrapper;
-    AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
-                                           requiresRestart);
+
+    // If the add-on wasn't already pending uninstall then notify listeners.
+    if (!wasPending) {
+      // Passing makePending as the requiresRestart parameter is a little
+      // strange as in some cases this operation can complete without a restart
+      // so really this is now saying that the uninstall isn't going to happen
+      // immediately but will happen later.
+      AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
+                                             makePending);
+    }
 
     // Reveal the highest priority add-on with the same ID
     function revealAddon(aAddon) {
       XPIDatabase.makeAddonVisible(aAddon);
 
       let wrappedAddon = aAddon.wrapper;
       AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false);
 
@@ -4845,17 +4883,17 @@ this.XPIProvider = {
 
     function findAddonAndReveal(aId) {
       let [locationName, ] = XPIStates.findAddon(aId);
       if (locationName) {
         XPIDatabase.getAddonInLocation(aId, locationName, revealAddon);
       }
     }
 
-    if (!requiresRestart) {
+    if (!makePending) {
       if (aAddon.bootstrap) {
         if (aAddon.active) {
           this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
                                    BOOTSTRAP_REASONS.ADDON_UNINSTALL);
         }
 
         this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "uninstall",
                                  BOOTSTRAP_REASONS.ADDON_UNINSTALL);
@@ -4864,47 +4902,62 @@ this.XPIProvider = {
       }
       aAddon._installLocation.uninstallAddon(aAddon.id);
       XPIDatabase.removeAddonMetadata(aAddon);
       XPIStates.removeAddon(aAddon.location, aAddon.id);
       AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
 
       findAddonAndReveal(aAddon.id);
     }
+    else if (aAddon.bootstrap && aAddon.active && !this.disableRequiresRestart(aAddon)) {
+      this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
+                               BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+      this.unloadBootstrapScope(aAddon.id);
+      XPIDatabase.updateAddonActive(aAddon, false);
+    }
 
     // Notify any other providers that a new theme has been enabled
     if (aAddon.type == "theme" && aAddon.active)
       AddonManagerPrivate.notifyAddonChanged(null, aAddon.type, requiresRestart);
   },
 
   /**
    * Cancels the pending uninstall of an add-on.
    *
    * @param  aAddon
    *         The DBAddonInternal to cancel uninstall for
    */
   cancelUninstallAddon: function(aAddon) {
     if (!(aAddon.inDatabase))
       throw new Error("Can only cancel uninstall for installed addons.");
-
-    aAddon._installLocation.cleanStagingDir([aAddon.id]);
+    if (!aAddon.pendingUninstall)
+      throw new Error("Add-on is not marked to be uninstalled");
+
+    if (aAddon._installLocation.name != KEY_APP_TEMPORARY)
+      aAddon._installLocation.cleanStagingDir([aAddon.id]);
 
     XPIDatabase.setAddonProperties(aAddon, {
       pendingUninstall: false
     });
 
     if (!aAddon.visible)
       return;
 
     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
 
     // TODO hide hidden add-ons (bug 557710)
     let wrapper = aAddon.wrapper;
     AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
 
+    if (aAddon.bootstrap && !aAddon.disabled && !this.enableRequiresRestart(aAddon)) {
+      this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup",
+                               BOOTSTRAP_REASONS.ADDON_INSTALL);
+      XPIDatabase.updateAddonActive(aAddon, true);
+    }
+
     // Notify any other providers that this theme is now enabled again.
     if (aAddon.type == "theme" && aAddon.active)
       AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
   }
 };
 
 function getHashStringForCrypto(aCrypto) {
   // return the two-digit hexadecimal code for a byte
@@ -5855,31 +5908,35 @@ AddonInstall.prototype = {
 
     let stagingDir = this.installLocation.getStagingDir();
     let stagedAddon = stagingDir.clone();
 
     Task.spawn((function*() {
       let installedUnpacked = 0;
       yield this.installLocation.requestStagingDir();
 
+      // Remove any staged items for this add-on
+      stagedAddon.append(this.addon.id);
+      yield removeAsync(stagedAddon);
+      stagedAddon.leafName = this.addon.id + ".xpi";
+      yield removeAsync(stagedAddon);
+
       // First stage the file regardless of whether restarting is necessary
       if (this.addon.unpack || Preferences.get(PREF_XPI_UNPACK, false)) {
         logger.debug("Addon " + this.addon.id + " will be installed as " +
             "an unpacked directory");
-        stagedAddon.append(this.addon.id);
-        yield removeAsync(stagedAddon);
+        stagedAddon.leafName = this.addon.id;
         yield OS.File.makeDir(stagedAddon.path);
         yield ZipUtils.extractFilesAsync(this.file, stagedAddon);
         installedUnpacked = 1;
       }
       else {
         logger.debug("Addon " + this.addon.id + " will be installed as " +
             "a packed xpi");
-        stagedAddon.append(this.addon.id + ".xpi");
-        yield removeAsync(stagedAddon);
+        stagedAddon.leafName = this.addon.id + ".xpi";
         yield OS.File.copy(this.file.path, stagedAddon.path);
       }
 
       if (requiresRestart) {
         // Point the add-on to its extracted files as the xpi may get deleted
         this.addon._sourceBundle = stagedAddon;
 
         // Cache the AddonInternal as it may have updated compatibility info
@@ -7049,31 +7106,23 @@ AddonWrapper.prototype = {
     return (addon._installLocation.name == KEY_APP_SYSTEM_DEFAULTS ||
             addon._installLocation.name == KEY_APP_SYSTEM_ADDONS);
   },
 
   isCompatibleWith: function(aAppVersion, aPlatformVersion) {
     return addonFor(this).isCompatibleWith(aAppVersion, aPlatformVersion);
   },
 
-  uninstall: function() {
+  uninstall: function(alwaysAllowUndo) {
     let addon = addonFor(this);
-    if (!(addon.inDatabase))
-      throw new Error("Cannot uninstall an add-on that isn't installed");
-    if (addon.pendingUninstall)
-      throw new Error("Add-on is already marked to be uninstalled");
-    XPIProvider.uninstallAddon(addon);
+    XPIProvider.uninstallAddon(addon, alwaysAllowUndo);
   },
 
   cancelUninstall: function() {
     let addon = addonFor(this);
-    if (!(addon.inDatabase))
-      throw new Error("Cannot cancel uninstall for an add-on that isn't installed");
-    if (!addon.pendingUninstall)
-      throw new Error("Add-on is not marked to be uninstalled");
     XPIProvider.cancelUninstallAddon(addon);
   },
 
   findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) {
     // Short-circuit updates for experiments because updates are handled
     // through the Experiments Manager.
     if (this.type == "experiment") {
       AddonManagerPrivate.callNoUpdateListeners(this, aListener, aReason,
@@ -8132,36 +8181,37 @@ WinRegInstallLocation.prototype = {
   isLinkedAddon: function(aId) {
     return true;
   }
 };
 
 var addonTypes = [
   new AddonManagerPrivate.AddonType("extension", URI_EXTENSION_STRINGS,
                                     STRING_TYPE_NAME,
-                                    AddonManager.VIEW_TYPE_LIST, 4000),
+                                    AddonManager.VIEW_TYPE_LIST, 4000,
+                                    AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL),
   new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS,
                                     STRING_TYPE_NAME,
                                     AddonManager.VIEW_TYPE_LIST, 5000),
   new AddonManagerPrivate.AddonType("dictionary", URI_EXTENSION_STRINGS,
                                     STRING_TYPE_NAME,
                                     AddonManager.VIEW_TYPE_LIST, 7000,
-                                    AddonManager.TYPE_UI_HIDE_EMPTY),
+                                    AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL),
   new AddonManagerPrivate.AddonType("locale", URI_EXTENSION_STRINGS,
                                     STRING_TYPE_NAME,
                                     AddonManager.VIEW_TYPE_LIST, 8000,
-                                    AddonManager.TYPE_UI_HIDE_EMPTY),
+                                    AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL),
 ];
 
 // We only register experiments support if the application supports them.
 // Ideally, we would install an observer to watch the pref. Installing
 // an observer for this pref is not necessary here and may be buggy with
 // regards to registering this XPIProvider twice.
 if (Preferences.get("experiments.supported", false)) {
   addonTypes.push(
     new AddonManagerPrivate.AddonType("experiment",
                                       URI_EXTENSION_STRINGS,
                                       STRING_TYPE_NAME,
                                       AddonManager.VIEW_TYPE_LIST, 11000,
-                                      AddonManager.TYPE_UI_HIDE_EMPTY));
+                                      AddonManager.TYPE_UI_HIDE_EMPTY | AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL));
 }
 
 AddonManagerPrivate.registerProvider(XPIProvider, addonTypes);
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_undoincompatible/bootstrap.js
@@ -0,0 +1,1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_undoincompatible/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>incompatible@tests.mozilla.org</em:id>
+    <em:version>1.0</em:version>
+    <em:bootstrap>true</em:bootstrap>
+
+    <!-- Front End MetaData -->
+    <em:name>Incompatible Addon</em:name>
+    <em:description>I am incompatible</em:description>
+
+    <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+    <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+    <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>xpcshell@tests.mozilla.org</em:id>
+        <em:minVersion>2</em:minVersion>
+        <em:maxVersion>2</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_undouninstall1/bootstrap.js
@@ -0,0 +1,1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_undouninstall1/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>undouninstall1@tests.mozilla.org</em:id>
+    <em:version>1.0</em:version>
+    <em:bootstrap>true</em:bootstrap>
+
+    <!-- Front End MetaData -->
+    <em:name>Test Bootstrap 1</em:name>
+    <em:description>Test Description</em:description>
+
+    <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+    <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+    <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>xpcshell@tests.mozilla.org</em:id>
+        <em:minVersion>1</em:minVersion>
+        <em:maxVersion>1</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+  </Description>
+</RDF>
--- a/toolkit/mozapps/extensions/test/browser/browser_bug596336.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug596336.js
@@ -105,18 +105,17 @@ add_task(function*() {
   ok(!aAddon.userDisabled, "Add-on should not be disabled");
 
   let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
   EventUtils.synthesizeMouseAtCenter(get_node(item, "remove-btn"), { }, gManagerWindow);
 
   // Force XBL to apply
   item.clientTop;
 
-  ok(aAddon.userDisabled, "Add-on should be disabled");
-  ok(!aAddon.pendingUninstall, "Add-on should not be pending uninstall");
+  ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
   is_element_visible(get_class_node(item, "pending"), "Pending message should be visible");
 
   yield install_addon("browser_bug596336_2");
   [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
   yield check_addon(aAddon, "2.0");
   ok(!aAddon.userDisabled, "Add-on should not be disabled");
 
   aAddon.uninstall();
@@ -134,18 +133,17 @@ add_task(function*() {
   ok(aAddon.userDisabled, "Add-on should be disabled");
 
   let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
   EventUtils.synthesizeMouseAtCenter(get_node(item, "remove-btn"), { }, gManagerWindow);
 
   // Force XBL to apply
   item.clientTop;
 
-  ok(aAddon.userDisabled, "Add-on should be disabled");
-  ok(!aAddon.pendingUninstall, "Add-on should not be pending uninstall");
+  ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
   is_element_visible(get_class_node(item, "pending"), "Pending message should be visible");
 
   yield install_addon("browser_bug596336_2");
   [aAddon] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org"]);
   yield check_addon(aAddon, "2.0");
   ok(aAddon.userDisabled, "Add-on should be disabled");
 
   aAddon.uninstall();
--- a/toolkit/mozapps/extensions/test/browser/browser_uninstalling.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_uninstalling.js
@@ -161,17 +161,17 @@ add_test(function() {
 
       EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
 
       // Force XBL to apply
       item.clientTop;
 
       is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
 
-      ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+      ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
       ok(!aAddon.isActive, "Add-on should be inactive");
 
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
       isnot(button, null, "Should have a restart button");
       ok(button.hidden, "Restart button should be hidden");
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
       isnot(button, null, "Should have an undo button");
 
@@ -216,17 +216,17 @@ add_test(function() {
 
       EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
 
       // Force XBL to apply
       item.clientTop;
 
       is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
 
-      ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+      ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
       ok(!aAddon.isActive, "Add-on should be inactive");
 
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
       isnot(button, null, "Should have a restart button");
       ok(button.hidden, "Restart button should be hidden");
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
       isnot(button, null, "Should have an undo button");
 
@@ -337,17 +337,17 @@ add_test(function() {
 
       EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
 
       // Force XBL to apply
       item.clientTop;
 
       is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
 
-      ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+      ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
       ok(!aAddon.isActive, "Add-on should be inactive");
 
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
       isnot(button, null, "Should have a restart button");
       ok(button.hidden, "Restart button should be hidden");
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
       isnot(button, null, "Should have an undo button");
 
@@ -400,17 +400,17 @@ add_test(function() {
 
       EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
 
       // Force XBL to apply
       item.clientTop;
 
       is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
 
-      ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+      ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
       ok(!aAddon.isActive, "Add-on should be inactive");
 
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
       isnot(button, null, "Should have a restart button");
       ok(button.hidden, "Restart button should be hidden");
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
       isnot(button, null, "Should have an undo button");
 
@@ -526,17 +526,17 @@ add_test(function() {
 
         wait_for_view_load(gManagerWindow, function() {
           is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
 
           var item = get_item_in_list(ID, list);
           isnot(item, null, "Should have found the add-on in the list");
           is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
 
-          ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+          ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
           ok(!aAddon.isActive, "Add-on should be inactive");
 
           // Force XBL to apply
           item.clientTop;
 
           var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
           isnot(button, null, "Should have a restart button");
           ok(button.hidden, "Restart button should be hidden");
@@ -593,17 +593,17 @@ add_test(function() {
 
         wait_for_view_load(gManagerWindow, function() {
           is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
 
           var item = get_item_in_list(ID, list);
           isnot(item, null, "Should have found the add-on in the list");
           is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
 
-          ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+          ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
           ok(!aAddon.isActive, "Add-on should be inactive");
 
           // Force XBL to apply
           item.clientTop;
 
           var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
           isnot(button, null, "Should have a restart button");
           ok(button.hidden, "Restart button should be hidden");
@@ -803,18 +803,17 @@ add_test(function() {
       ok(!button.disabled, "Button should not be disabled");
 
       EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
 
       // Force XBL to apply
       item.clientTop;
 
       is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
-
-      ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+      ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
       ok(!aAddon.isActive, "Add-on should be inactive");
 
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
       isnot(button, null, "Should have a restart button");
       ok(button.hidden, "Restart button should be hidden");
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
       isnot(button, null, "Should have an undo button");
 
@@ -883,17 +882,17 @@ add_test(function() {
 
       EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
 
       // Force XBL to apply
       item.clientTop;
 
       is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
 
-      ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+      ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
       ok(!aAddon.isActive, "Add-on should be inactive");
 
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
       isnot(button, null, "Should have a restart button");
       ok(button.hidden, "Restart button should be hidden");
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
       isnot(button, null, "Should have an undo button");
 
@@ -959,17 +958,17 @@ add_test(function() {
 
       EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
 
       // Force XBL to apply
       item.clientTop;
 
       is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
 
-      ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+      ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
       ok(!aAddon.isActive, "Add-on should be inactive");
 
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
       isnot(button, null, "Should have a restart button");
       ok(button.hidden, "Restart button should be hidden");
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
       isnot(button, null, "Should have an undo button");
 
@@ -1041,17 +1040,17 @@ add_test(function() {
 
       EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
 
       // Force XBL to apply
       item.clientTop;
 
       is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
 
-      ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+      ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
       ok(!aAddon.isActive, "Add-on should be inactive");
 
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
       isnot(button, null, "Should have a restart button");
       ok(button.hidden, "Restart button should be hidden");
       button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
       isnot(button, null, "Should have an undo button");
 
--- a/toolkit/mozapps/extensions/test/browser/head.js
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -677,17 +677,18 @@ function MockProvider(aUseAsyncCallbacks
   this.installs = [];
   this.callbackTimers = [];
   this.timerLocations = new Map();
   this.useAsyncCallbacks = (aUseAsyncCallbacks === undefined) ? true : aUseAsyncCallbacks;
   this.types = (aTypes === undefined) ? [{
     id: "extension",
     name: "Extensions",
     uiPriority: 4000,
-    flags: AddonManager.TYPE_UI_VIEW_LIST
+    flags: AddonManager.TYPE_UI_VIEW_LIST |
+           AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL,
   }] : aTypes;
 
   var self = this;
   registerCleanupFunction(function() {
     if (self.started)
       self.unregister();
   });
 
@@ -1129,17 +1130,18 @@ function MockAddon(aId, aName, aType, aO
     (AddonManager.OP_NEEDS_RESTART_INSTALL |
      AddonManager.OP_NEEDS_RESTART_UNINSTALL |
      AddonManager.OP_NEEDS_RESTART_ENABLE |
      AddonManager.OP_NEEDS_RESTART_DISABLE);
 }
 
 MockAddon.prototype = {
   get shouldBeActive() {
-    return !this.appDisabled && !this._userDisabled;
+    return !this.appDisabled && !this._userDisabled &&
+           !(this.pendingOperations & AddonManager.PENDING_UNINSTALL);
   },
 
   get appDisabled() {
     return this._appDisabled;
   },
 
   set appDisabled(val) {
     if (val == this._appDisabled)
@@ -1201,34 +1203,38 @@ MockAddon.prototype = {
   isCompatibleWith: function(aAppVersion, aPlatformVersion) {
     return true;
   },
 
   findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) {
     // Tests can implement this if they need to
   },
 
-  uninstall: function() {
-    if (this.pendingOperations & AddonManager.PENDING_UNINSTALL)
+  uninstall: function(aAlwaysAllowUndo = false) {
+    if ((this.operationsRequiringRestart & AddonManager.OP_NEED_RESTART_UNINSTALL)
+        && this.pendingOperations & AddonManager.PENDING_UNINSTALL)
       throw Components.Exception("Add-on is already pending uninstall");
 
-    var needsRestart = !!(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL);
+    var needsRestart = aAlwaysAllowUndo || !!(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL);
     this.pendingOperations |= AddonManager.PENDING_UNINSTALL;
     AddonManagerPrivate.callAddonListeners("onUninstalling", this, needsRestart);
     if (!needsRestart) {
       this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
       this._provider.removeAddon(this);
+    } else if (!(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE)) {
+      this.isActive = false;
     }
   },
 
   cancelUninstall: function() {
     if (!(this.pendingOperations & AddonManager.PENDING_UNINSTALL))
       throw Components.Exception("Add-on is not pending uninstall");
 
     this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
+    this.isActive = this.shouldBeActive;
     AddonManagerPrivate.callAddonListeners("onOperationCancelled", this);
   },
 
   markAsSeen: function() {
     this.seen = true;
   },
 
   _updateActiveState: function(currentActive, newActive) {
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -2010,28 +2010,41 @@ function callback_soon(aFunction) {
  * A promise-based variant of AddonManager.getAddonsByIDs.
  *
  * @param {array} list As the first argument of AddonManager.getAddonsByIDs
  * @return {promise}
  * @resolve {array} The list of add-ons sent by AddonManaget.getAddonsByIDs to
  * its callback.
  */
 function promiseAddonsByIDs(list) {
-  return new Promise((resolve, reject) => AddonManager.getAddonsByIDs(list, resolve));
+  return new Promise(resolve => AddonManager.getAddonsByIDs(list, resolve));
 }
 
 /**
  * A promise-based variant of AddonManager.getAddonByID.
  *
  * @param {string} aId The ID of the add-on.
  * @return {promise}
  * @resolve {AddonWrapper} The corresponding add-on, or null.
  */
 function promiseAddonByID(aId) {
-  return new Promise((resolve, reject) => AddonManager.getAddonByID(aId, resolve));
+  return new Promise(resolve => AddonManager.getAddonByID(aId, resolve));
+}
+
+/**
+ * A promise-based variant of AddonManager.getAddonsWithOperationsByTypes
+ *
+ * @param {array} aTypes The first argument to
+ *                       AddonManager.getAddonsWithOperationsByTypes
+ * @return {promise}
+ * @resolve {array} The list of add-ons sent by
+ *                  AddonManaget.getAddonsWithOperationsByTypes to its callback.
+ */
+function promiseAddonsWithOperationsByTypes(aTypes) {
+  return new Promise(resolve => AddonManager.getAddonsWithOperationsByTypes(aTypes, resolve));
 }
 
 /**
  * Returns a promise that will be resolved when an add-on update check is
  * complete. The value resolved will be an AddonInstall if a new version was
  * found.
  */
 function promiseFindAddonUpdates(addon, reason = AddonManager.UPDATE_WHEN_PERIODIC_UPDATE) {
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_undothemeuninstall.js
@@ -0,0 +1,421 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that forcing undo for uninstall works for themes
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm");
+
+const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
+
+var defaultTheme = {
+  id: "default@tests.mozilla.org",
+  version: "1.0",
+  name: "Test 1",
+  internalName: "classic/1.0",
+  targetApplications: [{
+    id: "xpcshell@tests.mozilla.org",
+    minVersion: "1",
+    maxVersion: "1"
+  }]
+};
+
+var theme1 = {
+  id: "theme1@tests.mozilla.org",
+  version: "1.0",
+  name: "Test 1",
+  internalName: "theme1",
+  targetApplications: [{
+    id: "xpcshell@tests.mozilla.org",
+    minVersion: "1",
+    maxVersion: "1"
+  }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function dummyLWTheme(id) {
+  return {
+    id: id || Math.random().toString(),
+    name: Math.random().toString(),
+    headerURL: "http://lwttest.invalid/a.png",
+    footerURL: "http://lwttest.invalid/b.png",
+    textcolor: Math.random().toString(),
+    accentcolor: Math.random().toString()
+  };
+}
+
+// Sets up the profile by installing an add-on.
+function run_test() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+  startupManager();
+  do_register_cleanup(promiseShutdownManager);
+
+  run_next_test();
+}
+
+add_task(function* checkDefault() {
+  writeInstallRDFForExtension(defaultTheme, profileDir);
+  yield promiseRestartManager();
+
+  let d = yield promiseAddonByID("default@tests.mozilla.org");
+
+  do_check_neq(d, null);
+  do_check_true(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+});
+
+// Tests that uninstalling an enabled theme offers the option to undo
+add_task(function* uninstallEnabledOffersUndo() {
+  writeInstallRDFForExtension(theme1, profileDir);
+
+  yield promiseRestartManager();
+
+  let t1 = yield promiseAddonByID("theme1@tests.mozilla.org");
+
+  do_check_neq(t1, null);
+  do_check_true(t1.userDisabled);
+
+  t1.userDisabled = false;
+
+  yield promiseRestartManager();
+
+  let d = null;
+  [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+                                        "default@tests.mozilla.org"]);
+  do_check_neq(d, null);
+  do_check_false(d.isActive);
+  do_check_true(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(t1, null);
+  do_check_true(t1.isActive);
+  do_check_false(t1.userDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme1");
+
+  prepare_test({
+    "default@tests.mozilla.org": [
+      "onEnabling"
+    ],
+    "theme1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  t1.uninstall(true);
+  ensure_test_completed();
+
+  do_check_neq(d, null);
+  do_check_false(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_ENABLE);
+
+  do_check_true(t1.isActive);
+  do_check_false(t1.userDisabled);
+  do_check_true(hasFlag(t1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme1");
+
+  yield promiseRestartManager();
+
+  [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+                                        "default@tests.mozilla.org"]);
+  do_check_neq(d, null);
+  do_check_true(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_eq(t1, null);
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+});
+
+//Tests that uninstalling an enabled theme can be undone
+add_task(function* canUndoUninstallEnabled() {
+  writeInstallRDFForExtension(theme1, profileDir);
+
+  yield promiseRestartManager();
+
+  let t1 = yield promiseAddonByID("theme1@tests.mozilla.org");
+
+  do_check_neq(t1, null);
+  do_check_true(t1.userDisabled);
+
+  t1.userDisabled = false;
+
+  yield promiseRestartManager();
+
+  let d = null;
+  [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+                                        "default@tests.mozilla.org"]);
+
+  do_check_neq(d, null);
+  do_check_false(d.isActive);
+  do_check_true(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(t1, null);
+  do_check_true(t1.isActive);
+  do_check_false(t1.userDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme1");
+
+  prepare_test({
+    "default@tests.mozilla.org": [
+      "onEnabling"
+    ],
+    "theme1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  t1.uninstall(true);
+  ensure_test_completed();
+
+  do_check_neq(d, null);
+  do_check_false(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_ENABLE);
+
+  do_check_true(t1.isActive);
+  do_check_false(t1.userDisabled);
+  do_check_true(hasFlag(t1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme1");
+
+  prepare_test({
+    "default@tests.mozilla.org": [
+      "onOperationCancelled"
+    ],
+    "theme1@tests.mozilla.org": [
+      "onOperationCancelled"
+    ]
+  });
+  t1.cancelUninstall();
+  ensure_test_completed();
+
+  do_check_neq(d, null);
+  do_check_false(d.isActive);
+  do_check_true(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(t1, null);
+  do_check_true(t1.isActive);
+  do_check_false(t1.userDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+  yield promiseRestartManager();
+
+  [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+                                        "default@tests.mozilla.org"]);
+
+  do_check_neq(d, null);
+  do_check_false(d.isActive);
+  do_check_true(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(t1, null);
+  do_check_true(t1.isActive);
+  do_check_false(t1.userDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme1");
+
+  t1.uninstall();
+  yield promiseRestartManager();
+});
+
+//Tests that uninstalling a disabled theme offers the option to undo
+add_task(function* uninstallDisabledOffersUndo() {
+  writeInstallRDFForExtension(theme1, profileDir);
+
+  yield promiseRestartManager();
+
+  let [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+                                            "default@tests.mozilla.org"]);
+
+  do_check_neq(d, null);
+  do_check_true(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(t1, null);
+  do_check_false(t1.isActive);
+  do_check_true(t1.userDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+  prepare_test({
+    "theme1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  t1.uninstall(true);
+  ensure_test_completed();
+
+  do_check_neq(d, null);
+  do_check_true(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_false(t1.isActive);
+  do_check_true(t1.userDisabled);
+  do_check_true(hasFlag(t1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+  yield promiseRestartManager();
+
+  [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+                                        "default@tests.mozilla.org"]);
+
+  do_check_neq(d, null);
+  do_check_true(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_eq(t1, null);
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+});
+
+//Tests that uninstalling a disabled theme can be undone
+add_task(function* canUndoUninstallDisabled() {
+  writeInstallRDFForExtension(theme1, profileDir);
+
+  yield promiseRestartManager();
+
+  let [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+                                            "default@tests.mozilla.org"]);
+
+  do_check_neq(d, null);
+  do_check_true(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(t1, null);
+  do_check_false(t1.isActive);
+  do_check_true(t1.userDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+  prepare_test({
+    "theme1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  t1.uninstall(true);
+  ensure_test_completed();
+
+  do_check_neq(d, null);
+  do_check_true(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_false(t1.isActive);
+  do_check_true(t1.userDisabled);
+  do_check_true(hasFlag(t1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+  prepare_test({
+    "theme1@tests.mozilla.org": [
+      "onOperationCancelled"
+    ]
+  });
+  t1.cancelUninstall();
+  ensure_test_completed();
+
+  do_check_neq(d, null);
+  do_check_true(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(t1, null);
+  do_check_false(t1.isActive);
+  do_check_true(t1.userDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+  yield promiseRestartManager();
+
+  [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+                                        "default@tests.mozilla.org"]);
+
+  do_check_neq(d, null);
+  do_check_true(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(t1, null);
+  do_check_false(t1.isActive);
+  do_check_true(t1.userDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+  t1.uninstall();
+  yield promiseRestartManager();
+});
+
+//Tests that uninstalling an enabled lightweight theme offers the option to undo
+add_task(function* uninstallLWTOffersUndo() {
+  // skipped since lightweight themes don't support undoable uninstall yet
+  return;
+  LightweightThemeManager.currentTheme = dummyLWTheme("theme1");
+
+  let [ t1, d ] = yield promiseAddonsByIDs(["theme1@personas.mozilla.org",
+                                            "default@tests.mozilla.org"]);
+
+  do_check_neq(d, null);
+  do_check_false(d.isActive);
+  do_check_true(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_neq(t1, null);
+  do_check_true(t1.isActive);
+  do_check_false(t1.userDisabled);
+  do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+  prepare_test({
+    "default@tests.mozilla.org": [
+      "onEnabling"
+    ],
+    "theme1@personas.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  t1.uninstall(true);
+  ensure_test_completed();
+
+  do_check_neq(d, null);
+  do_check_false(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_ENABLE);
+
+  do_check_true(t1.isActive);
+  do_check_false(t1.userDisabled);
+  do_check_true(hasFlag(t1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+  yield promiseRestartManager();
+
+  [ t1, d ] = yield promiseAddonsByIDs(["theme1@personas.mozilla.org",
+                                        "default@tests.mozilla.org"]);
+
+  do_check_neq(d, null);
+  do_check_true(d.isActive);
+  do_check_false(d.userDisabled);
+  do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+  do_check_eq(t1, null);
+
+  do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js
@@ -0,0 +1,792 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that forcing undo for uninstall works
+
+const APP_STARTUP                     = 1;
+const APP_SHUTDOWN                    = 2;
+const ADDON_ENABLE                    = 3;
+const ADDON_DISABLE                   = 4;
+const ADDON_INSTALL                   = 5;
+const ADDON_UNINSTALL                 = 6;
+const ADDON_UPGRADE                   = 7;
+const ADDON_DOWNGRADE                 = 8;
+
+const ID = "undouninstall1@tests.mozilla.org";
+const INCOMPAT_ID = "incompatible@tests.mozilla.org";
+
+var addon1 = {
+  id: "addon1@tests.mozilla.org",
+  version: "1.0",
+  name: "Test 1",
+  targetApplications: [{
+    id: "xpcshell@tests.mozilla.org",
+    minVersion: "1",
+    maxVersion: "1"
+  }]
+};
+
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+BootstrapMonitor.init();
+
+function getStartupReason(id) {
+  let info = BootstrapMonitor.started.get(id);
+  return info ? info.reason : undefined;
+}
+
+function getShutdownReason(id) {
+  let info = BootstrapMonitor.stopped.get(id);
+  return info ? info.reason : undefined;
+}
+
+function getInstallReason(id) {
+  let info = BootstrapMonitor.installed.get(id);
+  return info ? info.reason : undefined;
+}
+
+function getUninstallReason(id) {
+  let info = BootstrapMonitor.uninstalled.get(id);
+  return info ? info.reason : undefined;
+}
+
+function getStartupOldVersion(id) {
+  let info = BootstrapMonitor.started.get(id);
+  return info ? info.data.oldVersion : undefined;
+}
+
+function getShutdownNewVersion(id) {
+  let info = BootstrapMonitor.stopped.get(id);
+  return info ? info.data.newVersion : undefined;
+}
+
+function getInstallOldVersion(id) {
+  let info = BootstrapMonitor.installed.get(id);
+  return info ? info.data.oldVersion : undefined;
+}
+
+function getUninstallNewVersion(id) {
+  let info = BootstrapMonitor.uninstalled.get(id);
+  return info ? info.data.newVersion : undefined;
+}
+
+// Sets up the profile by installing an add-on.
+function run_test() {
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+  startupManager();
+  do_register_cleanup(promiseShutdownManager);
+
+  run_next_test();
+}
+
+add_task(function* installAddon() {
+  let olda1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+  do_check_eq(olda1, null);
+
+  writeInstallRDFForExtension(addon1, profileDir);
+  yield promiseRestartManager();
+
+  let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+  do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+  do_check_eq(a1.pendingOperations, 0);
+  do_check_in_crash_annotation(addon1.id, addon1.version);
+});
+
+// Uninstalling an add-on should work.
+add_task(function* uninstallAddon() {
+  prepare_test({
+    "addon1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+
+  let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+  do_check_eq(a1.pendingOperations, 0);
+  do_check_neq(a1.operationsRequiringRestart &
+               AddonManager.OP_NEEDS_RESTART_UNINSTALL, 0);
+  a1.uninstall(true);
+  do_check_true(hasFlag(a1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+  do_check_in_crash_annotation(addon1.id, addon1.version);
+
+  ensure_test_completed();
+
+  let list = yield promiseAddonsWithOperationsByTypes(null);
+
+  do_check_eq(list.length, 1);
+  do_check_eq(list[0].id, "addon1@tests.mozilla.org");
+
+  yield promiseRestartManager();
+
+  a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+  do_check_eq(a1, null);
+  do_check_false(isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org"));
+  do_check_not_in_crash_annotation(addon1.id, addon1.version);
+
+  var dest = profileDir.clone();
+  dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
+  do_check_false(dest.exists());
+  writeInstallRDFForExtension(addon1, profileDir);
+  yield promiseRestartManager();
+});
+
+// Cancelling the uninstall should send onOperationCancelled
+add_task(function* cancelUninstall() {
+  prepare_test({
+    "addon1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+
+  let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+  do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+  do_check_eq(a1.pendingOperations, 0);
+  a1.uninstall(true);
+  do_check_true(hasFlag(a1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+  ensure_test_completed();
+
+  prepare_test({
+    "addon1@tests.mozilla.org": [
+      "onOperationCancelled"
+    ]
+  });
+  a1.cancelUninstall();
+  do_check_eq(a1.pendingOperations, 0);
+
+  ensure_test_completed();
+  yield promiseRestartManager();
+
+  a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+  do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+});
+
+// Uninstalling an item pending disable should still require a restart
+add_task(function* pendingDisableRequestRestart() {
+  let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+  prepare_test({
+    "addon1@tests.mozilla.org": [
+      "onDisabling"
+    ]
+  });
+  a1.userDisabled = true;
+  ensure_test_completed();
+
+  do_check_true(hasFlag(AddonManager.PENDING_DISABLE, a1.pendingOperations));
+  do_check_true(a1.isActive);
+
+  prepare_test({
+    "addon1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  a1.uninstall(true);
+
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+
+  prepare_test({
+    "addon1@tests.mozilla.org": [
+      "onOperationCancelled"
+    ]
+  });
+  a1.cancelUninstall();
+  ensure_test_completed();
+  do_check_true(hasFlag(AddonManager.PENDING_DISABLE, a1.pendingOperations));
+
+  yield promiseRestartManager();
+});
+
+// Test that uninstalling an inactive item should still allow cancelling
+add_task(function* uninstallInactiveIsCancellable() {
+  let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  do_check_false(a1.isActive);
+  do_check_true(a1.userDisabled);
+  do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+
+  prepare_test({
+    "addon1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  a1.uninstall(true);
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+
+  prepare_test({
+    "addon1@tests.mozilla.org": [
+      "onOperationCancelled"
+    ]
+  });
+  a1.cancelUninstall();
+  ensure_test_completed();
+
+  yield promiseRestartManager();
+});
+
+//Test that an inactive item can be uninstalled
+add_task(function* uninstallInactive() {
+  let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  do_check_false(a1.isActive);
+  do_check_true(a1.userDisabled);
+  do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+
+  prepare_test({
+    "addon1@tests.mozilla.org": [
+      [ "onUninstalling", false ],
+      "onUninstalled"
+    ]
+  });
+  a1.uninstall();
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+  do_check_eq(a1, null);
+});
+
+// Tests that an enabled restartless add-on can be uninstalled and goes away
+// when the uninstall is committed
+add_task(function* uninstallRestartless() {
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      ["onInstalling", false],
+      "onInstalled"
+    ]
+  }, [
+    "onNewInstall",
+    "onInstallStarted",
+    "onInstallEnded"
+  ]);
+  yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+  ensure_test_completed();
+
+  let a1 = yield promiseAddonByID(ID);
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+  do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  a1.uninstall(true);
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID(ID);
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonInstalled(ID);
+  BootstrapMonitor.checkAddonNotStarted(ID);
+  do_check_eq(getShutdownReason(ID), ADDON_UNINSTALL);
+  do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+  do_check_false(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  // complete the uinstall
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      "onUninstalled"
+    ]
+  });
+  a1.uninstall();
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID(ID);
+
+  do_check_eq(a1, null);
+  BootstrapMonitor.checkAddonNotStarted(ID);
+});
+
+//Tests that an enabled restartless add-on can be uninstalled and then cancelled
+add_task(function* cancelUninstallOfRestartless() {
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      ["onInstalling", false],
+      "onInstalled"
+    ]
+  }, [
+    "onNewInstall",
+    "onInstallStarted",
+    "onInstallEnded"
+  ]);
+  yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID(ID);
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+  do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  a1.uninstall(true);
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonInstalled(ID);
+  BootstrapMonitor.checkAddonNotStarted(ID);
+  do_check_eq(getShutdownReason(ID), ADDON_UNINSTALL);
+  do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+  do_check_false(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      "onOperationCancelled"
+    ]
+  });
+  a1.cancelUninstall();
+  ensure_test_completed();
+
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  shutdownManager();
+
+  do_check_eq(getShutdownReason(ID), APP_SHUTDOWN);
+  do_check_eq(getShutdownNewVersion(ID), undefined);
+
+  startupManager(false);
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  do_check_eq(getStartupReason(ID), APP_STARTUP);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  a1.uninstall();
+});
+
+// Tests that reinstalling an enabled restartless add-on waiting to be
+// uninstalled aborts the uninstall and leaves the add-on enabled
+add_task(function* reinstallAddonAwaitingUninstall() {
+  yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+
+  let a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+  do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  a1.uninstall(true);
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonInstalled(ID);
+  BootstrapMonitor.checkAddonNotStarted(ID);
+  do_check_eq(getShutdownReason(ID), ADDON_UNINSTALL);
+  do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+  do_check_false(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      ["onInstalling", false],
+      "onInstalled"
+    ]
+  }, [
+    "onNewInstall",
+    "onInstallStarted",
+    "onInstallEnded"
+  ]);
+
+  yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  ensure_test_completed();
+
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  do_check_eq(getUninstallReason(ID), ADDON_DOWNGRADE);
+  do_check_eq(getInstallReason(ID), ADDON_DOWNGRADE);
+  do_check_eq(getStartupReason(ID), ADDON_DOWNGRADE);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  shutdownManager();
+
+  do_check_eq(getShutdownReason(ID), APP_SHUTDOWN);
+
+  startupManager(false);
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  do_check_eq(getStartupReason(ID), APP_STARTUP);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  a1.uninstall();
+});
+
+// Tests that a disabled restartless add-on can be uninstalled and goes away
+// when the uninstall is committed
+add_task(function* uninstallDisabledRestartless() {
+  yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+
+  let a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+  do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  a1.userDisabled = true;
+  BootstrapMonitor.checkAddonNotStarted(ID);
+  do_check_eq(getShutdownReason(ID), ADDON_DISABLE);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(a1.isActive);
+  do_check_true(a1.userDisabled);
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  a1.uninstall(true);
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonNotStarted(ID);
+  do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+  do_check_false(a1.isActive);
+  do_check_true(a1.userDisabled);
+
+  // commit the uninstall
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      "onUninstalled"
+    ]
+  });
+  a1.uninstall();
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_eq(a1, null);
+  BootstrapMonitor.checkAddonNotStarted(ID);
+  BootstrapMonitor.checkAddonNotInstalled(ID);
+  do_check_eq(getUninstallReason(ID), ADDON_UNINSTALL);
+});
+
+//Tests that a disabled restartless add-on can be uninstalled and then cancelled
+add_task(function* cancelUninstallDisabledRestartless() {
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      ["onInstalling", false],
+      "onInstalled"
+    ]
+  }, [
+    "onNewInstall",
+    "onInstallStarted",
+    "onInstallEnded"
+  ]);
+  yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+  ensure_test_completed();
+
+  let a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+  do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      ["onDisabling", false],
+      "onDisabled"
+    ]
+  });
+  a1.userDisabled = true;
+  ensure_test_completed();
+
+  BootstrapMonitor.checkAddonNotStarted(ID);
+  do_check_eq(getShutdownReason(ID), ADDON_DISABLE);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(a1.isActive);
+  do_check_true(a1.userDisabled);
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  a1.uninstall(true);
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonNotStarted(ID);
+  BootstrapMonitor.checkAddonInstalled(ID);
+  do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+  do_check_false(a1.isActive);
+  do_check_true(a1.userDisabled);
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      "onOperationCancelled"
+    ]
+  });
+  a1.cancelUninstall();
+  ensure_test_completed();
+
+  BootstrapMonitor.checkAddonNotStarted(ID);
+  BootstrapMonitor.checkAddonInstalled(ID);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(a1.isActive);
+  do_check_true(a1.userDisabled);
+
+  yield promiseRestartManager();
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonNotStarted(ID);
+  BootstrapMonitor.checkAddonInstalled(ID);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(a1.isActive);
+  do_check_true(a1.userDisabled);
+
+  a1.uninstall();
+});
+
+//Tests that reinstalling a disabled restartless add-on waiting to be
+//uninstalled aborts the uninstall and leaves the add-on disabled
+add_task(function* reinstallDisabledAddonAwaitingUninstall() {
+  yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+
+  let a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+  do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  a1.userDisabled = true;
+  BootstrapMonitor.checkAddonNotStarted(ID);
+  do_check_eq(getShutdownReason(ID), ADDON_DISABLE);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(a1.isActive);
+  do_check_true(a1.userDisabled);
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  a1.uninstall(true);
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonNotStarted(ID);
+  do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+  do_check_false(a1.isActive);
+  do_check_true(a1.userDisabled);
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      ["onInstalling", false],
+      "onInstalled"
+    ]
+  }, [
+    "onNewInstall",
+    "onInstallStarted",
+    "onInstallEnded"
+  ]);
+
+  yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  ensure_test_completed();
+
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+  BootstrapMonitor.checkAddonNotStarted(ID, "1.0");
+  do_check_eq(getUninstallReason(ID), ADDON_DOWNGRADE);
+  do_check_eq(getInstallReason(ID), ADDON_DOWNGRADE);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(a1.isActive);
+  do_check_true(a1.userDisabled);
+
+  yield promiseRestartManager();
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonNotStarted(ID, "1.0");
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_false(a1.isActive);
+  do_check_true(a1.userDisabled);
+
+  a1.uninstall();
+});
+
+
+// Test that uninstalling a temporary addon can be canceled
+add_task(function* cancelUninstallTemporary() {
+  yield AddonManager.installTemporaryAddon(do_get_addon("test_undouninstall1"));
+
+  let a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+  do_check_eq(getStartupReason(ID), ADDON_ENABLE);
+  do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+  do_check_true(a1.isActive);
+  do_check_false(a1.userDisabled);
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  a1.uninstall(true);
+  ensure_test_completed();
+
+  BootstrapMonitor.checkAddonNotStarted(ID, "1.0");
+  do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+
+  prepare_test({
+    "undouninstall1@tests.mozilla.org": [
+      "onOperationCancelled"
+    ]
+  });
+  a1.cancelUninstall();
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  do_check_eq(a1.pendingOperations, 0);
+
+  yield promiseRestartManager();
+});
+
+// Tests that cancelling the uninstall of an incompatible restartless addon
+// does not start the addon
+add_task(function* cancelUninstallIncompatibleRestartless() {
+  yield promiseInstallAllFiles([do_get_addon("test_undoincompatible")]);
+
+  let a1 = yield promiseAddonByID(INCOMPAT_ID);
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonNotStarted(INCOMPAT_ID);
+  do_check_false(a1.isActive);
+
+  prepare_test({
+    "incompatible@tests.mozilla.org": [
+      "onUninstalling"
+    ]
+  });
+  a1.uninstall(true);
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID(INCOMPAT_ID);
+  do_check_neq(a1, null);
+  do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+  do_check_false(a1.isActive);
+
+  prepare_test({
+    "incompatible@tests.mozilla.org": [
+      "onOperationCancelled"
+    ]
+  });
+  a1.cancelUninstall();
+  ensure_test_completed();
+
+  a1 = yield promiseAddonByID(INCOMPAT_ID);
+  do_check_neq(a1, null);
+  BootstrapMonitor.checkAddonNotStarted(INCOMPAT_ID);
+  do_check_eq(a1.pendingOperations, 0);
+  do_check_false(a1.isActive);
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_uninstall.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_uninstall.js
@@ -196,16 +196,17 @@ function run_test_4() {
 
     prepare_test({
       "addon1@tests.mozilla.org": [
         ["onUninstalling", false],
         "onUninstalled"
       ]
     });
     a1.uninstall();
+    ensure_test_completed();
 
     check_test_4();
   });
 }
 
 function check_test_4() {
   AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
     do_check_eq(a1, null);
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -257,16 +257,18 @@ run-if = addon_signing
 fail-if = os == "android"
 [test_syncGUID.js]
 [test_strictcompatibility.js]
 [test_targetPlatforms.js]
 [test_theme.js]
 # Bug 676992: test consistently fails on Android
 fail-if = os == "android"
 [test_types.js]
+[test_undothemeuninstall.js]
+[test_undouninstall.js]
 [test_uninstall.js]
 [test_update.js]
 # Bug 676992: test consistently hangs on Android
 skip-if = os == "android"
 [test_update_webextensions.js]
 [test_updateCancel.js]
 [test_update_strictcompat.js]
 # Bug 676992: test consistently hangs on Android
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -28,10 +28,9 @@ skip-if = appname != "firefox"
 [test_shutdown.js]
 [test_system_update.js]
 [test_system_reset.js]
 [test_XPIcancel.js]
 [test_XPIStates.js]
 [test_temporary.js]
 [test_proxy.js]
 
-
 [include:xpcshell-shared.ini]
--- a/toolkit/themes/shared/aboutNetworking.css
+++ b/toolkit/themes/shared/aboutNetworking.css
@@ -22,27 +22,35 @@ body {
   text-align: end;
   margin-bottom: 0.5em;
 }
 
 #refreshButton {
   vertical-align: middle;
 }
 
+/** Categories **/
+
 .category {
   cursor: pointer;
   /* Center category names */
   display: flex;
   align-items: center;
 }
 
 .category .category-name {
   pointer-events: none;
 }
 
+#categories hr {
+  border-top-color: rgba(255,255,255,0.15);
+}
+
+/** Warning container **/
+
 /* XXX: a lot of this is duplicated from info-pages.css since that stylesheet
    is incompatible with this type of layout */
 .warningBackground:not([hidden]) {
   display: flex;
 }
 
 .warningBackground {
   flex-direction: column;
@@ -79,41 +87,38 @@ body {
 }
 
 .warningBackground button {
   margin-top: 1em;
   margin-left: 0;
   min-width: 100px;
 }
 
+/** Content area **/
+
 .main-content {
   flex: 1;
 }
 
 .tab {
   padding: 0.5em 0;
 }
 
 .tab table {
   border: 1;
   width: 100%;
 }
 
-hr {
+th, td, table {
+  border-collapse: collapse;
   border: none;
-  border-bottom: 1px solid rgba(255,255,255,0.15);
+  text-align: start;
 }
 
 th {
   padding-bottom: 0.5em;
   font-size: larger;
 }
 
-th, td, table {
-  border-collapse: collapse;
-  border: none;
-  text-align: start;
-}
-
 td {
   padding-bottom: 0.25em;
   border-bottom: 1px solid var(--in-content-box-border-color);
 }