Bug 1599360 - Factor out appUpdater from aboutDialog-appUpdater.js into a jsm module r=rstrong a=jcristau
authorDrew Willcoxon <adw@mozilla.com>
Tue, 03 Dec 2019 06:01:32 +0000
changeset 566681 849518df51c9527b9d8bdcd35eadcd5a6fec532f
parent 566680 ee5eb4cfe6414eeea88d2bf7b973b01333de6ae2
child 566682 76bfe47a96892f087cc63b184d39e9ff03a573f4
push id12373
push userccoroiu@mozilla.com
push dateWed, 04 Dec 2019 10:47:12 +0000
treeherdermozilla-beta@b69378d81dcf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrstrong, jcristau
bugs1599360
milestone72.0
Bug 1599360 - Factor out appUpdater from aboutDialog-appUpdater.js into a jsm module r=rstrong a=jcristau Differential Revision: https://phabricator.services.mozilla.com/D54677
browser/modules/AppUpdater.jsm
browser/modules/moz.build
copy from browser/base/content/aboutDialog-appUpdater.js
copy to browser/modules/AppUpdater.jsm
--- a/browser/base/content/aboutDialog-appUpdater.js
+++ b/browser/modules/AppUpdater.jsm
@@ -1,512 +1,508 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-// Note: this file is included in aboutDialog.xul and preferences/advanced.xhtml
-// if MOZ_UPDATER is defined.
+"use strict";
 
-/* import-globals-from aboutDialog.js */
+var EXPORTED_SYMBOLS = ["AppUpdater"];
 
 var { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
-
-ChromeUtils.defineModuleGetter(
-  this,
-  "DownloadUtils",
-  "resource://gre/modules/DownloadUtils.jsm"
-);
-ChromeUtils.defineModuleGetter(
-  this,
-  "UpdateUtils",
-  "resource://gre/modules/UpdateUtils.jsm"
-);
+XPCOMUtils.defineLazyModuleGetters(this, {
+  AppConstants: "resource://gre/modules/AppConstants.jsm",
+  Services: "resource://gre/modules/Services.jsm",
+  UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
+});
 
 const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
 const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never";
 
-var gAppUpdater;
-
-function onUnload(aEvent) {
-  if (gAppUpdater) {
-    if (gAppUpdater.isChecking) {
-      gAppUpdater.checker.stopCurrentCheck();
-    }
-    // Safe to call even when there isn't a download in progress.
-    gAppUpdater.removeDownloadListener();
-    gAppUpdater = null;
-  }
-}
-
-function appUpdater(options = {}) {
-  XPCOMUtils.defineLazyServiceGetter(
-    this,
-    "aus",
-    "@mozilla.org/updates/update-service;1",
-    "nsIApplicationUpdateService"
-  );
-  XPCOMUtils.defineLazyServiceGetter(
-    this,
-    "checker",
-    "@mozilla.org/updates/update-checker;1",
-    "nsIUpdateChecker"
-  );
-  XPCOMUtils.defineLazyServiceGetter(
-    this,
-    "um",
-    "@mozilla.org/updates/update-manager;1",
-    "nsIUpdateManager"
-  );
-
-  this.options = options;
-  this.updateDeck = document.getElementById("updateDeck");
-  this.promiseAutoUpdateSetting = null;
-
-  this.bundle = Services.strings.createBundle(
-    "chrome://browser/locale/browser.properties"
-  );
-
-  let manualURL = Services.urlFormatter.formatURLPref("app.update.url.manual");
-  let manualLink = document.getElementById("manualLink");
-  manualLink.textContent = manualURL;
-  manualLink.href = manualURL;
-  document.getElementById("failedLink").href = manualURL;
-
-  if (this.updateDisabledByPolicy) {
-    this.selectPanel("policyDisabled");
-    return;
+/**
+ * This class checks for app updates in the foreground.  It has several public
+ * methods for checking for updates, downloading updates, stopping the current
+ * update, and getting the current update status.  It can also register
+ * listeners that will be called back as different stages of updates occur.
+ */
+class AppUpdater {
+  constructor() {
+    this._listeners = new Set();
+    XPCOMUtils.defineLazyServiceGetter(
+      this,
+      "aus",
+      "@mozilla.org/updates/update-service;1",
+      "nsIApplicationUpdateService"
+    );
+    XPCOMUtils.defineLazyServiceGetter(
+      this,
+      "checker",
+      "@mozilla.org/updates/update-checker;1",
+      "nsIUpdateChecker"
+    );
+    XPCOMUtils.defineLazyServiceGetter(
+      this,
+      "um",
+      "@mozilla.org/updates/update-manager;1",
+      "nsIUpdateManager"
+    );
   }
 
-  if (this.isReadyForRestart) {
-    this.selectPanel("apply");
-    return;
-  }
+  /**
+   * The main entry point for checking for updates.  As different stages of the
+   * check and possible subsequent update occur, the updater's status is set and
+   * listeners are called.
+   */
+  check() {
+    if (!AppConstants.MOZ_UPDATER) {
+      this._setStatus(AppUpdater.STATUS.NO_UPDATER);
+      return;
+    }
 
-  if (this.aus.isOtherInstanceHandlingUpdates) {
-    this.selectPanel("otherInstanceHandlingUpdates");
-    return;
-  }
+    if (this.updateDisabledByPolicy) {
+      this._setStatus(AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY);
+      return;
+    }
 
-  if (this.isDownloading) {
-    this.startDownload();
-    // selectPanel("downloading") is called from setupDownloadingUI().
-    return;
-  }
+    if (this.isReadyForRestart) {
+      this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
+      return;
+    }
 
-  if (this.isStaging) {
-    this.waitForUpdateToStage();
-    // selectPanel("applying"); is called from waitForUpdateToStage().
-    return;
-  }
+    if (this.aus.isOtherInstanceHandlingUpdates) {
+      this._setStatus(AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES);
+      return;
+    }
 
-  // We might need this value later, so start loading it from the disk now.
-  this.promiseAutoUpdateSetting = UpdateUtils.getAppUpdateAutoEnabled();
+    if (this.isDownloading) {
+      this.startDownload();
+      return;
+    }
 
-  // That leaves the options
-  // "Check for updates, but let me choose whether to install them", and
-  // "Automatically install updates".
-  // In both cases, we check for updates without asking.
-  // In the "let me choose" case, we ask before downloading though, in onCheckComplete.
-  this.checkForUpdates();
-}
+    if (this.isStaging) {
+      this._waitForUpdateToStage();
+      return;
+    }
+
+    // We might need this value later, so start loading it from the disk now.
+    this.promiseAutoUpdateSetting = UpdateUtils.getAppUpdateAutoEnabled();
 
-appUpdater.prototype = {
-  // true when there is an update check in progress.
-  isChecking: false,
+    // That leaves the options
+    // "Check for updates, but let me choose whether to install them", and
+    // "Automatically install updates".
+    // In both cases, we check for updates without asking.
+    // In the "let me choose" case, we ask before downloading though, in onCheckComplete.
+    this.checkForUpdates();
+  }
 
   // true when there is an update ready to be applied on restart or staged.
   get isPending() {
     if (this.update) {
       return (
         this.update.state == "pending" ||
         this.update.state == "pending-service" ||
         this.update.state == "pending-elevate"
       );
     }
     return (
       this.um.activeUpdate &&
       (this.um.activeUpdate.state == "pending" ||
         this.um.activeUpdate.state == "pending-service" ||
         this.um.activeUpdate.state == "pending-elevate")
     );
-  },
+  }
 
   // true when there is an update already staged.
   get isApplied() {
     if (this.update) {
       return (
         this.update.state == "applied" || this.update.state == "applied-service"
       );
     }
     return (
       this.um.activeUpdate &&
       (this.um.activeUpdate.state == "applied" ||
         this.um.activeUpdate.state == "applied-service")
     );
-  },
+  }
 
   get isStaging() {
     if (!this.updateStagingEnabled) {
       return false;
     }
     let errorCode;
     if (this.update) {
       errorCode = this.update.errorCode;
     } else if (this.um.activeUpdate) {
       errorCode = this.um.activeUpdate.errorCode;
     }
     // If the state is pending and the error code is not 0, staging must have
     // failed.
     return this.isPending && errorCode == 0;
-  },
+  }
 
   // true when an update ready to restart to finish the update process.
   get isReadyForRestart() {
     if (this.updateStagingEnabled) {
       let errorCode;
       if (this.update) {
         errorCode = this.update.errorCode;
       } else if (this.um.activeUpdate) {
         errorCode = this.um.activeUpdate.errorCode;
       }
       // If the state is pending and the error code is not 0, staging must have
       // failed and Firefox should be restarted to try to apply the update
       // without staging.
       return this.isApplied || (this.isPending && errorCode != 0);
     }
     return this.isPending;
-  },
+  }
 
   // true when there is an update download in progress.
   get isDownloading() {
     if (this.update) {
       return this.update.state == "downloading";
     }
     return this.um.activeUpdate && this.um.activeUpdate.state == "downloading";
-  },
+  }
 
   // true when updating has been disabled by enterprise policy
   get updateDisabledByPolicy() {
     return Services.policies && !Services.policies.isAllowed("appUpdate");
-  },
+  }
 
   // true when updating in background is enabled.
   get updateStagingEnabled() {
     return !this.updateDisabledByPolicy && this.aus.canStageUpdates;
-  },
-
-  /**
-   * Sets the panel of the updateDeck.
-   *
-   * @param  aChildID
-   *         The id of the deck's child to select, e.g. "apply".
-   */
-  selectPanel(aChildID) {
-    let panel = document.getElementById(aChildID);
-
-    let button = panel.querySelector("button");
-    if (button) {
-      if (aChildID == "downloadAndInstall") {
-        let updateVersion = gAppUpdater.update.displayVersion;
-        // Include the build ID if this is an "a#" (nightly or aurora) build
-        if (/a\d+$/.test(updateVersion)) {
-          let buildID = gAppUpdater.update.buildID;
-          let year = buildID.slice(0, 4);
-          let month = buildID.slice(4, 6);
-          let day = buildID.slice(6, 8);
-          updateVersion += ` (${year}-${month}-${day})`;
-        }
-        button.label = this.bundle.formatStringFromName(
-          "update.downloadAndInstallButton.label",
-          [updateVersion]
-        );
-        button.accessKey = this.bundle.GetStringFromName(
-          "update.downloadAndInstallButton.accesskey"
-        );
-      }
-      this.updateDeck.selectedPanel = panel;
-      if (
-        this.options.buttonAutoFocus &&
-        (!document.commandDispatcher.focusedElement || // don't steal the focus
-          document.commandDispatcher.focusedElement.localName == "button")
-      ) {
-        // except from the other buttons
-        button.focus();
-      }
-    } else {
-      this.updateDeck.selectedPanel = panel;
-    }
-  },
+  }
 
   /**
    * Check for updates
    */
   checkForUpdates() {
     // Clear prefs that could prevent a user from discovering available updates.
     if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) {
       Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
     }
     if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
       Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
     }
-    this.selectPanel("checkingForUpdates");
-    this.isChecking = true;
-    this.checker.checkForUpdates(this.updateCheckListener, true);
+    this._setStatus(AppUpdater.STATUS.CHECKING);
+    this.checker.checkForUpdates(this._updateCheckListener, true);
     // after checking, onCheckComplete() is called
-  },
-
-  /**
-   * Handles oncommand for the "Restart to Update" button
-   * which is presented after the download has been downloaded.
-   */
-  buttonRestartAfterDownload() {
-    if (!this.isReadyForRestart) {
-      return;
-    }
-
-    gAppUpdater.selectPanel("restarting");
-
-    // Notify all windows that an application quit has been requested.
-    let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
-      Ci.nsISupportsPRBool
-    );
-    Services.obs.notifyObservers(
-      cancelQuit,
-      "quit-application-requested",
-      "restart"
-    );
-
-    // Something aborted the quit process.
-    if (cancelQuit.data) {
-      gAppUpdater.selectPanel("apply");
-      return;
-    }
-
-    // If already in safe mode restart in safe mode (bug 327119)
-    if (Services.appinfo.inSafeMode) {
-      Services.startup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit);
-      return;
-    }
-
-    Services.startup.quit(
-      Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart
-    );
-  },
+  }
 
   /**
    * Implements nsIUpdateCheckListener. The methods implemented by
    * nsIUpdateCheckListener are in a different scope from nsIIncrementalDownload
    * to make it clear which are used by each interface.
    */
-  updateCheckListener: {
-    /**
-     * See nsIUpdateService.idl
-     */
-    onCheckComplete(aRequest, aUpdates) {
-      gAppUpdater.isChecking = false;
-      gAppUpdater.update = gAppUpdater.aus.selectUpdate(aUpdates);
-      if (!gAppUpdater.update) {
-        gAppUpdater.selectPanel("noUpdatesFound");
-        return;
-      }
+  get _updateCheckListener() {
+    if (!this.__updateCheckListener) {
+      this.__updateCheckListener = {
+        /**
+         * See nsIUpdateService.idl
+         */
+        onCheckComplete: (aRequest, aUpdates) => {
+          this.update = this.aus.selectUpdate(aUpdates);
+          if (!this.update) {
+            this._setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND);
+            return;
+          }
 
-      if (gAppUpdater.update.unsupported) {
-        if (gAppUpdater.update.detailsURL) {
-          let unsupportedLink = document.getElementById("unsupportedLink");
-          unsupportedLink.href = gAppUpdater.update.detailsURL;
-        }
-        gAppUpdater.selectPanel("unsupportedSystem");
-        return;
-      }
+          if (this.update.unsupported) {
+            this._setStatus(AppUpdater.STATUS.UNSUPPORTED_SYSTEM);
+            return;
+          }
 
-      if (!gAppUpdater.aus.canApplyUpdates) {
-        gAppUpdater.selectPanel("manualUpdate");
-        return;
-      }
+          if (!this.aus.canApplyUpdates) {
+            this._setStatus(AppUpdater.STATUS.MANUAL_UPDATE);
+            return;
+          }
 
-      if (!gAppUpdater.promiseAutoUpdateSetting) {
-        gAppUpdater.promiseAutoUpdateSetting = UpdateUtils.getAppUpdateAutoEnabled();
-      }
-      gAppUpdater.promiseAutoUpdateSetting.then(updateAuto => {
-        if (updateAuto) {
-          // automatically download and install
-          gAppUpdater.startDownload();
-        } else {
-          // ask
-          gAppUpdater.selectPanel("downloadAndInstall");
-        }
-      });
-    },
+          if (!this.promiseAutoUpdateSetting) {
+            this.promiseAutoUpdateSetting = UpdateUtils.getAppUpdateAutoEnabled();
+          }
+          this.promiseAutoUpdateSetting.then(updateAuto => {
+            if (updateAuto) {
+              // automatically download and install
+              this.startDownload();
+            } else {
+              // ask
+              this._setStatus(AppUpdater.STATUS.DOWNLOAD_AND_INSTALL);
+            }
+          });
+        },
 
-    /**
-     * See nsIUpdateService.idl
-     */
-    onError(aRequest, aUpdate) {
-      // Errors in the update check are treated as no updates found. If the
-      // update check fails repeatedly without a success the user will be
-      // notified with the normal app update user interface so this is safe.
-      gAppUpdater.isChecking = false;
-      gAppUpdater.selectPanel("noUpdatesFound");
-    },
+        /**
+         * See nsIUpdateService.idl
+         */
+        onError: (aRequest, aUpdate) => {
+          // Errors in the update check are treated as no updates found. If the
+          // update check fails repeatedly without a success the user will be
+          // notified with the normal app update user interface so this is safe.
+          this._setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND);
+        },
 
-    /**
-     * See nsISupports.idl
-     */
-    QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckListener"]),
-  },
+        /**
+         * See nsISupports.idl
+         */
+        QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckListener"]),
+      };
+    }
+    return this.__updateCheckListener;
+  }
 
   /**
-   * Shows the applying UI until the update has finished staging
+   * Sets the status to STAGING.  The status will then be set again when the
+   * update finishes staging.
    */
-  waitForUpdateToStage() {
+  _waitForUpdateToStage() {
     if (!this.update) {
       this.update = this.um.activeUpdate;
     }
     this.update.QueryInterface(Ci.nsIWritablePropertyBag);
     this.update.setProperty("foregroundDownload", "true");
-    this.selectPanel("applying");
-    this.updateUIWhenStagingComplete();
-  },
+    this._setStatus(AppUpdater.STATUS.STAGING);
+    this._awaitStagingComplete();
+  }
 
   /**
    * Starts the download of an update mar.
    */
   startDownload() {
     if (!this.update) {
       this.update = this.um.activeUpdate;
     }
     this.update.QueryInterface(Ci.nsIWritablePropertyBag);
     this.update.setProperty("foregroundDownload", "true");
 
     let state = this.aus.downloadUpdate(this.update, false);
     if (state == "failed") {
-      this.selectPanel("downloadFailed");
+      this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
       return;
     }
 
-    this.setupDownloadingUI();
-  },
+    this._setupDownloadListener();
+  }
 
   /**
-   * Switches to the UI responsible for tracking the download.
+   * Starts tracking the download.
    */
-  setupDownloadingUI() {
-    this.downloadStatus = document.getElementById("downloadStatus");
-    this.downloadStatus.textContent = DownloadUtils.getTransferTotal(
-      0,
-      this.update.selectedPatch.size
-    );
-    this.selectPanel("downloading");
+  _setupDownloadListener() {
+    this._setStatus(AppUpdater.STATUS.DOWNLOADING);
     this.aus.addDownloadListener(this);
-  },
-
-  removeDownloadListener() {
-    if (this.aus) {
-      this.aus.removeDownloadListener(this);
-    }
-  },
+  }
 
   /**
    * See nsIRequestObserver.idl
    */
-  onStartRequest(aRequest) {},
+  onStartRequest(aRequest) {}
 
   /**
    * See nsIRequestObserver.idl
    */
   onStopRequest(aRequest, aStatusCode) {
     switch (aStatusCode) {
       case Cr.NS_ERROR_UNEXPECTED:
         if (
           this.update.selectedPatch.state == "download-failed" &&
           (this.update.isCompleteUpdate || this.update.patchCount != 2)
         ) {
           // Verification error of complete patch, informational text is held in
           // the update object.
-          this.removeDownloadListener();
-          this.selectPanel("downloadFailed");
+          this.aus.removeDownloadListener(this);
+          this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
           break;
         }
         // Verification failed for a partial patch, complete patch is now
         // downloading so return early and do NOT remove the download listener!
         break;
       case Cr.NS_BINDING_ABORTED:
         // Do not remove UI listener since the user may resume downloading again.
         break;
       case Cr.NS_OK:
-        this.removeDownloadListener();
+        this.aus.removeDownloadListener(this);
         if (this.updateStagingEnabled) {
-          this.selectPanel("applying");
-          this.updateUIWhenStagingComplete();
+          this._setStatus(AppUpdater.STATUS.STAGING);
+          this._awaitStagingComplete();
         } else {
-          this.selectPanel("apply");
+          this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
         }
         break;
       default:
-        this.removeDownloadListener();
-        this.selectPanel("downloadFailed");
+        this.aus.removeDownloadListener(this);
+        this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
         break;
     }
-  },
+  }
 
   /**
    * See nsIProgressEventSink.idl
    */
-  onStatus(aRequest, aContext, aStatus, aStatusArg) {},
+  onStatus(aRequest, aContext, aStatus, aStatusArg) {}
 
   /**
    * See nsIProgressEventSink.idl
    */
   onProgress(aRequest, aContext, aProgress, aProgressMax) {
-    this.downloadStatus.textContent = DownloadUtils.getTransferTotal(
-      aProgress,
-      aProgressMax
-    );
-  },
+    this._setStatus(AppUpdater.STATUS.DOWNLOADING, aProgress, aProgressMax);
+  }
 
   /**
    * This function registers an observer that watches for the staging process
-   * to complete. Once it does, it updates the UI to either request that the
+   * to complete. Once it does, it sets the status to either request that the
    * user restarts to install the update on success, request that the user
    * manually download and install the newer version, or automatically download
    * a complete update if applicable.
    */
-  updateUIWhenStagingComplete() {
+  _awaitStagingComplete() {
     let observer = (aSubject, aTopic, aData) => {
       // Update the UI when the background updater is finished
       let status = aData;
       if (
         status == "applied" ||
         status == "applied-service" ||
         status == "pending" ||
         status == "pending-service" ||
         status == "pending-elevate"
       ) {
         // If the update is successfully applied, or if the updater has
         // fallen back to non-staged updates, show the "Restart to Update"
         // button.
-        this.selectPanel("apply");
+        this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
       } else if (status == "failed") {
         // Background update has failed, let's show the UI responsible for
         // prompting the user to update manually.
-        this.selectPanel("downloadFailed");
+        this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
       } else if (status == "downloading") {
         // We've fallen back to downloading the complete update because the
         // partial update failed to get staged in the background.
         // Therefore we need to keep our observer.
-        this.setupDownloadingUI();
+        this._setupDownloadListener();
         return;
       }
       Services.obs.removeObserver(observer, "update-staged");
     };
     Services.obs.addObserver(observer, "update-staged");
-  },
+  }
+
+  /**
+   * Stops the current check for updates and any ongoing download.
+   */
+  stop() {
+    this.checker.stopCurrentCheck();
+    this.aus.removeDownloadListener(this);
+  }
+
+  /**
+   * {AppUpdater.STATUS} The status of the current check or update.
+   */
+  get status() {
+    if (!this._status) {
+      if (!AppConstants.MOZ_UPDATER) {
+        this._status = AppUpdater.STATUS.NO_UPDATER;
+      } else if (this.updateDisabledByPolicy) {
+        this._status = AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY;
+      } else if (this.isReadyForRestart) {
+        this._status = AppUpdater.STATUS.READY_FOR_RESTART;
+      } else if (this.aus.isOtherInstanceHandlingUpdates) {
+        this._status = AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES;
+      } else if (this.isDownloading) {
+        this._status = AppUpdater.STATUS.DOWNLOADING;
+      } else if (this.isStaging) {
+        this._status = AppUpdater.STATUS.STAGING;
+      } else {
+        this._status = AppUpdater.STATUS.NEVER_CHECKED;
+      }
+    }
+    return this._status;
+  }
+
+  /**
+   * Adds a listener function that will be called back on status changes as
+   * different stages of updates occur.  The function will be called without
+   * arguments for most status changes; see the comments around the STATUS value
+   * definitions below.  This is safe to call multiple times with the same
+   * function.  It will be added only once.
+   *
+   * @param {function} listener
+   *   The listener function to add.
+   */
+  addListener(listener) {
+    this._listeners.add(listener);
+  }
+
+  /**
+   * Removes a listener.  This is safe to call multiple times with the same
+   * function, or with a function that was never added.
+   *
+   * @param {function} listener
+   *   The listener function to remove.
+   */
+  removeListener(listener) {
+    this._listeners.delete(listener);
+  }
 
   /**
-   * See nsISupports.idl
+   * Sets the updater's current status and calls listeners.
+   *
+   * @param {AppUpdater.STATUS} status
+   *   The new updater status.
+   * @param {*} listenerArgs
+   *   Arguments to pass to listeners.
    */
-  QueryInterface: ChromeUtils.generateQI([
-    "nsIProgressEventSink",
-    "nsIRequestObserver",
-  ]),
+  _setStatus(status, ...listenerArgs) {
+    this._status = status;
+    for (let listener of this._listeners) {
+      listener(status, ...listenerArgs);
+    }
+    return status;
+  }
+}
+
+AppUpdater.STATUS = {
+  // Updates are allowed and there's no downloaded or staged update, but the
+  // AppUpdater hasn't checked for updates yet, so it doesn't know more than
+  // that.
+  NEVER_CHECKED: 0,
+
+  // The updater isn't available (AppConstants.MOZ_UPDATER is falsey).
+  NO_UPDATER: 1,
+
+  // "appUpdate" is not allowed by policy.
+  UPDATE_DISABLED_BY_POLICY: 2,
+
+  // Another app instance is handling updates.
+  OTHER_INSTANCE_HANDLING_UPDATES: 3,
+
+  // There's an update, but it's not supported on this system.
+  UNSUPPORTED_SYSTEM: 4,
+
+  // The user must apply updates manually.
+  MANUAL_UPDATE: 5,
+
+  // The AppUpdater is checking for updates.
+  CHECKING: 6,
+
+  // The AppUpdater checked for updates and none were found.
+  NO_UPDATES_FOUND: 7,
+
+  // The AppUpdater is downloading an update.  Listeners are notified of this
+  // status as a download starts.  They are also notified on download progress,
+  // and in that case they are passed two arguments: the current download
+  // progress and the total download size.
+  DOWNLOADING: 8,
+
+  // The AppUpdater tried to download an update but it failed.
+  DOWNLOAD_FAILED: 9,
+
+  // There's an update available, but the user wants us to ask them to download
+  // and install it.
+  DOWNLOAD_AND_INSTALL: 10,
+
+  // An update is staging.
+  STAGING: 11,
+
+  // An update is downloaded and staged and will be applied on restart.
+  READY_FOR_RESTART: 12,
 };
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -128,16 +128,17 @@ with Files("ZoomUI.jsm"):
 BROWSER_CHROME_MANIFESTS += [
     'test/browser/browser.ini',
     'test/browser/formValidation/browser.ini',
 ]
 XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
 
 EXTRA_JS_MODULES += [
     'AboutNewTab.jsm',
+    'AppUpdater.jsm',
     'AsyncTabSwitcher.jsm',
     'BrowserUsageTelemetry.jsm',
     'BrowserWindowTracker.jsm',
     'ContentCrashHandlers.jsm',
     'ContentObservers.js',
     'ContentSearch.jsm',
     'Discovery.jsm',
     'EveryWindow.jsm',