Backed out 3 changesets (bug 1315407) for bc7 permafail a=backout
authorWes Kocher <wkocher@mozilla.com>
Fri, 11 Nov 2016 12:44:58 -0800
changeset 322295 00132770eecba298426f1f8fad715e1f92738f94
parent 322294 af938e1a6b64ed5f062028badaceec25eb9b0ee6
child 322296 b333ba0fdd87463f9cc8bece66faf1c22c4c0716
push id30945
push usercbook@mozilla.com
push dateMon, 14 Nov 2016 09:22:29 +0000
treeherdermozilla-central@1196bf3032e1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs1315407
milestone52.0a1
backs outa3fbd5543a77e9a3ba8b2aa86fe16034b8fad340
50984e605fd7a688bea8c14cf7dd645c697a6967
c7fa771f5bb1893d1e4bb6812aaf5e2dc6015aea
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
Backed out 3 changesets (bug 1315407) for bc7 permafail a=backout Backed out changeset a3fbd5543a77 (bug 1315407) Backed out changeset 50984e605fd7 (bug 1315407) Backed out changeset c7fa771f5bb1 (bug 1315407)
addon-sdk/source/lib/sdk/addon/installer.js
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/addon-sdk/source/lib/sdk/addon/installer.js
+++ b/addon-sdk/source/lib/sdk/addon/installer.js
@@ -59,17 +59,17 @@ exports.install = function install(xpiPa
     },
     onDownloadFailed: function(aInstall) {
       this.onInstallFailed(aInstall);
     }
   };
 
   // Order AddonManager to install the addon
   AddonManager.getInstallForFile(file, function(install) {
-    if (install.error == 0) {
+    if (install.error != null) {
       install.addListener(listener);
       install.install();
     } else {
       reject(install.error);
     }
   });
 
   return promise;
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -2087,16 +2087,35 @@ function getDirectoryEntries(aDir, aSort
   finally {
     if (dirEnum) {
       dirEnum.close();
     }
   }
 }
 
 /**
+ * Wraps a function in an exception handler to protect against exceptions inside callbacks
+ * @param aFunction function(args...)
+ * @return function(args...), a function that takes the same arguments as aFunction
+ *         and returns the same result unless aFunction throws, in which case it logs
+ *         a warning and returns undefined.
+ */
+function makeSafe(aFunction) {
+  return function(...aArgs) {
+    try {
+      return aFunction(...aArgs);
+    }
+    catch (ex) {
+      logger.warn("XPIProvider callback failed", ex);
+    }
+    return undefined;
+  }
+}
+
+/**
  * Record a bit of per-addon telemetry
  * @param aAddon the addon to record
  */
 function recordAddonTelemetry(aAddon) {
   let locale = aAddon.defaultLocale;
   if (locale) {
     if (locale.name)
       XPIProvider.setTelemetry(aAddon.id, "name", locale.name);
@@ -2717,17 +2736,17 @@ this.XPIProvider = {
       XPIProvider.installLocationsByName[location.name] = location;
     }
 
     try {
       AddonManagerPrivate.recordTimestamp("XPI_startup_begin");
 
       logger.debug("startup");
       this.runPhase = XPI_STARTING;
-      this.installs = new Set();
+      this.installs = [];
       this.installLocations = [];
       this.installLocationsByName = {};
       // Hook for tests to detect when saving database at shutdown time fails
       this._shutdownError = null;
       // Clear this at startup for xpcshell test restarts
       this._telemetryDetails = {};
       // Register our details structure with AddonManager
       AddonManagerPrivate.setTelemetryDetails("XPI", this._telemetryDetails);
@@ -3518,17 +3537,18 @@ this.XPIProvider = {
             source: stageDirEntry,
             existingAddonID
           });
         }
         catch (e) {
           logger.error("Failed to install staged add-on " + id + " in " + location.name,
                 e);
           // Re-create the staged install
-          new StagedAddonInstall(location, stageDirEntry, addon);
+          AddonInstall.createStagedInstall(location, stageDirEntry,
+                                           addon);
           // Make sure not to delete the cached manifest json file
           seenFiles.pop();
 
           delete aManifests[location.name][id];
 
           if (oldBootstrap) {
             // Re-install the old add-on
             this.callBootstrapMethod(createAddonDetails(existingAddonID, oldBootstrap),
@@ -3978,33 +3998,36 @@ this.XPIProvider = {
    *         A version for the install
    * @param  aBrowser
    *         The browser performing the install
    * @param  aCallback
    *         A callback to pass the AddonInstall to
    */
   getInstallForURL: function(aUrl, aHash, aName, aIcons, aVersion, aBrowser,
                              aCallback) {
-    createDownloadInstall(function(aInstall) {
+    AddonInstall.createDownload(function(aInstall) {
       aCallback(aInstall.wrapper);
     }, aUrl, aHash, aName, aIcons, aVersion, aBrowser);
   },
 
   /**
    * Called to get an AddonInstall to install an add-on from a local file.
    *
    * @param  aFile
    *         The file to be installed
    * @param  aCallback
    *         A callback to pass the AddonInstall to
    */
   getInstallForFile: function(aFile, aCallback) {
-    createLocalInstall(aFile).then(install => {
-      aCallback(install ? install.wrapper : null);
-    });
+    AddonInstall.createInstall(function(aInstall) {
+      if (aInstall)
+        aCallback(aInstall.wrapper);
+      else
+        aCallback(null);
+    }, aFile);
   },
 
   /**
    * Temporarily installs add-on from a local XPI file or directory.
    * As this is intended for development, the signature is not checked and
    * the add-on does not persist on application restart.
    *
    * @param aFile
@@ -4174,17 +4197,23 @@ this.XPIProvider = {
 
   /**
    * Removes an AddonInstall from the list of active installs.
    *
    * @param  install
    *         The AddonInstall to remove
    */
   removeActiveInstall: function(aInstall) {
-    this.installs.delete(aInstall);
+    let where = this.installs.indexOf(aInstall);
+    if (where == -1) {
+      logger.warn("removeActiveInstall: could not find active install for "
+          + aInstall.sourceURI.spec);
+      return;
+    }
+    this.installs.splice(where, 1);
   },
 
   /**
    * Called to get an Addon with a particular ID.
    *
    * @param  aId
    *         The ID of the add-on to retrieve
    * @param  aCallback
@@ -4253,17 +4282,17 @@ this.XPIProvider = {
    * types.
    *
    * @param  aTypes
    *         An array of types or null to get all types
    * @param  aCallback
    *         A callback to pass the array of AddonInstalls to
    */
   getInstallsByTypes: function(aTypes, aCallback) {
-    let results = [...this.installs];
+    let results = this.installs.slice(0);
     if (aTypes) {
       results = results.filter(install => {
         return aTypes.includes(getExternalType(install.type));
       });
     }
 
     aCallback(results.map(install => install.wrapper));
   },
@@ -5331,112 +5360,293 @@ function getHashStringForCrypto(aCrypto)
 
   // convert the binary hash data to a hex string.
   let binary = aCrypto.finish(false);
   let hash = Array.from(binary, c => toHexString(c.charCodeAt(0)))
   return hash.join("").toLowerCase();
 }
 
 /**
- * Base class for objects that manage the installation of an addon.
- * This class isn't instantiated directly, see the derived classes below.
+ * Instantiates an AddonInstall.
+ *
+ * @param  aInstallLocation
+ *         The install location the add-on will be installed into
+ * @param  aUrl
+ *         The nsIURL to get the add-on from. If this is an nsIFileURL then
+ *         the add-on will not need to be downloaded
+ * @param  aHash
+ *         An optional hash for the add-on
+ * @param  aReleaseNotesURI
+ *         An optional nsIURI of release notes for the add-on
+ * @param  aExistingAddon
+ *         The add-on this install will update if known
+ * @param  aBrowser
+ *         The browser performing the install
+ * @throws if the url is the url of a local file and the hash does not match
+ *         or the add-on does not contain an valid install manifest
  */
-class AddonInstall {
-  /**
-   * Instantiates an AddonInstall.
+function AddonInstall(aInstallLocation, aUrl, aHash, aReleaseNotesURI,
+                      aExistingAddon, aBrowser) {
+  this.wrapper = new AddonInstallWrapper(this);
+  this.installLocation = aInstallLocation;
+  this.sourceURI = aUrl;
+  this.releaseNotesURI = aReleaseNotesURI;
+  if (aHash) {
+    let hashSplit = aHash.toLowerCase().split(":");
+    this.originalHash = {
+      algorithm: hashSplit[0],
+      data: hashSplit[1]
+    };
+  }
+  this.hash = this.originalHash;
+  this.browser = aBrowser;
+  this.listeners = [];
+  this.icons = {};
+  this.existingAddon = aExistingAddon;
+  this.error = 0;
+  this.window = aBrowser ? aBrowser.contentWindow : null;
+
+  // Giving each instance of AddonInstall a reference to the logger.
+  this.logger = logger;
+}
+
+AddonInstall.prototype = {
+  installLocation: null,
+  wrapper: null,
+  stream: null,
+  crypto: null,
+  originalHash: null,
+  hash: null,
+  browser: null,
+  badCertHandler: null,
+  listeners: null,
+  restartDownload: false,
+
+  name: null,
+  type: null,
+  version: null,
+  icons: null,
+  releaseNotesURI: null,
+  sourceURI: null,
+  file: null,
+  ownsTempFile: false,
+  certificate: null,
+  certName: null,
+
+  linkedInstalls: null,
+  existingAddon: null,
+  addon: null,
+
+  state: null,
+  error: null,
+  progress: null,
+  maxProgress: null,
+
+  /**
+   * Initialises this install to be a staged install waiting to be applied
+   *
+   * @param  aManifest
+   *         The cached manifest for the staged install
+   */
+  initStagedInstall: function(aManifest) {
+    this.name = aManifest.name;
+    this.type = aManifest.type;
+    this.version = aManifest.version;
+    this.icons = aManifest.icons;
+    this.releaseNotesURI = aManifest.releaseNotesURI ?
+                           NetUtil.newURI(aManifest.releaseNotesURI) :
+                           null
+    this.sourceURI = aManifest.sourceURI ?
+                     NetUtil.newURI(aManifest.sourceURI) :
+                     null;
+    this.file = null;
+    this.addon = aManifest;
+
+    this.state = AddonManager.STATE_INSTALLED;
+
+    XPIProvider.installs.push(this);
+  },
+
+  /**
+   * Initialises this install to be an install from a local file.
    *
-   * @param  aInstallLocation
-   *         The install location the add-on will be installed into
-   * @param  aUrl
-   *         The nsIURL to get the add-on from. If this is an nsIFileURL then
-   *         the add-on will not need to be downloaded
-   * @param  aHash
-   *         An optional hash for the add-on
-   * @param  aExistingAddon
-   *         The add-on this install will update if known
-   */
-  constructor(aInstallLocation, aUrl, aHash, aExistingAddon) {
-    this.wrapper = new AddonInstallWrapper(this);
-    this.installLocation = aInstallLocation;
-    this.sourceURI = aUrl;
-
-    if (aHash) {
-      let hashSplit = aHash.toLowerCase().split(":");
-      this.originalHash = {
-        algorithm: hashSplit[0],
-        data: hashSplit[1]
-      };
-    }
-    this.hash = this.originalHash;
-    this.existingAddon = aExistingAddon;
-    this.releaseNotesURI = null;
-
-    this.listeners = [];
-    this.icons = {};
-    this.error = 0;
-
+   * @param  aCallback
+   *         The callback to pass the initialised AddonInstall to
+   */
+  initLocalInstall: function(aCallback) {
+    aCallback = makeSafe(aCallback);
+    this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file;
+
+    if (!this.file.exists()) {
+      logger.warn("XPI file " + this.file.path + " does not exist");
+      this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+      this.error = AddonManager.ERROR_NETWORK_FAILURE;
+      aCallback(this);
+      return;
+    }
+
+    this.state = AddonManager.STATE_DOWNLOADED;
+    this.progress = this.file.fileSize;
+    this.maxProgress = this.file.fileSize;
+
+    if (this.hash) {
+      let crypto = Cc["@mozilla.org/security/hash;1"].
+                   createInstance(Ci.nsICryptoHash);
+      try {
+        crypto.initWithString(this.hash.algorithm);
+      }
+      catch (e) {
+        logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
+        this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+        this.error = AddonManager.ERROR_INCORRECT_HASH;
+        aCallback(this);
+        return;
+      }
+
+      let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+                createInstance(Ci.nsIFileInputStream);
+      fis.init(this.file, -1, -1, false);
+      crypto.updateFromStream(fis, this.file.fileSize);
+      let calculatedHash = getHashStringForCrypto(crypto);
+      if (calculatedHash != this.hash.data) {
+        logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" +
+             this.hash.data + ")");
+        this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+        this.error = AddonManager.ERROR_INCORRECT_HASH;
+        aCallback(this);
+        return;
+      }
+    }
+
+    this.loadManifest(this.file).then(() => {
+      XPIDatabase.getVisibleAddonForID(this.addon.id, aAddon => {
+        this.existingAddon = aAddon;
+        if (aAddon)
+          applyBlocklistChanges(aAddon, this.addon);
+        this.addon.updateDate = Date.now();
+        this.addon.installDate = aAddon ? aAddon.installDate : this.addon.updateDate;
+
+        if (!this.addon.isCompatible) {
+          // TODO Should we send some event here?
+          this.state = AddonManager.STATE_CHECKING;
+          new UpdateChecker(this.addon, {
+            onUpdateFinished: aAddon => {
+              this.state = AddonManager.STATE_DOWNLOADED;
+              XPIProvider.installs.push(this);
+              AddonManagerPrivate.callInstallListeners("onNewInstall",
+                                                       this.listeners,
+                                                       this.wrapper);
+
+              aCallback(this);
+            }
+          }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
+        }
+        else {
+          XPIProvider.installs.push(this);
+          AddonManagerPrivate.callInstallListeners("onNewInstall",
+                                                   this.listeners,
+                                                   this.wrapper);
+
+          aCallback(this);
+        }
+      });
+    }, ([error, message]) => {
+      logger.warn("Invalid XPI", message);
+      this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+      this.error = error;
+      AddonManagerPrivate.callInstallListeners("onNewInstall",
+                                               this.listeners,
+                                               this.wrapper);
+
+      aCallback(this);
+    });
+  },
+
+  /**
+   * Initialises this install to be a download from a remote url.
+   *
+   * @param  aCallback
+   *         The callback to pass the initialised AddonInstall to
+   * @param  aName
+   *         An optional name for the add-on
+   * @param  aType
+   *         An optional type for the add-on
+   * @param  aIcons
+   *         Optional icons for the add-on
+   * @param  aVersion
+   *         An optional version for the add-on
+   */
+  initAvailableDownload: function(aName, aType, aIcons, aVersion, aCallback) {
+    this.state = AddonManager.STATE_AVAILABLE;
+    this.name = aName;
+    this.type = aType;
+    this.version = aVersion;
+    this.icons = aIcons;
     this.progress = 0;
     this.maxProgress = -1;
 
-    // Giving each instance of AddonInstall a reference to the logger.
-    this.logger = logger;
-
-    this.name = null;
-    this.type = null;
-    this.version = null;
-
-    this.file = null;
-    this.ownsTempFile = null;
-    this.certificate = null;
-    this.certName = null;
-
-    this.linkedInstalls = null;
-    this.addon = null;
-    this.state = null;
-
-    XPIProvider.installs.add(this);
-  }
+    XPIProvider.installs.push(this);
+    AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners,
+                                             this.wrapper);
+
+    makeSafe(aCallback)(this);
+  },
 
   /**
    * Starts installation of this add-on from whatever state it is currently at
    * if possible.
    *
-   * Note this method is overridden to handle additional state in
-   * the subclass DownloadAddonInstall.
-   *
    * @throws if installation cannot proceed from the current state
    */
-  install() {
+  install: function() {
     switch (this.state) {
+    case AddonManager.STATE_AVAILABLE:
+      this.startDownload();
+      break;
     case AddonManager.STATE_DOWNLOADED:
       this.startInstall();
       break;
+    case AddonManager.STATE_DOWNLOAD_FAILED:
+    case AddonManager.STATE_INSTALL_FAILED:
+    case AddonManager.STATE_CANCELLED:
+      this.removeTemporaryFile();
+      this.state = AddonManager.STATE_AVAILABLE;
+      this.error = 0;
+      this.progress = 0;
+      this.maxProgress = -1;
+      this.hash = this.originalHash;
+      XPIProvider.installs.push(this);
+      this.startDownload();
+      break;
     case AddonManager.STATE_POSTPONED:
       logger.debug(`Postponing install of ${this.addon.id}`);
       break;
     case AddonManager.STATE_DOWNLOADING:
     case AddonManager.STATE_CHECKING:
     case AddonManager.STATE_INSTALLING:
       // Installation is already running
       return;
     default:
       throw new Error("Cannot start installing from this state");
     }
-  }
+  },
 
   /**
    * Cancels installation of this add-on.
    *
-   * Note this method is overridden to handle additional state in
-   * the subclass DownloadAddonInstall.
-   *
    * @throws if installation cannot be cancelled from the current state
    */
-  cancel() {
+  cancel: function() {
     switch (this.state) {
+    case AddonManager.STATE_DOWNLOADING:
+      if (this.channel) {
+        logger.debug("Cancelling download of " + this.sourceURI.spec);
+        this.channel.cancel(Cr.NS_BINDING_ABORTED);
+      }
+      break;
     case AddonManager.STATE_AVAILABLE:
     case AddonManager.STATE_DOWNLOADED:
       logger.debug("Cancelling download of " + this.sourceURI.spec);
       this.state = AddonManager.STATE_CANCELLED;
       XPIProvider.removeActiveInstall(this);
       AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
                                                this.listeners, this.wrapper);
       this.removeTemporaryFile();
@@ -5472,46 +5682,46 @@ class AddonInstall {
       let stagingDir = this.installLocation.getStagingDir();
       let stagedAddon = stagingDir.clone();
 
       this.unstageInstall(stagedAddon);
     default:
       throw new Error("Cannot cancel install of " + this.sourceURI.spec +
                       " from this state (" + this.state + ")");
     }
-  }
+  },
 
   /**
    * Adds an InstallListener for this instance if the listener is not already
    * registered.
    *
    * @param  aListener
    *         The InstallListener to add
    */
-  addListener(aListener) {
+  addListener: function(aListener) {
     if (!this.listeners.some(function(i) { return i == aListener; }))
       this.listeners.push(aListener);
-  }
+  },
 
   /**
    * Removes an InstallListener for this instance if it is registered.
    *
    * @param  aListener
    *         The InstallListener to remove
    */
-  removeListener(aListener) {
+  removeListener: function(aListener) {
     this.listeners = this.listeners.filter(function(i) {
       return i != aListener;
     });
-  }
+  },
 
   /**
    * Removes the temporary file owned by this AddonInstall if there is one.
    */
-  removeTemporaryFile() {
+  removeTemporaryFile: function() {
     // Only proceed if this AddonInstall owns its XPI file
     if (!this.ownsTempFile) {
       this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " does not own temp file");
       return;
     }
 
     try {
       this.logger.debug("removeTemporaryFile: " + this.sourceURI.spec + " removing temp file " +
@@ -5519,276 +5729,610 @@ class AddonInstall {
       this.file.remove(true);
       this.ownsTempFile = false;
     }
     catch (e) {
       this.logger.warn("Failed to remove temporary file " + this.file.path + " for addon " +
           this.sourceURI.spec,
           e);
     }
-  }
+  },
 
   /**
    * Updates the sourceURI and releaseNotesURI values on the Addon being
    * installed by this AddonInstall instance.
    */
-  updateAddonURIs() {
+  updateAddonURIs: function() {
     this.addon.sourceURI = this.sourceURI.spec;
     if (this.releaseNotesURI)
       this.addon.releaseNotesURI = this.releaseNotesURI.spec;
-  }
+  },
 
   /**
    * Fills out linkedInstalls with AddonInstall instances for the other files
    * in a multi-package XPI.
    *
    * @param  aFiles
    *         An array of { entryName, file } for each remaining file from the
    *         multi-package XPI.
    */
-  _createLinkedInstalls(aFiles) {
-    return Task.spawn((function*() {
-      if (aFiles.length == 0)
-        return;
-
-      // Create new AddonInstall instances for every remaining file
-      if (!this.linkedInstalls)
-        this.linkedInstalls = [];
-
-      for (let { entryName, file } of aFiles) {
-        logger.debug("Creating linked install from " + entryName);
-        let install = yield createLocalInstall(file);
-
-        // Make the new install own its temporary file
-        install.ownsTempFile = true;
-
-        this.linkedInstalls.push(install);
-
-        // If one of the internal XPIs was multipackage then move its linked
-        // installs to the outer install
-        if (install.linkedInstalls) {
-          this.linkedInstalls.push(...install.linkedInstalls);
-          install.linkedInstalls = null;
-        }
-
-        install.sourceURI = this.sourceURI;
-        install.releaseNotesURI = this.releaseNotesURI;
-        if (install.state != AddonManager.STATE_DOWNLOAD_FAILED)
-          install.updateAddonURIs();
-      }
-    }).bind(this));
-  }
+  _createLinkedInstalls: Task.async(function*(aFiles) {
+    if (aFiles.length == 0)
+      return;
+
+    // Create new AddonInstall instances for every remaining file
+    if (!this.linkedInstalls)
+      this.linkedInstalls = [];
+
+    for (let { entryName, file } of aFiles) {
+      logger.debug("Creating linked install from " + entryName);
+      let install = yield new Promise(
+        resolve => AddonInstall.createInstall(resolve, file)
+      );
+
+      // Make the new install own its temporary file
+      install.ownsTempFile = true;
+
+      this.linkedInstalls.push(install);
+
+      // If one of the internal XPIs was multipackage then move its linked
+      // installs to the outer install
+      if (install.linkedInstalls) {
+        this.linkedInstalls.push(...install.linkedInstalls);
+        install.linkedInstalls = null;
+      }
+
+      install.sourceURI = this.sourceURI;
+      install.releaseNotesURI = this.releaseNotesURI;
+      if (install.state != AddonManager.STATE_DOWNLOAD_FAILED)
+        install.updateAddonURIs();
+    }
+  }),
 
   /**
    * Loads add-on manifests from a multi-package XPI file. Each of the
    * XPI and JAR files contained in the XPI will be extracted. Any that
    * do not contain valid add-ons will be ignored. The first valid add-on will
    * be installed by this AddonInstall instance, the rest will have new
    * AddonInstall instances created for them.
    *
    * @param  aZipReader
    *         An open nsIZipReader for the multi-package XPI's files. This will
    *         be closed before this method returns.
    */
-  _loadMultipackageManifests(aZipReader) {
-    return Task.spawn((function*() {
-      let files = [];
-      let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
-      while (entries.hasMore()) {
-        let entryName = entries.getNext();
-        let file = getTemporaryFile();
-        try {
-          aZipReader.extract(entryName, file);
-          files.push({ entryName, file });
-        }
-        catch (e) {
-          logger.warn("Failed to extract " + entryName + " from multi-package " +
-                      "XPI", e);
-          file.remove(false);
-        }
-      }
-
-      aZipReader.close();
-
-      if (files.length == 0) {
-        return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
-                               "Multi-package XPI does not contain any packages to install"]);
-      }
-
-      let addon = null;
-
-      // Find the first file that is a valid install and use it for
-      // the add-on that this AddonInstall instance will install.
-      for (let { entryName, file } of files) {
-        this.removeTemporaryFile();
-        try {
-          yield this.loadManifest(file);
-          logger.debug("Base multi-package XPI install came from " + entryName);
-          this.file = file;
-          this.ownsTempFile = true;
-
-          yield this._createLinkedInstalls(files.filter(f => f.file != file));
-          return undefined;
-        }
-        catch (e) {
-          // _createLinkedInstalls will log errors when it tries to process this
-          // file
-        }
-      }
-
-      // No valid add-on was found, delete all the temporary files
-      for (let { file } of files) {
-        try {
-          file.remove(true);
-        } catch (e) {
-          this.logger.warn("Could not remove temp file " + file.path);
-        }
-      }
-
+  _loadMultipackageManifests: Task.async(function*(aZipReader) {
+    let files = [];
+    let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
+    while (entries.hasMore()) {
+      let entryName = entries.getNext();
+      let file = getTemporaryFile();
+      try {
+        aZipReader.extract(entryName, file);
+        files.push({ entryName, file });
+      }
+      catch (e) {
+        logger.warn("Failed to extract " + entryName + " from multi-package " +
+             "XPI", e);
+        file.remove(false);
+      }
+    }
+
+    aZipReader.close();
+
+    if (files.length == 0) {
       return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
-                             "Multi-package XPI does not contain any valid packages to install"]);
-    }).bind(this));
-  }
+                             "Multi-package XPI does not contain any packages to install"]);
+    }
+
+    let addon = null;
+
+    // Find the first file that is a valid install and use it for
+    // the add-on that this AddonInstall instance will install.
+    for (let { entryName, file } of files) {
+      this.removeTemporaryFile();
+      try {
+        yield this.loadManifest(file);
+        logger.debug("Base multi-package XPI install came from " + entryName);
+        this.file = file;
+        this.ownsTempFile = true;
+
+        yield this._createLinkedInstalls(files.filter(f => f.file != file));
+        return undefined;
+      }
+      catch (e) {
+        // _createLinkedInstalls will log errors when it tries to process this
+        // file
+      }
+    }
+
+    // No valid add-on was found, delete all the temporary files
+    for (let { file } of files) {
+      try {
+        file.remove(true);
+      } catch (e) {
+        this.logger.warn("Could not remove temp file " + file.path);
+      }
+    }
+
+    return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
+                           "Multi-package XPI does not contain any valid packages to install"]);
+  }),
 
   /**
    * Called after the add-on is a local file and the signature and install
    * manifest can be read.
    *
    * @param  aCallback
    *         A function to call when the manifest has been loaded
    * @throws if the add-on does not contain a valid install manifest or the
    *         XPI is incorrectly signed
    */
-  loadManifest(file) {
-    return Task.spawn((function*() {
-      let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
-          createInstance(Ci.nsIZipReader);
+  loadManifest: Task.async(function*(file) {
+    let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
+                    createInstance(Ci.nsIZipReader);
+    try {
+      zipreader.open(file);
+    }
+    catch (e) {
+      zipreader.close();
+      return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
+    }
+
+    try {
+      // loadManifestFromZipReader performs the certificate verification for us
+      this.addon = yield loadManifestFromZipReader(zipreader, this.installLocation);
+    }
+    catch (e) {
+      zipreader.close();
+      return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
+    }
+
+    // A multi-package XPI is a container, the add-ons it holds each
+    // have their own id.  Everything else had better have an id here.
+    if (!this.addon.id && this.addon.type != "multipackage") {
+      let err = new Error(`Cannot find id for addon ${file.path}`);
+      return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, err]);
+    }
+
+    if (this.existingAddon) {
+      // Check various conditions related to upgrades
+      if (this.addon.id != this.existingAddon.id) {
+        zipreader.close();
+        return Promise.reject([AddonManager.ERROR_INCORRECT_ID,
+                               `Refusing to upgrade addon ${this.existingAddon.id} to different ID {this.addon.id}`]);
+      }
+
+      if (this.addon.type == "multipackage") {
+        zipreader.close();
+        return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
+                               `Refusing to upgrade addon ${this.existingAddon.id} to a multi-package xpi`]);
+      }
+
+      if (this.existingAddon.type == "webextension" && this.addon.type != "webextension") {
+        zipreader.close();
+        return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
+                               "WebExtensions may not be upated to other extension types"]);
+      }
+    }
+
+    if (mustSign(this.addon.type)) {
+      if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
+        // This add-on isn't properly signed by a signature that chains to the
+        // trusted root.
+        let state = this.addon.signedState;
+        this.addon = null;
+        zipreader.close();
+
+        if (state == AddonManager.SIGNEDSTATE_MISSING)
+          return Promise.reject([AddonManager.ERROR_SIGNEDSTATE_REQUIRED,
+                                 "signature is required but missing"])
+
+        return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
+                               "signature verification failed"])
+      }
+    }
+    else if (this.addon.signedState == AddonManager.SIGNEDSTATE_UNKNOWN ||
+             this.addon.signedState == AddonManager.SIGNEDSTATE_NOT_REQUIRED) {
+      // Check object signing certificate, if any
+      let x509 = zipreader.getSigningCert(null);
+      if (x509) {
+        logger.debug("Verifying XPI signature");
+        if (verifyZipSigning(zipreader, x509)) {
+          this.certificate = x509;
+          if (this.certificate.commonName.length > 0) {
+            this.certName = this.certificate.commonName;
+          } else {
+            this.certName = this.certificate.organization;
+          }
+        } else {
+          zipreader.close();
+          return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
+                                 "XPI is incorrectly signed"]);
+        }
+      }
+    }
+
+    if (this.addon.type == "multipackage")
+      return this._loadMultipackageManifests(zipreader);
+
+    zipreader.close();
+
+    this.updateAddonURIs();
+
+    this.addon._install = this;
+    this.name = this.addon.selectedLocale.name;
+    this.type = this.addon.type;
+    this.version = this.addon.version;
+
+    // Setting the iconURL to something inside the XPI locks the XPI and
+    // makes it impossible to delete on Windows.
+
+    // Try to load from the existing cache first
+    let repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve));
+
+    // It wasn't there so try to re-download it
+    if (!repoAddon) {
+      yield new Promise(resolve => AddonRepository.cacheAddons([this.addon.id], resolve));
+      repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve));
+    }
+
+    this.addon._repositoryAddon = repoAddon;
+    this.name = this.name || this.addon._repositoryAddon.name;
+    this.addon.compatibilityOverrides = repoAddon ?
+                                    repoAddon.compatibilityOverrides :
+                                    null;
+    this.addon.appDisabled = !isUsableAddon(this.addon);
+    return undefined;
+  }),
+
+  observe: function(aSubject, aTopic, aData) {
+    // Network is going offline
+    this.cancel();
+  },
+
+  /**
+   * Starts downloading the add-on's XPI file.
+   */
+  startDownload: function() {
+    this.state = AddonManager.STATE_DOWNLOADING;
+    if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted",
+                                                  this.listeners, this.wrapper)) {
+      logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec);
+      this.state = AddonManager.STATE_CANCELLED;
+      XPIProvider.removeActiveInstall(this);
+      AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
+                                               this.listeners, this.wrapper)
+      return;
+    }
+
+    // If a listener changed our state then do not proceed with the download
+    if (this.state != AddonManager.STATE_DOWNLOADING)
+      return;
+
+    if (this.channel) {
+      // A previous download attempt hasn't finished cleaning up yet, signal
+      // that it should restart when complete
+      logger.debug("Waiting for previous download to complete");
+      this.restartDownload = true;
+      return;
+    }
+
+    this.openChannel();
+  },
+
+  openChannel: function() {
+    this.restartDownload = false;
+
+    try {
+      this.file = getTemporaryFile();
+      this.ownsTempFile = true;
+      this.stream = Cc["@mozilla.org/network/file-output-stream;1"].
+                    createInstance(Ci.nsIFileOutputStream);
+      this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+                       FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0);
+    }
+    catch (e) {
+      logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
+      this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+      this.error = AddonManager.ERROR_FILE_ACCESS;
+      XPIProvider.removeActiveInstall(this);
+      AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+                                               this.listeners, this.wrapper);
+      return;
+    }
+
+    let listener = Cc["@mozilla.org/network/stream-listener-tee;1"].
+                   createInstance(Ci.nsIStreamListenerTee);
+    listener.init(this, this.stream);
+    try {
+      let requireBuiltIn = Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true);
+      this.badCertHandler = new CertUtils.BadCertHandler(!requireBuiltIn);
+
+      this.channel = NetUtil.newChannel({
+        uri: this.sourceURI,
+        loadUsingSystemPrincipal: true
+      });
+      this.channel.notificationCallbacks = this;
+      if (this.channel instanceof Ci.nsIHttpChannel) {
+        this.channel.setRequestHeader("Moz-XPI-Update", "1", true);
+        if (this.channel instanceof Ci.nsIHttpChannelInternal)
+          this.channel.forceAllowThirdPartyCookie = true;
+      }
+      this.channel.asyncOpen2(listener);
+
+      Services.obs.addObserver(this, "network:offline-about-to-go-offline", false);
+    }
+    catch (e) {
+      logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
+      this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+      this.error = AddonManager.ERROR_NETWORK_FAILURE;
+      XPIProvider.removeActiveInstall(this);
+      AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+                                               this.listeners, this.wrapper);
+    }
+  },
+
+  /**
+   * Update the crypto hasher with the new data and call the progress listeners.
+   *
+   * @see nsIStreamListener
+   */
+  onDataAvailable: function(aRequest, aContext, aInputstream,
+                                               aOffset, aCount) {
+    this.crypto.updateFromStream(aInputstream, aCount);
+    this.progress += aCount;
+    if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress",
+                                                  this.listeners, this.wrapper)) {
+      // TODO cancel the download and make it available again (bug 553024)
+    }
+  },
+
+  /**
+   * Check the redirect response for a hash of the target XPI and verify that
+   * we don't end up on an insecure channel.
+   *
+   * @see nsIChannelEventSink
+   */
+  asyncOnChannelRedirect: function(aOldChannel, aNewChannel, aFlags, aCallback) {
+    if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
+        aOldChannel instanceof Ci.nsIHttpChannel) {
       try {
-        zipreader.open(file);
+        let hashStr = aOldChannel.getResponseHeader("X-Target-Digest");
+        let hashSplit = hashStr.toLowerCase().split(":");
+        this.hash = {
+          algorithm: hashSplit[0],
+          data: hashSplit[1]
+        };
       }
       catch (e) {
-        zipreader.close();
-        return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
-      }
-
+      }
+    }
+
+    // Verify that we don't end up on an insecure channel if we haven't got a
+    // hash to verify with (see bug 537761 for discussion)
+    if (!this.hash)
+      this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback);
+    else
+      aCallback.onRedirectVerifyCallback(Cr.NS_OK);
+
+    this.channel = aNewChannel;
+  },
+
+  /**
+   * This is the first chance to get at real headers on the channel.
+   *
+   * @see nsIStreamListener
+   */
+  onStartRequest: function(aRequest, aContext) {
+    this.crypto = Cc["@mozilla.org/security/hash;1"].
+                  createInstance(Ci.nsICryptoHash);
+    if (this.hash) {
       try {
-        // loadManifestFromZipReader performs the certificate verification for us
-        this.addon = yield loadManifestFromZipReader(zipreader, this.installLocation);
+        this.crypto.initWithString(this.hash.algorithm);
+      }
+      catch (e) {
+        logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
+        this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+        this.error = AddonManager.ERROR_INCORRECT_HASH;
+        XPIProvider.removeActiveInstall(this);
+        AddonManagerPrivate.callInstallListeners("onDownloadFailed",
+                                                 this.listeners, this.wrapper);
+        aRequest.cancel(Cr.NS_BINDING_ABORTED);
+        return;
+      }
+    }
+    else {
+      // We always need something to consume data from the inputstream passed
+      // to onDataAvailable so just create a dummy cryptohasher to do that.
+      this.crypto.initWithString("sha1");
+    }
+
+    this.progress = 0;
+    if (aRequest instanceof Ci.nsIChannel) {
+      try {
+        this.maxProgress = aRequest.contentLength;
       }
       catch (e) {
-        zipreader.close();
-        return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, e]);
-      }
-
-      // A multi-package XPI is a container, the add-ons it holds each
-      // have their own id.  Everything else had better have an id here.
-      if (!this.addon.id && this.addon.type != "multipackage") {
-        let err = new Error(`Cannot find id for addon ${file.path}`);
-        return Promise.reject([AddonManager.ERROR_CORRUPT_FILE, err]);
-      }
+      }
+      logger.debug("Download started for " + this.sourceURI.spec + " to file " +
+          this.file.path);
+    }
+  },
+
+  /**
+   * The download is complete.
+   *
+   * @see nsIStreamListener
+   */
+  onStopRequest: function(aRequest, aContext, aStatus) {
+    this.stream.close();
+    this.channel = null;
+    this.badCerthandler = null;
+    Services.obs.removeObserver(this, "network:offline-about-to-go-offline");
+
+    // If the download was cancelled then update the state and send events
+    if (aStatus == Cr.NS_BINDING_ABORTED) {
+      if (this.state == AddonManager.STATE_DOWNLOADING) {
+        logger.debug("Cancelled download of " + this.sourceURI.spec);
+        this.state = AddonManager.STATE_CANCELLED;
+        XPIProvider.removeActiveInstall(this);
+        AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
+                                                 this.listeners, this.wrapper);
+        // If a listener restarted the download then there is no need to
+        // remove the temporary file
+        if (this.state != AddonManager.STATE_CANCELLED)
+          return;
+      }
+
+      this.removeTemporaryFile();
+      if (this.restartDownload)
+        this.openChannel();
+      return;
+    }
+
+    logger.debug("Download of " + this.sourceURI.spec + " completed.");
+
+    if (Components.isSuccessCode(aStatus)) {
+      if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) {
+        if (!this.hash && (aRequest instanceof Ci.nsIChannel)) {
+          try {
+            CertUtils.checkCert(aRequest,
+                                !Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true));
+          }
+          catch (e) {
+            this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e);
+            return;
+          }
+        }
+
+        // convert the binary hash data to a hex string.
+        let calculatedHash = getHashStringForCrypto(this.crypto);
+        this.crypto = null;
+        if (this.hash && calculatedHash != this.hash.data) {
+          this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH,
+                              "Downloaded file hash (" + calculatedHash +
+                              ") did not match provided hash (" + this.hash.data + ")");
+          return;
+        }
+
+        this.loadManifest(this.file).then(() => {
+          if (this.addon.isCompatible) {
+            this.downloadCompleted();
+          }
+          else {
+            // TODO Should we send some event here (bug 557716)?
+            this.state = AddonManager.STATE_CHECKING;
+            new UpdateChecker(this.addon, {
+              onUpdateFinished: aAddon => this.downloadCompleted(),
+            }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
+          }
+        }, ([error, message]) => {
+          this.removeTemporaryFile();
+          this.downloadFailed(error, message);
+        });
+      }
+      else if (aRequest instanceof Ci.nsIHttpChannel) {
+        this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE,
+                            aRequest.responseStatus + " " +
+                            aRequest.responseStatusText);
+      }
+      else {
+        this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
+      }
+    }
+    else {
+      this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
+    }
+  },
+
+  /**
+   * Notify listeners that the download failed.
+   *
+   * @param  aReason
+   *         Something to log about the failure
+   * @param  error
+   *         The error code to pass to the listeners
+   */
+  downloadFailed: function(aReason, aError) {
+    logger.warn("Download of " + this.sourceURI.spec + " failed", aError);
+    this.state = AddonManager.STATE_DOWNLOAD_FAILED;
+    this.error = aReason;
+    XPIProvider.removeActiveInstall(this);
+    AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners,
+                                             this.wrapper);
+
+    // If the listener hasn't restarted the download then remove any temporary
+    // file
+    if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) {
+      logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec);
+      this.removeTemporaryFile();
+    }
+    else
+      logger.debug("downloadFailed: listener changed AddonInstall state for " +
+          this.sourceURI.spec + " to " + this.state);
+  },
+
+  /**
+   * Notify listeners that the download completed.
+   */
+  downloadCompleted: function() {
+    XPIDatabase.getVisibleAddonForID(this.addon.id, aAddon => {
+      if (aAddon)
+        this.existingAddon = aAddon;
+
+      this.state = AddonManager.STATE_DOWNLOADED;
+      this.addon.updateDate = Date.now();
 
       if (this.existingAddon) {
-        // Check various conditions related to upgrades
-        if (this.addon.id != this.existingAddon.id) {
-          zipreader.close();
-          return Promise.reject([AddonManager.ERROR_INCORRECT_ID,
-                                 `Refusing to upgrade addon ${this.existingAddon.id} to different ID {this.addon.id}`]);
-        }
-
-        if (this.addon.type == "multipackage") {
-          zipreader.close();
-          return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
-                                 `Refusing to upgrade addon ${this.existingAddon.id} to a multi-package xpi`]);
-        }
-
-        if (this.existingAddon.type == "webextension" && this.addon.type != "webextension") {
-          zipreader.close();
-          return Promise.reject([AddonManager.ERROR_UNEXPECTED_ADDON_TYPE,
-                                 "WebExtensions may not be upated to other extension types"]);
-        }
-      }
-
-      if (mustSign(this.addon.type)) {
-        if (this.addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
-          // This add-on isn't properly signed by a signature that chains to the
-          // trusted root.
-          let state = this.addon.signedState;
-          this.addon = null;
-          zipreader.close();
-
-          if (state == AddonManager.SIGNEDSTATE_MISSING)
-            return Promise.reject([AddonManager.ERROR_SIGNEDSTATE_REQUIRED,
-                                   "signature is required but missing"])
-
-          return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
-                                 "signature verification failed"])
-        }
-      }
-      else if (this.addon.signedState == AddonManager.SIGNEDSTATE_UNKNOWN ||
-               this.addon.signedState == AddonManager.SIGNEDSTATE_NOT_REQUIRED) {
-        // Check object signing certificate, if any
-        let x509 = zipreader.getSigningCert(null);
-        if (x509) {
-          logger.debug("Verifying XPI signature");
-          if (verifyZipSigning(zipreader, x509)) {
-            this.certificate = x509;
-            if (this.certificate.commonName.length > 0) {
-              this.certName = this.certificate.commonName;
-            } else {
-              this.certName = this.certificate.organization;
+        this.addon.existingAddonID = this.existingAddon.id;
+        this.addon.installDate = this.existingAddon.installDate;
+        applyBlocklistChanges(this.existingAddon, this.addon);
+      }
+      else {
+        this.addon.installDate = this.addon.updateDate;
+      }
+
+      if (AddonManagerPrivate.callInstallListeners("onDownloadEnded",
+                                                   this.listeners,
+                                                   this.wrapper)) {
+        // If a listener changed our state then do not proceed with the install
+        if (this.state != AddonManager.STATE_DOWNLOADED)
+          return;
+
+        // If an upgrade listener is registered for this add-on, pass control
+        // over the upgrade to the add-on.
+        if (AddonManagerPrivate.hasUpgradeListener(this.addon.id)) {
+          logger.info(`add-on ${this.addon.id} has an upgrade listener, postponing upgrade until restart`);
+          let resumeFn = () => {
+            logger.info(`${this.addon.id} has resumed a previously postponed upgrade`);
+            this.state = AddonManager.STATE_DOWNLOADED;
+            this.install();
+          }
+          this.postpone(resumeFn);
+        } else {
+          // no upgrade listener present, so proceed with normal install
+          this.install();
+          if (this.linkedInstalls) {
+            for (let install of this.linkedInstalls) {
+              if (install.state == AddonManager.STATE_DOWNLOADED)
+                install.install();
             }
-          } else {
-            zipreader.close();
-            return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
-                                   "XPI is incorrectly signed"]);
           }
         }
       }
-
-      if (this.addon.type == "multipackage")
-        return this._loadMultipackageManifests(zipreader);
-
-      zipreader.close();
-
-      this.updateAddonURIs();
-
-      this.addon._install = this;
-      this.name = this.addon.selectedLocale.name;
-      this.type = this.addon.type;
-      this.version = this.addon.version;
-
-      // Setting the iconURL to something inside the XPI locks the XPI and
-      // makes it impossible to delete on Windows.
-
-      // Try to load from the existing cache first
-      let repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve));
-
-      // It wasn't there so try to re-download it
-      if (!repoAddon) {
-        yield new Promise(resolve => AddonRepository.cacheAddons([this.addon.id], resolve));
-        repoAddon = yield new Promise(resolve => AddonRepository.getCachedAddonByID(this.addon.id, resolve));
-      }
-
-      this.addon._repositoryAddon = repoAddon;
-      this.name = this.name || this.addon._repositoryAddon.name;
-      this.addon.compatibilityOverrides = repoAddon ?
-        repoAddon.compatibilityOverrides :
-        null;
-      this.addon.appDisabled = !isUsableAddon(this.addon);
-      return undefined;
-    }).bind(this));
-  }
+    });
+  },
 
   // TODO This relies on the assumption that we are always installing into the
   // highest priority install location so the resulting add-on will be visible
   // overriding any existing copy in another install location (bug 557710).
   /**
    * Installs the add-on into the install location.
    */
-  startInstall() {
+  startInstall: function() {
     this.state = AddonManager.STATE_INSTALLING;
     if (!AddonManagerPrivate.callInstallListeners("onInstallStarted",
                                                   this.listeners, this.wrapper)) {
       this.state = AddonManager.STATE_DOWNLOADED;
       XPIProvider.removeActiveInstall(this);
       AddonManagerPrivate.callInstallListeners("onInstallCancelled",
                                                this.listeners, this.wrapper)
       return;
@@ -5834,16 +6378,20 @@ class AddonInstall {
         AddonManagerPrivate.callInstallListeners("onInstallEnded",
                                                  this.listeners, this.wrapper,
                                                  this.addon.wrapper);
       }
       else {
         // The install is completed so it should be removed from the active list
         XPIProvider.removeActiveInstall(this);
 
+        // TODO We can probably reduce the number of DB operations going on here
+        // We probably also want to support rolling back failed upgrades etc.
+        // See bug 553015.
+
         // Deactivate and remove the old add-on as necessary
         let reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
         if (this.existingAddon) {
           if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0)
             reason = BOOTSTRAP_REASONS.ADDON_UPGRADE;
           else
             reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
 
@@ -5943,775 +6491,194 @@ class AddonInstall {
                                              this.addon.wrapper);
       AddonManagerPrivate.callInstallListeners("onInstallFailed",
                                                this.listeners,
                                                this.wrapper);
     }).then(() => {
       this.removeTemporaryFile();
       return this.installLocation.releaseStagingDir();
     });
-  }
+  },
 
   /**
    * Stages an upgrade for next application restart.
    */
-  stageInstall(restartRequired, stagedAddon, isUpgrade) {
-    return Task.spawn((function*() {
-      let stagedJSON = stagedAddon.clone();
-      stagedJSON.leafName = this.addon.id + ".json";
-
-      let installedUnpacked = 0;
-
-      // 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.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.leafName = this.addon.id + ".xpi";
-
-        yield OS.File.copy(this.file.path, stagedAddon.path);
-      }
-
-      if (restartRequired) {
-        // 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
-        writeStringToFile(stagedJSON, JSON.stringify(this.addon));
-
-        logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart.");
-        if (isUpgrade) {
-          delete this.existingAddon.pendingUpgrade;
-          this.existingAddon.pendingUpgrade = this.addon;
-        }
-      }
-
-      return installedUnpacked;
-    }).bind(this));
-  }
+  stageInstall: function*(restartRequired, stagedAddon, isUpgrade) {
+    let stagedJSON = stagedAddon.clone();
+    stagedJSON.leafName = this.addon.id + ".json";
+
+    let installedUnpacked = 0;
+
+    // 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.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.leafName = this.addon.id + ".xpi";
+
+      yield OS.File.copy(this.file.path, stagedAddon.path);
+    }
+
+    if (restartRequired) {
+      // 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
+      writeStringToFile(stagedJSON, JSON.stringify(this.addon));
+
+      logger.debug("Staged install of " + this.addon.id + " from " + this.sourceURI.spec + " ready; waiting for restart.");
+      if (isUpgrade) {
+        delete this.existingAddon.pendingUpgrade;
+        this.existingAddon.pendingUpgrade = this.addon;
+      }
+    }
+
+    return installedUnpacked;
+  },
 
   /**
    * Removes any previously staged upgrade.
    */
-  unstageInstall(stagedAddon) {
-    return Task.spawn((function*() {
-      let stagedJSON = stagedAddon.clone();
-      let removedAddon = stagedAddon.clone();
-
-      stagedJSON.append(this.addon.id + ".json");
-
-      if (stagedJSON.exists()) {
-        stagedJSON.remove(true);
-      }
-
-      removedAddon.append(this.addon.id);
-      yield removeAsync(removedAddon);
-      removedAddon.leafName = this.addon.id + ".xpi";
-      yield removeAsync(removedAddon);
-    }).bind(this));
-  }
-
-  /**
-    * Postone a pending update, until restart or until the add-on resumes.
-    *
-    * @param {Function} resumeFunction - a function for the add-on to run
-    *                                    when resuming.
-    */
-  postpone(resumeFunction) {
-    return Task.spawn((function*() {
-      this.state = AddonManager.STATE_POSTPONED;
-
-      let stagingDir = this.installLocation.getStagingDir();
-      let stagedAddon = stagingDir.clone();
-
-      yield this.installLocation.requestStagingDir();
-      yield this.unstageInstall(stagedAddon);
-
-      stagedAddon.append(this.addon.id);
-      stagedAddon.leafName = this.addon.id + ".xpi";
-
-      yield this.stageInstall(true, stagedAddon, true);
-
-      AddonManagerPrivate.callInstallListeners("onInstallPostponed",
-                                               this.listeners, this.wrapper)
-
-      // upgrade has been staged for restart, provide a way for it to call the
-      // resume function.
-      if (resumeFunction) {
-        let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id);
-        if (callback) {
-          callback({
-            version: this.version,
-            install: () => {
-              switch (this.state) {
-              case AddonManager.STATE_POSTPONED:
-                resumeFunction();
-                break;
-              default:
-                logger.warn(`${this.addon.id} cannot resume postponed upgrade from state (${this.state})`);
-                break;
-              }
-            },
-          });
-        }
-      }
-      this.installLocation.releaseStagingDir();
-    }).bind(this));
-  }
-}
-
-class LocalAddonInstall extends AddonInstall {
-  /**
-   * Initialises this install to be an install from a local file.
-   *
-   * @returns Promise
-   *          A Promise that resolves when the object is ready to use.
-   */
-  init() {
-    return Task.spawn((function*() {
-      this.file = this.sourceURI.QueryInterface(Ci.nsIFileURL).file;
-
-      if (!this.file.exists()) {
-        logger.warn("XPI file " + this.file.path + " does not exist");
-        this.state = AddonManager.STATE_DOWNLOAD_FAILED;
-        this.error = AddonManager.ERROR_NETWORK_FAILURE;
-        XPIProvider.removeActiveInstall(this);
-        return;
-      }
-
-      this.state = AddonManager.STATE_DOWNLOADED;
-      this.progress = this.file.fileSize;
-      this.maxProgress = this.file.fileSize;
-
-      if (this.hash) {
-        let crypto = Cc["@mozilla.org/security/hash;1"].
-            createInstance(Ci.nsICryptoHash);
-        try {
-          crypto.initWithString(this.hash.algorithm);
-        }
-        catch (e) {
-          logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
-          this.state = AddonManager.STATE_DOWNLOAD_FAILED;
-          this.error = AddonManager.ERROR_INCORRECT_HASH;
-          XPIProvider.removeActiveInstall(this);
-          return;
-        }
-
-        let fis = Cc["@mozilla.org/network/file-input-stream;1"].
-            createInstance(Ci.nsIFileInputStream);
-        fis.init(this.file, -1, -1, false);
-        crypto.updateFromStream(fis, this.file.fileSize);
-        let calculatedHash = getHashStringForCrypto(crypto);
-        if (calculatedHash != this.hash.data) {
-          logger.warn("File hash (" + calculatedHash + ") did not match provided hash (" +
-                      this.hash.data + ")");
-          this.state = AddonManager.STATE_DOWNLOAD_FAILED;
-          this.error = AddonManager.ERROR_INCORRECT_HASH;
-          XPIProvider.removeActiveInstall(this);
-          return;
-        }
-      }
-
-      try {
-        yield this.loadManifest(this.file);
-      } catch ([error, message]) {
-        logger.warn("Invalid XPI", message);
-        this.state = AddonManager.STATE_DOWNLOAD_FAILED;
-        this.error = error;
-        XPIProvider.removeActiveInstall(this);
-        AddonManagerPrivate.callInstallListeners("onNewInstall",
-                                                 this.listeners,
-                                                 this.wrapper);
-        return;
-      }
-
-      let addon = yield new Promise(resolve => {
-        XPIDatabase.getVisibleAddonForID(this.addon.id, resolve);
-      });
-
-      this.existingAddon = addon;
-      if (addon)
-        applyBlocklistChanges(addon, this.addon);
-      this.addon.updateDate = Date.now();
-      this.addon.installDate = addon ? addon.installDate : this.addon.updateDate;
-
-      if (!this.addon.isCompatible) {
-        this.state = AddonManager.STATE_CHECKING;
-
-        yield new Promise(resolve => {
-          new UpdateChecker(this.addon, {
-            onUpdateFinished: aAddon => {
-              this.state = AddonManager.STATE_DOWNLOADED;
-              AddonManagerPrivate.callInstallListeners("onNewInstall",
-                                                       this.listeners,
-                                                       this.wrapper);
-              resolve();
-            }
-          }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
-        });
-      }
-      else {
-        AddonManagerPrivate.callInstallListeners("onNewInstall",
-                                                 this.listeners,
-                                                 this.wrapper);
-
-      }
-    }).bind(this));
-  }
-}
-
-class DownloadAddonInstall extends AddonInstall {
-  /**
-   * Instantiates a DownloadAddonInstall
-   *
-   * @param  installLocation
-   *         The InstallLocation the add-on will be installed into
-   * @param  url
-   *         The nsIURL to get the add-on from
-   * @param  name
-   *         An optional name for the add-on
-   * @param  hash
-   *         An optional hash for the add-on
-   * @param  existingAddon
-   *         The add-on this install will update if known
-   * @param  browser
-   *         The browser performing the install, used to display
-   *         authentication prompts.
-   * @param  type
-   *         An optional type for the add-on
-   * @param  icons
-   *         Optional icons for the add-on
-   * @param  version
-   *         An optional version for the add-on
-   */
-  constructor(installLocation, url, hash, existingAddon, browser,
-              name, type, icons, version) {
-    super(installLocation, url, hash, existingAddon);
-
-    this.browser = browser;
-
-    this.state = AddonManager.STATE_AVAILABLE;
-    this.name = name;
-    this.type = type;
-    this.version = version;
-    this.icons = icons;
-
-    this.stream = null;
-    this.crypto = null;
-    this.badCertHandler = null;
-    this.restartDownload = false;
-
-    AddonManagerPrivate.callInstallListeners("onNewInstall", this.listeners,
-                                            this.wrapper);
-  }
-
-  install() {
-    switch (this.state) {
-    case AddonManager.STATE_AVAILABLE:
-      this.startDownload();
-      break;
-    case AddonManager.STATE_DOWNLOAD_FAILED:
-    case AddonManager.STATE_INSTALL_FAILED:
-    case AddonManager.STATE_CANCELLED:
-      this.removeTemporaryFile();
-      this.state = AddonManager.STATE_AVAILABLE;
-      this.error = 0;
-      this.progress = 0;
-      this.maxProgress = -1;
-      this.hash = this.originalHash;
-      this.startDownload();
-      break;
-    default:
-      super.install();
-    }
-  }
-
-  cancel() {
-    if (this.state == AddonManager.STATE_DOWNLOADING) {
-      if (this.channel) {
-        logger.debug("Cancelling download of " + this.sourceURI.spec);
-        this.channel.cancel(Cr.NS_BINDING_ABORTED);
-      }
-    } else {
-      super.cancel();
-    }
-  }
-
-  observe(aSubject, aTopic, aData) {
-    // Network is going offline
-    this.cancel();
-  }
-
-  /**
-   * Starts downloading the add-on's XPI file.
-   */
-  startDownload() {
-    this.state = AddonManager.STATE_DOWNLOADING;
-    if (!AddonManagerPrivate.callInstallListeners("onDownloadStarted",
-                                                  this.listeners, this.wrapper)) {
-      logger.debug("onDownloadStarted listeners cancelled installation of addon " + this.sourceURI.spec);
-      this.state = AddonManager.STATE_CANCELLED;
-      XPIProvider.removeActiveInstall(this);
-      AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
-                                               this.listeners, this.wrapper)
-      return;
-    }
-
-    // If a listener changed our state then do not proceed with the download
-    if (this.state != AddonManager.STATE_DOWNLOADING)
-      return;
-
-    if (this.channel) {
-      // A previous download attempt hasn't finished cleaning up yet, signal
-      // that it should restart when complete
-      logger.debug("Waiting for previous download to complete");
-      this.restartDownload = true;
-      return;
-    }
-
-    this.openChannel();
-  }
-
-  openChannel() {
-    this.restartDownload = false;
-
-    try {
-      this.file = getTemporaryFile();
-      this.ownsTempFile = true;
-      this.stream = Cc["@mozilla.org/network/file-output-stream;1"].
-                    createInstance(Ci.nsIFileOutputStream);
-      this.stream.init(this.file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
-                       FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE, 0);
-    }
-    catch (e) {
-      logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
-      this.state = AddonManager.STATE_DOWNLOAD_FAILED;
-      this.error = AddonManager.ERROR_FILE_ACCESS;
-      XPIProvider.removeActiveInstall(this);
-      AddonManagerPrivate.callInstallListeners("onDownloadFailed",
-                                               this.listeners, this.wrapper);
-      return;
-    }
-
-    let listener = Cc["@mozilla.org/network/stream-listener-tee;1"].
-                   createInstance(Ci.nsIStreamListenerTee);
-    listener.init(this, this.stream);
-    try {
-      let requireBuiltIn = Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true);
-      this.badCertHandler = new CertUtils.BadCertHandler(!requireBuiltIn);
-
-      this.channel = NetUtil.newChannel({
-        uri: this.sourceURI,
-        loadUsingSystemPrincipal: true
-      });
-      this.channel.notificationCallbacks = this;
-      if (this.channel instanceof Ci.nsIHttpChannel) {
-        this.channel.setRequestHeader("Moz-XPI-Update", "1", true);
-        if (this.channel instanceof Ci.nsIHttpChannelInternal)
-          this.channel.forceAllowThirdPartyCookie = true;
-      }
-      this.channel.asyncOpen2(listener);
-
-      Services.obs.addObserver(this, "network:offline-about-to-go-offline", false);
-    }
-    catch (e) {
-      logger.warn("Failed to start download for addon " + this.sourceURI.spec, e);
-      this.state = AddonManager.STATE_DOWNLOAD_FAILED;
-      this.error = AddonManager.ERROR_NETWORK_FAILURE;
-      XPIProvider.removeActiveInstall(this);
-      AddonManagerPrivate.callInstallListeners("onDownloadFailed",
-                                               this.listeners, this.wrapper);
-    }
-  }
-
-  /**
-   * Update the crypto hasher with the new data and call the progress listeners.
-   *
-   * @see nsIStreamListener
-   */
-  onDataAvailable(aRequest, aContext, aInputstream, aOffset, aCount) {
-    this.crypto.updateFromStream(aInputstream, aCount);
-    this.progress += aCount;
-    if (!AddonManagerPrivate.callInstallListeners("onDownloadProgress",
-                                                  this.listeners, this.wrapper)) {
-      // TODO cancel the download and make it available again (bug 553024)
-    }
-  }
-
-  /**
-   * Check the redirect response for a hash of the target XPI and verify that
-   * we don't end up on an insecure channel.
-   *
-   * @see nsIChannelEventSink
-   */
-  asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback) {
-    if (!this.hash && aOldChannel.originalURI.schemeIs("https") &&
-        aOldChannel instanceof Ci.nsIHttpChannel) {
-      try {
-        let hashStr = aOldChannel.getResponseHeader("X-Target-Digest");
-        let hashSplit = hashStr.toLowerCase().split(":");
-        this.hash = {
-          algorithm: hashSplit[0],
-          data: hashSplit[1]
-        };
-      }
-      catch (e) {
-      }
-    }
-
-    // Verify that we don't end up on an insecure channel if we haven't got a
-    // hash to verify with (see bug 537761 for discussion)
-    if (!this.hash)
-      this.badCertHandler.asyncOnChannelRedirect(aOldChannel, aNewChannel, aFlags, aCallback);
-    else
-      aCallback.onRedirectVerifyCallback(Cr.NS_OK);
-
-    this.channel = aNewChannel;
-  }
-
-  /**
-   * This is the first chance to get at real headers on the channel.
-   *
-   * @see nsIStreamListener
-   */
-  onStartRequest(aRequest, aContext) {
-    this.crypto = Cc["@mozilla.org/security/hash;1"].
-                  createInstance(Ci.nsICryptoHash);
-    if (this.hash) {
-      try {
-        this.crypto.initWithString(this.hash.algorithm);
-      }
-      catch (e) {
-        logger.warn("Unknown hash algorithm '" + this.hash.algorithm + "' for addon " + this.sourceURI.spec, e);
-        this.state = AddonManager.STATE_DOWNLOAD_FAILED;
-        this.error = AddonManager.ERROR_INCORRECT_HASH;
-        XPIProvider.removeActiveInstall(this);
-        AddonManagerPrivate.callInstallListeners("onDownloadFailed",
-                                                 this.listeners, this.wrapper);
-        aRequest.cancel(Cr.NS_BINDING_ABORTED);
-        return;
-      }
-    }
-    else {
-      // We always need something to consume data from the inputstream passed
-      // to onDataAvailable so just create a dummy cryptohasher to do that.
-      this.crypto.initWithString("sha1");
-    }
-
-    this.progress = 0;
-    if (aRequest instanceof Ci.nsIChannel) {
-      try {
-        this.maxProgress = aRequest.contentLength;
-      }
-      catch (e) {
-      }
-      logger.debug("Download started for " + this.sourceURI.spec + " to file " +
-          this.file.path);
-    }
-  }
-
-  /**
-   * The download is complete.
-   *
-   * @see nsIStreamListener
-   */
-  onStopRequest(aRequest, aContext, aStatus) {
-    this.stream.close();
-    this.channel = null;
-    this.badCerthandler = null;
-    Services.obs.removeObserver(this, "network:offline-about-to-go-offline");
-
-    // If the download was cancelled then update the state and send events
-    if (aStatus == Cr.NS_BINDING_ABORTED) {
-      if (this.state == AddonManager.STATE_DOWNLOADING) {
-        logger.debug("Cancelled download of " + this.sourceURI.spec);
-        this.state = AddonManager.STATE_CANCELLED;
-        XPIProvider.removeActiveInstall(this);
-        AddonManagerPrivate.callInstallListeners("onDownloadCancelled",
-                                                 this.listeners, this.wrapper);
-        // If a listener restarted the download then there is no need to
-        // remove the temporary file
-        if (this.state != AddonManager.STATE_CANCELLED)
-          return;
-      }
-
-      this.removeTemporaryFile();
-      if (this.restartDownload)
-        this.openChannel();
-      return;
-    }
-
-    logger.debug("Download of " + this.sourceURI.spec + " completed.");
-
-    if (Components.isSuccessCode(aStatus)) {
-      if (!(aRequest instanceof Ci.nsIHttpChannel) || aRequest.requestSucceeded) {
-        if (!this.hash && (aRequest instanceof Ci.nsIChannel)) {
-          try {
-            CertUtils.checkCert(aRequest,
-                                !Preferences.get(PREF_INSTALL_REQUIREBUILTINCERTS, true));
-          }
-          catch (e) {
-            this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, e);
-            return;
-          }
-        }
-
-        // convert the binary hash data to a hex string.
-        let calculatedHash = getHashStringForCrypto(this.crypto);
-        this.crypto = null;
-        if (this.hash && calculatedHash != this.hash.data) {
-          this.downloadFailed(AddonManager.ERROR_INCORRECT_HASH,
-                              "Downloaded file hash (" + calculatedHash +
-                              ") did not match provided hash (" + this.hash.data + ")");
-          return;
-        }
-
-        this.loadManifest(this.file).then(() => {
-          if (this.addon.isCompatible) {
-            this.downloadCompleted();
-          }
-          else {
-            // TODO Should we send some event here (bug 557716)?
-            this.state = AddonManager.STATE_CHECKING;
-            new UpdateChecker(this.addon, {
-              onUpdateFinished: aAddon => this.downloadCompleted(),
-            }, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
-          }
-        }, ([error, message]) => {
-          this.removeTemporaryFile();
-          this.downloadFailed(error, message);
-        });
-      }
-      else if (aRequest instanceof Ci.nsIHttpChannel) {
-        this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE,
-                            aRequest.responseStatus + " " +
-                            aRequest.responseStatusText);
-      }
-      else {
-        this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
-      }
-    }
-    else {
-      this.downloadFailed(AddonManager.ERROR_NETWORK_FAILURE, aStatus);
-    }
-  }
-
-  /**
-   * Notify listeners that the download failed.
-   *
-   * @param  aReason
-   *         Something to log about the failure
-   * @param  error
-   *         The error code to pass to the listeners
-   */
-  downloadFailed(aReason, aError) {
-    logger.warn("Download of " + this.sourceURI.spec + " failed", aError);
-    this.state = AddonManager.STATE_DOWNLOAD_FAILED;
-    this.error = aReason;
-    XPIProvider.removeActiveInstall(this);
-    AddonManagerPrivate.callInstallListeners("onDownloadFailed", this.listeners,
-                                             this.wrapper);
-
-    // If the listener hasn't restarted the download then remove any temporary
-    // file
-    if (this.state == AddonManager.STATE_DOWNLOAD_FAILED) {
-      logger.debug("downloadFailed: removing temp file for " + this.sourceURI.spec);
-      this.removeTemporaryFile();
-    }
-    else
-      logger.debug("downloadFailed: listener changed AddonInstall state for " +
-          this.sourceURI.spec + " to " + this.state);
-  }
-
-  /**
-   * Notify listeners that the download completed.
-   */
-  downloadCompleted() {
-    XPIDatabase.getVisibleAddonForID(this.addon.id, aAddon => {
-      if (aAddon)
-        this.existingAddon = aAddon;
-
-      this.state = AddonManager.STATE_DOWNLOADED;
-      this.addon.updateDate = Date.now();
-
-      if (this.existingAddon) {
-        this.addon.existingAddonID = this.existingAddon.id;
-        this.addon.installDate = this.existingAddon.installDate;
-        applyBlocklistChanges(this.existingAddon, this.addon);
-      }
-      else {
-        this.addon.installDate = this.addon.updateDate;
-      }
-
-      if (AddonManagerPrivate.callInstallListeners("onDownloadEnded",
-                                                   this.listeners,
-                                                   this.wrapper)) {
-        // If a listener changed our state then do not proceed with the install
-        if (this.state != AddonManager.STATE_DOWNLOADED)
-          return;
-
-        // If an upgrade listener is registered for this add-on, pass control
-        // over the upgrade to the add-on.
-        if (AddonManagerPrivate.hasUpgradeListener(this.addon.id)) {
-          logger.info(`add-on ${this.addon.id} has an upgrade listener, postponing upgrade until restart`);
-          let resumeFn = () => {
-            logger.info(`${this.addon.id} has resumed a previously postponed upgrade`);
-            this.state = AddonManager.STATE_DOWNLOADED;
-            this.install();
-          }
-          this.postpone(resumeFn);
-        } else {
-          // no upgrade listener present, so proceed with normal install
-          this.install();
-          if (this.linkedInstalls) {
-            for (let install of this.linkedInstalls) {
-              if (install.state == AddonManager.STATE_DOWNLOADED)
-                install.install();
-            }
-          }
-        }
-      }
-    });
-  }
-
-  getInterface(iid) {
+  unstageInstall: function*(stagedAddon) {
+    let stagedJSON = stagedAddon.clone();
+    let removedAddon = stagedAddon.clone();
+
+    stagedJSON.append(this.addon.id + ".json");
+
+    if (stagedJSON.exists()) {
+      stagedJSON.remove(true);
+    }
+
+    removedAddon.append(this.addon.id);
+    yield removeAsync(removedAddon);
+    removedAddon.leafName = this.addon.id + ".xpi";
+    yield removeAsync(removedAddon);
+  },
+
+  getInterface: function(iid) {
     if (iid.equals(Ci.nsIAuthPrompt2)) {
-      let win = null;
-      if (this.browser) {
-        win = this.browser.contentWindow || this.browser.ownerDocument.defaultView;
-      }
+      let win = this.window;
+      if (!win && this.browser)
+        win = this.browser.ownerDocument.defaultView;
 
       let factory = Cc["@mozilla.org/prompter;1"].
                     getService(Ci.nsIPromptFactory);
       let prompt = factory.getPrompt(win, Ci.nsIAuthPrompt2);
 
       if (this.browser && prompt instanceof Ci.nsILoginManagerPrompter)
         prompt.browser = this.browser;
 
       return prompt;
     }
     else if (iid.equals(Ci.nsIChannelEventSink)) {
       return this;
     }
 
     return this.badCertHandler.getInterface(iid);
-  }
+  },
 
   /**
     * Postone a pending update, until restart or until the add-on resumes.
     *
     * @param {Function} resumeFn - a function for the add-on to run
     *                                    when resuming.
     */
-  postpone(resumeFn) {
-    return Task.spawn((function*() {
-      this.state = AddonManager.STATE_POSTPONED;
-
-      let stagingDir = this.installLocation.getStagingDir();
-      let stagedAddon = stagingDir.clone();
-
-      yield this.installLocation.requestStagingDir();
-      yield this.unstageInstall(stagedAddon);
-
-      stagedAddon.append(this.addon.id);
-      stagedAddon.leafName = this.addon.id + ".xpi";
-
-      yield this.stageInstall(true, stagedAddon, true);
-
-      AddonManagerPrivate.callInstallListeners("onInstallPostponed",
-                                               this.listeners, this.wrapper)
-
-      // upgrade has been staged for restart, provide a way for it to call the
-      // resume function.
-      let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id);
-      if (callback) {
-        callback({
-          version: this.version,
-          install: () => {
-            switch (this.state) {
+  postpone: Task.async(function*(resumeFn) {
+    this.state = AddonManager.STATE_POSTPONED;
+
+    let stagingDir = this.installLocation.getStagingDir();
+    let stagedAddon = stagingDir.clone();
+
+    yield this.installLocation.requestStagingDir();
+    yield this.unstageInstall(stagedAddon);
+
+    stagedAddon.append(this.addon.id);
+    stagedAddon.leafName = this.addon.id + ".xpi";
+
+    yield this.stageInstall(true, stagedAddon, true);
+
+    AddonManagerPrivate.callInstallListeners("onInstallPostponed",
+                                             this.listeners, this.wrapper)
+
+    // upgrade has been staged for restart, provide a way for it to call the
+    // resume function.
+    let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id);
+    if (callback) {
+      callback({
+        version: this.version,
+        install: () => {
+          switch (this.state) {
             case AddonManager.STATE_POSTPONED:
               if (resumeFn) {
                 resumeFn();
               }
               break;
             default:
               logger.warn(`${this.addon.id} cannot resume postponed upgrade from state (${this.state})`);
               break;
-            }
-          },
-        });
-      }
-      // Release the staging directory lock, but since the staging dir is populated
-      // it will not be removed until resumed or installed by restart.
-      // See also cleanStagingDir()
-      this.installLocation.releaseStagingDir();
-    }).bind(this));
+          }
+        },
+      });
+    }
+    // Release the staging directory lock, but since the staging dir is populated
+    // it will not be removed until resumed or installed by restart.
+    // See also cleanStagingDir()
+    this.installLocation.releaseStagingDir();
   }
-}
+)}
 
 /**
- * This class exists just for the specific case of staged add-ons that
- * fail to install at startup.  When that happens, the add-on remains
- * staged but we want to keep track of it like other installs so that we
- * can clean it up if the same add-on is installed again (see the comment
- * about "pending installs for the same add-on" in AddonInstall.startInstall)
+ * Creates a new AddonInstall for an already staged install. Used when
+ * installing the staged install failed for some reason.
+ * @param  aInstallLocation
+ *         The install location holding the staged install.
+ * @param  aDir
+ *         The directory holding the staged install
+ * @param  aManifest
+ *         The cached manifest for the install
  */
-class StagedAddonInstall extends AddonInstall {
-  constructor(installLocation, dir, manifest) {
-    super(installLocation, dir);
-
-    this.name = manifest.name;
-    this.type = manifest.type;
-    this.version = manifest.version;
-    this.icons = manifest.icons;
-    this.releaseNotesURI = manifest.releaseNotesURI ?
-                           NetUtil.newURI(manifest.releaseNotesURI) :
-                           null;
-    this.sourceURI = manifest.sourceURI ?
-                     NetUtil.newURI(manifest.sourceURI) :
-                     null;
-    this.file = null;
-    this.addon = manifest;
-
-    this.state = AddonManager.STATE_INSTALLED;
-  }
-}
+AddonInstall.createStagedInstall = function(aInstallLocation, aDir, aManifest) {
+  let url = Services.io.newFileURI(aDir);
+
+  let install = new AddonInstall(aInstallLocation, aDir);
+
+  install.initStagedInstall(aManifest);
+};
 
 /**
  * Creates a new AddonInstall to install an add-on from a local file.
  *
- * @param  file
+ * @param  aCallback
+ *         The callback to pass the new AddonInstall to
+ * @param  aFile
  *         The file to install
- * @param  location
+ * @param  aLocation
  *         The location to install to
- * @returns Promise
- *          A Promise that resolves with the new install object.
  */
-function createLocalInstall(file, location) {
-  if (!location) {
-    location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+AddonInstall.createInstall = function(aCallback, aFile, aLocation = undefined) {
+  if (!aLocation) {
+    aLocation = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
   }
-  let url = Services.io.newFileURI(file);
+  let url = Services.io.newFileURI(aFile);
 
   try {
-    let install = new LocalAddonInstall(location, url);
-    return install.init().then(() => install);
+    let install = new AddonInstall(aLocation, url);
+    install.initLocalInstall(aCallback);
   }
   catch (e) {
     logger.error("Error creating install", e);
-    XPIProvider.removeActiveInstall(this);
-    return Promise.resolve(null);
+    makeSafe(aCallback)(null);
   }
-}
+};
 
 /**
  * Creates a new AddonInstall to download and install a URL.
  *
  * @param  aCallback
  *         The callback to pass the new AddonInstall to
  * @param  aUri
  *         The URI to download
@@ -6721,68 +6688,59 @@ function createLocalInstall(file, locati
  *         A name for the add-on
  * @param  aIcons
  *         An icon URLs for the add-on
  * @param  aVersion
  *         A version for the add-on
  * @param  aBrowser
  *         The browser performing the install
  */
-function createDownloadInstall(aCallback, aUri, aHash, aName, aIcons,
-                               aVersion, aBrowser) {
+AddonInstall.createDownload = function(aCallback, aUri, aHash, aName, aIcons,
+                                       aVersion, aBrowser) {
   let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
   let url = NetUtil.newURI(aUri);
 
-  if (url instanceof Ci.nsIFileURL) {
-    let install = new LocalAddonInstall(location, url, aHash);
-    install.init().then(() => { aCallback(install); });
-  } else {
-    let install = new DownloadAddonInstall(location, url, aHash, null,
-                                           aBrowser, aName, null, aIcons,
-                                           aVersion);
-    aCallback(install);
-  }
-}
+  let install = new AddonInstall(location, url, aHash, null, null, aBrowser);
+  if (url instanceof Ci.nsIFileURL)
+    install.initLocalInstall(aCallback);
+  else
+    install.initAvailableDownload(aName, null, aIcons, aVersion, aCallback);
+};
 
 /**
  * Creates a new AddonInstall for an update.
  *
  * @param  aCallback
  *         The callback to pass the new AddonInstall to
  * @param  aAddon
  *         The add-on being updated
  * @param  aUpdate
  *         The metadata about the new version from the update manifest
  */
-function createUpdate(aCallback, aAddon, aUpdate) {
+AddonInstall.createUpdate = function(aCallback, aAddon, aUpdate) {
   let url = NetUtil.newURI(aUpdate.updateURL);
-
-  Task.spawn(function*() {
-    let install;
-    if (url instanceof Ci.nsIFileURL) {
-      install = new LocalAddonInstall(aAddon._installLocation, url,
-                                      aUpdate.updateHash, aAddon);
-      yield install.init();
-    } else {
-      install = new DownloadAddonInstall(aAddon._installLocation, url,
-                                         aUpdate.updateHash, aAddon, null,
-                                         aAddon.selectedLocale.name, aAddon.type,
-                                         aAddon.icons, aUpdate.version);
-    }
-    try {
-      if (aUpdate.updateInfoURL)
-        install.releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
-    }
-    catch (e) {
-      // If the releaseNotesURI cannot be parsed then just ignore it.
-    }
-
-    aCallback(install);
-  });
-}
+  let releaseNotesURI = null;
+  try {
+    if (aUpdate.updateInfoURL)
+      releaseNotesURI = NetUtil.newURI(escapeAddonURI(aAddon, aUpdate.updateInfoURL));
+  }
+  catch (e) {
+    // If the releaseNotesURI cannot be parsed then just ignore it.
+  }
+
+  let install = new AddonInstall(aAddon._installLocation, url,
+                                 aUpdate.updateHash, releaseNotesURI, aAddon);
+  if (url instanceof Ci.nsIFileURL) {
+    install.initLocalInstall(aCallback);
+  }
+  else {
+    install.initAvailableDownload(aAddon.selectedLocale.name, aAddon.type,
+                                  aAddon.icons, aUpdate.version, aCallback);
+  }
+};
 
 // This map is shared between AddonInstallWrapper and AddonWrapper
 const wrapperMap = new WeakMap();
 let installFor = wrapper => wrapperMap.get(wrapper);
 let addonFor = installFor;
 
 /**
  * Creates a wrapper for an AddonInstall that only exposes the public API
@@ -7022,17 +6980,17 @@ UpdateChecker.prototype = {
           logger.debug("Found an existing AddonInstall for " + this.addon.id);
           sendUpdateAvailableMessages(this, currentInstall);
         }
         else
           sendUpdateAvailableMessages(this, null);
         return;
       }
 
-      createUpdate(aInstall => {
+      AddonInstall.createUpdate(aInstall => {
         sendUpdateAvailableMessages(this, aInstall);
       }, this.addon, update);
     }
     else {
       sendUpdateAvailableMessages(this, null);
     }
   },
 
@@ -8950,17 +8908,19 @@ Object.assign(SystemAddonInstallLocation
     let state = { schema: 1, directory: newDir.leafName, addons: {} };
     this._saveAddonSet(state);
 
     this._nextDir = newDir;
     let location = this;
 
     let installs = [];
     for (let addon of aAddons) {
-      let install = yield createLocalInstall(addon._sourceBundle, location);
+      let install = yield new Promise(resolve => {
+        AddonInstall.createInstall(resolve, addon._sourceBundle, location);
+      });
       installs.push(install);
     }
 
     let installAddon = Task.async(function*(install) {
       // Make the new install own its temporary file.
       install.ownsTempFile = true;
       install.install();
     });