Backed out 2 changesets (bug 1315407) for test-addon-installer.js.test and browser_bug553455.js failures a=backout
authorWes Kocher <wkocher@mozilla.com>
Thu, 10 Nov 2016 23:36:48 -0800
changeset 352065 8b8c3c3b29c93c0efb810b682bca08364164f960
parent 352064 2a0faaf8a4ffa86bd72cf437cb890e0271964121
child 352066 0b58909e7626c6c36aa00fa52cab7143205e912f
push id6795
push userjlund@mozilla.com
push dateMon, 23 Jan 2017 14:19:46 +0000
treeherdermozilla-esr52@76101b503191 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbackout
bugs1315407, 553455
milestone52.0a1
backs out7f747c8f6c60d87ab282d79899e9f10655d8ec82
95ef280ccc1bafebc7bdcbf47c399713195e5b62
Backed out 2 changesets (bug 1315407) for test-addon-installer.js.test and browser_bug553455.js failures a=backout Backed out changeset 7f747c8f6c60 (bug 1315407) Backed out changeset 95ef280ccc1b (bug 1315407)
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- 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();
     });