Bug 893505 - Simplify the application update UI draft
authorDoug Thayer <dougathayer@gmail.com>
Tue, 24 Jan 2017 12:13:15 -0800
changeset 465773 9932997fb7c5da4b7037adf0f3e14033f0411519
parent 462512 6a23526fe5168087d7e4132c0705aefcaed5f571
child 543251 a2d27e40c88f528fcb9d00ecfa226ae7245c935a
push id42712
push userbmo:dothayer@mozilla.com
push dateTue, 24 Jan 2017 21:10:14 +0000
bugs893505
milestone53.0a1
Bug 893505 - Simplify the application update UI MozReview-Commit-ID: 24sESMTosNX
browser/app/profile/firefox.js
browser/base/content/browser-fxaccounts.js
browser/base/content/browser.css
browser/base/content/browser.js
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_menuButtonBadgeManager.js
browser/base/content/test/simpleUpdate/.eslintrc.js
browser/base/content/test/simpleUpdate/browser.ini
browser/base/content/test/simpleUpdate/browser_noUpdates.js
browser/base/content/test/simpleUpdate/browser_updatesBasicPrompt.js
browser/base/content/test/simpleUpdate/browser_updatesBasicPromptNoStaging.js
browser/base/content/test/simpleUpdate/browser_updatesCompleteAndPartialPatchesWithBadCompleteSize.js
browser/base/content/test/simpleUpdate/browser_updatesCompleteAndPartialPatchesWithBadPartialSize.js
browser/base/content/test/simpleUpdate/browser_updatesCompleteAndPartialPatchesWithBadSizes.js
browser/base/content/test/simpleUpdate/browser_updatesCompletePatchApplyFailure.js
browser/base/content/test/simpleUpdate/browser_updatesCompletePatchWithBadCompleteSize.js
browser/base/content/test/simpleUpdate/browser_updatesDownloadReprompt.js
browser/base/content/test/simpleUpdate/browser_updatesLaunchManualInstaller.js
browser/base/content/test/simpleUpdate/browser_updatesMalformedXml.js
browser/base/content/test/simpleUpdate/browser_updatesPartialPatchApplyFailure.js
browser/base/content/test/simpleUpdate/browser_updatesPartialPatchApplyFailureWithCompleteAvailable.js
browser/base/content/test/simpleUpdate/browser_updatesPartialPatchApplyFailureWithCompleteValidationFailure.js
browser/base/content/test/simpleUpdate/browser_updatesPartialPatchWithBadPartialSize.js
browser/base/content/test/simpleUpdate/downloadPage.html
browser/base/content/test/simpleUpdate/head.js
browser/base/content/test/simpleUpdate/sharedUpdateXML.js
browser/base/content/test/simpleUpdate/simple.mar
browser/base/content/test/simpleUpdate/update.sjs
browser/base/moz.build
browser/components/customizableui/content/panelUI.inc.xul
browser/components/customizableui/content/panelUI.js
browser/components/customizableui/test/browser.ini
browser/components/customizableui/test/browser_panelUINotifications.js
browser/components/downloads/content/indicator.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/locales/en-US/chrome/browser/browser.properties
browser/themes/shared/customizableui/panelUI.inc.css
browser/themes/shared/notification-icons.inc.css
browser/themes/shared/notification-icons.svg
browser/themes/shared/update-badge.svg
toolkit/mozapps/update/nsUpdateService.js
toolkit/mozapps/update/tests/chrome/utils.js
toolkit/mozapps/update/tests/data/shared.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -74,16 +74,28 @@ pref("extensions.webextensions.default-c
 
 // Require signed add-ons by default
 pref("xpinstall.signatures.required", true);
 pref("xpinstall.signatures.devInfoURL", "https://wiki.mozilla.org/Addons/Extension_Signing");
 
 // Dictionary download preference
 pref("browser.dictionaries.download.url", "https://addons.mozilla.org/%LOCALE%/firefox/dictionaries/");
 
+// When we display a doorhanger to update firefox, there will be a "What's new"
+// link which will point to this URL.
+#ifdef RELEASE_OR_BETA
+pref("app.releaseNotesURL", "https://www.mozilla.org/%LOCALE%/firefox/{targetVersion}/releasenotes/");
+#else
+#ifdef NIGHTLY_BUILD
+pref("app.releaseNotesURL", "https://blog.nightly.mozilla.org/");
+#else
+pref("app.releaseNotesURL", "https://www.mozilla.org/%LOCALE%/firefox/{targetVersion}/auroranotes/");
+#endif
+#endif
+
 // At startup, should we check to see if the installation
 // date is older than some threshold
 pref("app.update.checkInstallTime", true);
 
 // The number of days a binary is permitted to be old without checking is defined in
 // firefox-branding.js (app.update.checkInstallTime.days)
 
 // The minimum delay in seconds for the timer to fire between the notification
@@ -112,16 +124,19 @@ pref("app.update.log", false);
 // The number of general background check failures to allow before notifying the
 // user of the failure. User initiated update checks always notify the user of
 // the failure.
 pref("app.update.backgroundMaxErrors", 10);
 
 // Whether or not app updates are enabled
 pref("app.update.enabled", true);
 
+// Whether or not to use the simplified doorhanger application update UI.
+pref("app.update.doorhanger", true);
+
 // If set to true, the Update Service will automatically download updates when
 // app updates are enabled per the app.update.enabled preference and if the user
 // can apply updates.
 pref("app.update.auto", true);
 
 // If set to true, the Update Service will present no UI for any event.
 pref("app.update.silent", false);
 
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -258,19 +258,19 @@ var gFxAccounts = {
           this.panelUIFooter.setAttribute("fxastatus", "signedin");
           this.panelUILabel.setAttribute("label", userData.email);
         }
         if (profileInfoEnabled) {
           this.panelUIFooter.setAttribute("fxaprofileimage", "enabled");
         }
       }
       if (showErrorBadge) {
-        gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
+        PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
       } else {
-        gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_FXA);
+        PanelUI.removeNotification("fxa-needs-authentication");
       }
     }
 
     let updateWithProfile = (profile) => {
       if (profileInfoEnabled) {
         if (profile.displayName) {
           this.panelUILabel.setAttribute("label", profile.displayName);
         }
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -1139,17 +1139,17 @@ toolbarpaletteitem[place="palette"][hidd
 
 #customization-palette .toolbarpaletteitem-box {
   -moz-box-pack: center;
   -moz-box-flex: 1;
   width: 10em;
   max-width: 10em;
 }
 
-#main-window[customizing=true] #PanelUI-update-status {
+#main-window[customizing=true] .PanelUI-notification-menu-item {
   display: none;
 }
 
 /* UI Tour */
 
 @keyframes uitour-wobble {
   from {
     transform: rotate(0deg) translateX(3px) rotate(0deg);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1357,18 +1357,16 @@ var gBrowserInit = {
     gSyncUI.init();
     gFxAccounts.init();
 
     if (AppConstants.MOZ_DATA_REPORTING)
       gDataNotificationInfoBar.init();
 
     gBrowserThumbnails.init();
 
-    gMenuButtonBadgeManager.init();
-
     gMenuButtonUpdateBadge.init();
 
     window.addEventListener("mousemove", MousePosTracker);
     window.addEventListener("dragover", MousePosTracker);
 
     gNavToolbox.addEventListener("customizationstarting", CustomizationHandler);
     gNavToolbox.addEventListener("customizationchange", CustomizationHandler);
     gNavToolbox.addEventListener("customizationending", CustomizationHandler);
@@ -1522,18 +1520,16 @@ var gBrowserInit = {
     CompactTheme.uninit();
 
     TrackingProtection.uninit();
 
     RefreshBlocker.uninit();
 
     gMenuButtonUpdateBadge.uninit();
 
-    gMenuButtonBadgeManager.uninit();
-
     SidebarUI.uninit();
 
     // Now either cancel delayedStartup, or clean up the services initialized from
     // it.
     if (this._boundDelayedStartup) {
       this._cancelDelayedStartup();
     } else {
       if (Win7Features)
@@ -2561,196 +2557,304 @@ function SetPageProxyState(aState, aOpti
   }
 }
 
 function PageProxyClickHandler(aEvent) {
   if (aEvent.button == 1 && gPrefService.getBoolPref("middlemouse.paste"))
     middleMousePaste(aEvent);
 }
 
-var gMenuButtonBadgeManager = {
-  BADGEID_APPUPDATE: "update",
-  BADGEID_DOWNLOAD: "download",
-  BADGEID_FXA: "fxa",
-
-  fxaBadge: null,
-  downloadBadge: null,
-  appUpdateBadge: null,
+// Setup the hamburger button badges for updates, if enabled.
+var gMenuButtonUpdateBadge = {
+  kTopics: [
+    "update-staged",
+    "update-downloaded",
+    "update-available",
+    "update-error",
+  ],
+
+  timeouts: [],
+  manualWorkflowStarted: null,
+
+  tryGetPref(type, name, defaultValue) {
+    try {
+      return Services.prefs[`get${type}Pref`](name);
+    } catch (e) {
+      return defaultValue;
+    }
+  },
+
+  get enabled() {
+    return Services.prefs.getBoolPref("app.update.doorhanger");
+  },
+
+  get repromptDownloadWaitTime() {
+    return this.tryGetPref("Int",
+                      "app.update.repromptDownloadWaitTime",
+                      60 * 1000);
+  },
+
+  get launchInstallerPopupWaitTime() {
+    return this.tryGetPref("Int",
+                           "app.update.launchInstallerPromptWaitTime",
+                           5 * 60 * 1000);
+  },
+
+  get badgeWaitTime() {
+    return this.tryGetPref("Int",
+                           "app.update.promptBadgeWaitTime",
+                           2 * 24 * 3600 * 1000); // 2 days
+  },
 
   init() {
-    PanelUI.panel.addEventListener("popupshowing", this, true);
-  },
-
-  uninit() {
-    PanelUI.panel.removeEventListener("popupshowing", this, true);
-  },
-
-  handleEvent(e) {
-    if (e.type === "popupshowing") {
-      this.clearBadges();
-    }
-  },
-
-  _showBadge() {
-    let badgeToShow = this.downloadBadge || this.appUpdateBadge || this.fxaBadge;
-
-    if (badgeToShow) {
-      PanelUI.menuButton.setAttribute("badge-status", badgeToShow);
-    } else {
-      PanelUI.menuButton.removeAttribute("badge-status");
-    }
-  },
-
-  _changeBadge(badgeId, badgeStatus = null) {
-    if (badgeId == this.BADGEID_APPUPDATE) {
-      this.appUpdateBadge = badgeStatus;
-    } else if (badgeId == this.BADGEID_DOWNLOAD) {
-      this.downloadBadge = badgeStatus;
-    } else if (badgeId == this.BADGEID_FXA) {
-      this.fxaBadge = badgeStatus;
-    } else {
-      Cu.reportError("The badge ID '" + badgeId + "' is unknown!");
-    }
-    this._showBadge();
-  },
-
-  addBadge(badgeId, badgeStatus) {
-    if (!badgeStatus) {
-      Cu.reportError("badgeStatus must be defined");
-      return;
-    }
-    this._changeBadge(badgeId, badgeStatus);
-  },
-
-  removeBadge(badgeId) {
-    this._changeBadge(badgeId);
-  },
-
-  clearBadges() {
-    this.appUpdateBadge = null;
-    this.downloadBadge = null;
-    this.fxaBadge = null;
-    this._showBadge();
-  }
-};
-
-// Setup the hamburger button badges for updates, if enabled.
-var gMenuButtonUpdateBadge = {
-  enabled: false,
-  badgeWaitTime: 0,
-  timer: null,
-  cancelObserverRegistered: false,
-
-  init() {
-    try {
-      this.enabled = Services.prefs.getBoolPref("app.update.badge");
-    } catch (e) {}
     if (this.enabled) {
-      try {
-        this.badgeWaitTime = Services.prefs.getIntPref("app.update.badgeWaitTime");
-      } catch (e) {
-        this.badgeWaitTime = 345600; // 4 days
-      }
-      Services.obs.addObserver(this, "update-staged", false);
-      Services.obs.addObserver(this, "update-downloaded", false);
+      this.kTopics.forEach(t => {
+        Services.obs.addObserver(this, t, false);
+      });
     }
   },
 
   uninit() {
-    if (this.timer)
-      this.timer.cancel();
     if (this.enabled) {
-      Services.obs.removeObserver(this, "update-staged");
-      Services.obs.removeObserver(this, "update-downloaded");
-      this.enabled = false;
-    }
-    if (this.cancelObserverRegistered) {
-      Services.obs.removeObserver(this, "update-canceled");
-      this.cancelObserverRegistered = false;
-    }
-  },
-
-  onMenuPanelCommand(event) {
-    if (event.originalTarget.getAttribute("update-status") === "succeeded") {
-      // restart the app
-      let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
-                       .createInstance(Ci.nsISupportsPRBool);
-      Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
-
-      if (!cancelQuit.data) {
-        Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart);
+      this.kTopics.forEach(t => {
+        Services.obs.removeObserver(this, t);
+      });
+    }
+
+    PanelUI.removeNotification(/^update-/);
+    this.clearCallbacks();
+  },
+
+  clearCallbacks() {
+    this.timeouts.forEach(t => clearTimeout(t));
+    this.timeouts = [];
+
+    Downloads.getList(Downloads.ALL).then(list => list.removeView(this));
+  },
+
+  addTimeout(time, callback) {
+    this.timeouts.push(setTimeout(() => {
+      this.clearCallbacks();
+      callback();
+    }, time));
+  },
+
+  replaceReleaseNotes(update, whatsNewId) {
+    let whatsNewLink = document.getElementById(whatsNewId);
+    if (update && update.appVersion) {
+      let releaseNotesURL = Services.urlFormatter.formatURLPref("app.releaseNotesURL");
+      let targetVersion = update.appVersion;
+      releaseNotesURL = releaseNotesURL.replace("{targetVersion}", targetVersion);
+      whatsNewLink.href = releaseNotesURL;
+      whatsNewLink.hidden = false;
+    } else {
+      whatsNewLink.href = null;
+      whatsNewLink.hidden = true;
+    }
+  },
+
+  requestRestart() {
+    let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
+                     .createInstance(Ci.nsISupportsPRBool);
+    Services.obs.notifyObservers(cancelQuit, "quit-application-requested", "restart");
+
+    if (!cancelQuit.data) {
+      Services.startup.quit(Services.startup.eAttemptQuit | Services.startup.eRestart);
+    }
+  },
+
+  openManualUpdateUrl() {
+    let manualUpdateUrl = Services.urlFormatter.formatURLPref("app.update.url.manual");
+    openUILinkIn(manualUpdateUrl, "tab");
+    this.manualWorkflowStarted = Date.now();
+    Downloads.getList(Downloads.ALL)
+      .then(list => list.addView(this))
+      .then(null, Cu.reportError);
+  },
+
+  showRestartNotification(dismissed) {
+    let action = {
+      callback: () => { gMenuButtonUpdateBadge.requestRestart(); }
+    };
+
+    PanelUI.showNotification("update-restart", action, [], { dismissed });
+  },
+
+  showUpdateAvailableNotification(update, dismissed) {
+    let action = {
+      callback: () => {
+        let updateService = Cc["@mozilla.org/updates/update-service;1"]
+                            .getService(Ci.nsIApplicationUpdateService);
+        updateService.downloadUpdate(update, true);
       }
-    } else {
-      // open the page for manual update
-      let url = Services.urlFormatter.formatURLPref("app.update.url.manual");
-      openUILinkIn(url, "tab");
-    }
+    };
+
+    this.replaceReleaseNotes(update, "update-available-whats-new");
+
+    PanelUI.showNotification("update-available", action, [], { dismissed });
+  },
+
+  showLaunchInstallerNotification(dismissed) {
+    let action = {
+      callback: () => {
+        if (download.succeeded) {
+          download.launch();
+        } else {
+          download.launchWhenSucceeded = true;
+        }
+      }
+    };
+
+    PanelUI.showNotification("update-launch", action, [], { dismissed });
+  },
+
+  showManualUpdateNotification(update, dismissed) {
+    let action = {
+      callback: () => {
+        let self = gMenuButtonUpdateBadge;
+        self.openManualUpdateUrl();
+
+        this.addTimeout(this.repromptDownloadWaitTime, () => {
+          this.showManualUpdateNotification(true);
+        });
+      }
+    };
+
+    this.replaceReleaseNotes(update, "update-manual-whats-new");
+
+    PanelUI.showNotification("update-manual", action, [], { dismissed });
+  },
+
+  handleUpdateError(update, status) {
+    switch (status) {
+      case "all-downloads-failed":
+      case "check-attempts-exceeded":
+      case "elevation-canceled":
+      case "unknown":
+        // Background update has failed, let's show the UI responsible for
+        // prompting the user to update manually.
+        this.clearCallbacks();
+        this.showManualUpdateNotification(update, false);
+        break;
+    }
+  },
+
+  handleUpdateDownloaded(update, status) {
+    switch (status) {
+      case "pending-service":
+      case "success":
+        this.clearCallbacks();
+        let badgeWaitTime = this.badgeWaitTime;
+        let doorhangerWaitTime = update.promptWaitTime * 1000;
+
+        if (badgeWaitTime < doorhangerWaitTime) {
+          this.addTimeout(badgeWaitTime, () => {
+            this.showRestartNotification(true);
+
+            let remainingTime = doorhangerWaitTime - badgeWaitTime;
+            this.addTimeout(remainingTime, () => {
+              this.showRestartNotification(false);
+            });
+          });
+        } else {
+          this.addTimeout(doorhangerWaitTime, () => {
+            this.showRestartNotification(false);
+          });
+        }
+        break;
+    }
+  },
+
+  handleUpdateAvailable(update, status) {
+    switch (status) {
+      case "show-prompt":
+        // Background update has failed, let's show the UI responsible for
+        // prompting the user to update manually.
+        this.clearCallbacks();
+        this.showUpdateAvailableNotification(update, false);
+        break;
+    }
+  },
+
+  handleUpdateStaged(update, status) {
+    handleUpdateDownloaded(status);
   },
 
   observe(subject, topic, status) {
-    if (topic == "update-canceled") {
-      this.reset();
+    if (!this.enabled) {
       return;
     }
-    if (status == "failed") {
-      // Background update has failed, let's show the UI responsible for
-      // prompting the user to update manually.
-      this.uninit();
-      this.displayBadge(false);
+
+    let update = subject && subject.QueryInterface(Ci.nsIUpdate);
+
+    switch (topic) {
+      case "update-available":
+        this.handleUpdateAvailable(update, status);
+        break;
+      case "update-downloaded":
+        this.handleUpdateDownloaded(update, status);
+        break;
+      case "update-staged":
+        this.handleUpdateStaged(update, status);
+        break;
+      case "update-error":
+        this.handleUpdateError(update, status);
+        break;
+    }
+  },
+
+  onDownloadAdded(download) {
+    if (download.startTime < this.manualWorkflowStarted) {
+      // This download was from before we prompted them, so it's likely not
+      // the installer we directed them toward.
       return;
     }
 
-    // Give the user badgeWaitTime seconds to react before prompting.
-    this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-    this.timer.initWithCallback(this, this.badgeWaitTime * 1000,
-                                this.timer.TYPE_ONE_SHOT);
-    // The timer callback will call uninit() when it completes.
-  },
-
-  notify() {
-    // If the update is successfully applied, or if the updater has fallen back
-    // to non-staged updates, add a badge to the hamburger menu to indicate an
-    // update will be applied once the browser restarts.
-    this.uninit();
-    this.displayBadge(true);
-  },
-
-  displayBadge(succeeded) {
-    let status = succeeded ? "succeeded" : "failed";
-    let badgeStatus = "update-" + status;
-    gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, badgeStatus);
-
-    let stringId;
-    let updateButtonText;
-    if (succeeded) {
-      let brandBundle = document.getElementById("bundle_brand");
-      let brandShortName = brandBundle.getString("brandShortName");
-      stringId = "appmenu.restartNeeded.description";
-      updateButtonText = gNavigatorBundle.getFormattedString(stringId,
-                                                             [brandShortName]);
-      Services.obs.addObserver(this, "update-canceled", false);
-      this.cancelObserverRegistered = true;
-    } else {
-      stringId = "appmenu.updateFailed.description";
-      updateButtonText = gNavigatorBundle.getString(stringId);
-    }
-
-    let updateButton = document.getElementById("PanelUI-update-status");
-    updateButton.setAttribute("label", updateButtonText);
-    updateButton.setAttribute("update-status", status);
-    updateButton.hidden = false;
+    let tryGetBaseDomain = uriString => {
+      try {
+        let uri = NetUtil.newURI(uriString);
+        return Services.eTLD.getBaseDomain(uri);
+      } catch (e) {
+        Cu.reportError(`Bad URI in onDownloadAdded: [${uriString}] (${e})`);
+        return null;
+      }
+    };
+
+    let referrerBaseDomain = tryGetBaseDomain(download.source.referrer);
+    let manualUpdateUrl = Services.urlFormatter.formatURLPref("app.update.url.manual");
+    let manualUpdateBaseDomain = tryGetBaseDomain(manualUpdateUrl);
+
+    // Assume that the first download from the same base domain as our manual
+    // update URL is the installer. This is a bit clunky, but it should
+    // generally work just fine, especially since this listener will be
+    // cancelled if they don't download the installer within
+    // repromptDownloadWaitTime.
+    if (referrerBaseDomain && referrerBaseDomain == manualUpdateBaseDomain) {
+      this.clearCallbacks();
+
+      // This download is already going to launch when it finishes, so don't
+      // bother showing the user anything.
+      if (download.launchWhenSucceeded) {
+        return;
+      }
+
+      PanelUI.removeNotification("update-manual");
+      this.showLaunchInstallerNotification(true);
+
+      // Give the user launchInstallerPopupWaitTime before escalating the badge
+      // into a popup
+      this.addTimeout(this.launchInstallerPopupWaitTime, () => {
+        this.showLaunchInstallerNotification(false);
+      });
+    }
   },
 
   reset() {
-    gMenuButtonBadgeManager.removeBadge(
-      gMenuButtonBadgeManager.BADGEID_APPUPDATE);
-    let updateButton = document.getElementById("PanelUI-update-status");
-    updateButton.hidden = true;
-    this.uninit();
-    this.init();
+    PanelUI.removeNotification(/^update-/);
+    this.clearCallbacks();
   }
 };
 
 // Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
 const TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED   = 2;
 const TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED = 3;
 const TLS_ERROR_REPORT_TELEMETRY_MANUAL_SEND    = 4;
 const TLS_ERROR_REPORT_TELEMETRY_AUTO_SEND      = 5;
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -489,17 +489,16 @@ tags = mcb
 tags = psm
 [browser_mcb_redirect.js]
 tags = mcb
 [browser_windowactivation.js]
 [browser_contextmenu_childprocess.js]
 [browser_bug963945.js]
 [browser_domFullscreen_fullscreenMode.js]
 tags = fullscreen
-[browser_menuButtonBadgeManager.js]
 [browser_newTabDrop.js]
 [browser_newWindowDrop.js]
 [browser_csp_block_all_mixedcontent.js]
 tags = mcb
 [browser_newwindow_focus.js]
 skip-if = (os == "linux" && !e10s) # Bug 1263254 - Perma fails on Linux without e10s for some reason.
 [browser_bug1299667.js]
 [browser_close_dependent_tabs.js]
deleted file mode 100644
--- a/browser/base/content/test/general/browser_menuButtonBadgeManager.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/* 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/. */
-
-var menuButton = document.getElementById("PanelUI-menu-button");
-
-add_task(function* testButtonActivities() {
-  is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-  is(menuButton.hasAttribute("badge"), false, "Should not have the badge attribute set");
-
-  gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
-  is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
-
-  gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, "update-succeeded");
-  is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
-
-  gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, "update-failed");
-  is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
-
-  gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, "download-severe");
-  is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
-
-  gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, "download-warning");
-  is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
-
-  gMenuButtonBadgeManager.addBadge("unknownbadge", "attr");
-  is(menuButton.getAttribute("badge-status"), "download-warning", "Should not have changed badge status");
-
-  gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD);
-  is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
-
-  gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE);
-  is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
-
-  gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_FXA);
-  is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
-
-  yield PanelUI.show();
-  is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (Hamburger menu opened)");
-  PanelUI.hide();
-
-  gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_FXA, "fxa-needs-authentication");
-  gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_APPUPDATE, "update-succeeded");
-  gMenuButtonBadgeManager.clearBadges();
-  is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (clearBadges called)");
-});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "../../../../../testing/mochitest/browser.eslintrc.js"
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser.ini
@@ -0,0 +1,23 @@
+[DEFAULT]
+support-files =
+  head.js
+  update.sjs
+  sharedUpdateXML.js
+  simple.mar
+  downloadPage.html
+
+[browser_noUpdates.js]
+[browser_updatesBasicPrompt.js]
+[browser_updatesBasicPromptNoStaging.js]
+[browser_updatesCompleteAndPartialPatchesWithBadCompleteSize.js]
+[browser_updatesCompleteAndPartialPatchesWithBadPartialSize.js]
+[browser_updatesCompleteAndPartialPatchesWithBadSizes.js]
+[browser_updatesCompletePatchApplyFailure.js]
+[browser_updatesCompletePatchWithBadCompleteSize.js]
+[browser_updatesDownloadReprompt.js]
+[browser_updatesLaunchManualInstaller.js]
+[browser_updatesMalformedXml.js]
+[browser_updatesPartialPatchApplyFailure.js]
+[browser_updatesPartialPatchApplyFailureWithCompleteAvailable.js]
+[browser_updatesPartialPatchApplyFailureWithCompleteValidationFailure.js]
+[browser_updatesPartialPatchWithBadPartialSize.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_noUpdates.js
@@ -0,0 +1,10 @@
+add_task(function* testNoUpdates() {
+  let updateParams = "noUpdates=1";
+
+  yield runUpdateTest(updateParams, 1, []);
+
+  yield sleep(100);
+
+  is(PanelUI.activeNotification, null,
+     "No notification should be present if there are no updates.")
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesBasicPrompt.js
@@ -0,0 +1,22 @@
+add_task(function* testBasicPrompt() {
+  let updateParams = "showPrompt=1&promptWaitTime=0";
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      notificationId: "update-available",
+      button: "button",
+      beforeClick() {
+        let whatsNewLink = document.getElementById("update-available-whats-new");
+        is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+           "The what's new link points to the release notes for the update version.");
+      }
+    },
+    {
+      notificationId: "update-restart",
+      button: "secondarybutton",
+      cleanup() {
+        PanelUI.removeNotification(/.*/);
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesBasicPromptNoStaging.js
@@ -0,0 +1,26 @@
+add_task(function* testBasicPromptNoStaging() {
+  pushPref("Bool", PREF_APP_UPDATE_STAGING_ENABLED, false);
+
+  let updateParams = "showPrompt=1&promptWaitTime=0";
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      notificationId: "update-available",
+      button: "button",
+      beforeClick() {
+        let whatsNewLink = document.getElementById("update-available-whats-new");
+        is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+           "The what's new link points to the release notes for the update version.");
+      }
+    },
+    {
+      notificationId: "update-restart",
+      button: "secondarybutton",
+      cleanup() {
+        PanelUI.removeNotification(/.*/);
+      }
+    },
+  ]);
+
+  popPref(PREF_APP_UPDATE_STAGING_ENABLED);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesCompleteAndPartialPatchesWithBadCompleteSize.js
@@ -0,0 +1,13 @@
+add_task(function* testCompleteAndPartialPatchesWithBadCompleteSize() {
+  let updateParams = "invalidCompleteSize=1&promptWaitTime=0";
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      notificationId: "update-restart",
+      button: "secondarybutton",
+      cleanup() {
+        PanelUI.removeNotification(/.*/);
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesCompleteAndPartialPatchesWithBadPartialSize.js
@@ -0,0 +1,13 @@
+add_task(function* testCompleteAndPartialPatchesWithBadPartialSize() {
+  let updateParams = "invalidPartialSize=1&promptWaitTime=0";
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      notificationId: "update-restart",
+      button: "secondarybutton",
+      cleanup() {
+        PanelUI.removeNotification(/.*/);
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesCompleteAndPartialPatchesWithBadSizes.js
@@ -0,0 +1,25 @@
+add_task(function* testCompleteAndPartialPatchesWithBadSizes() {
+  let updateParams = "invalidPartialSize=1&invalidCompleteSize=1";
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      // if we have only an invalid patch, then something's wrong and we don't have an automatic
+      // way to fix it, so show the manual update doorhanger.
+      notificationId: "update-manual",
+      button: "button",
+      beforeClick() {
+        let whatsNewLink = document.getElementById("update-manual-whats-new");
+        is(whatsNewLink.hidden, false,
+           "We should have a what's new link if we had an update.");
+        is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+           "The what's new link points to the release notes for the update version.");
+      },
+      *cleanup() {
+        yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+        is(gBrowser.selectedBrowser.currentURI.spec,
+           URL_MANUAL_UPDATE, "Landed on manual update page.")
+        gBrowser.removeTab(gBrowser.selectedTab);
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesCompletePatchApplyFailure.js
@@ -0,0 +1,28 @@
+add_task(function* testCompletePatchApplyFailure() {
+  let patches = getLocalPatchString("complete", null, null, null, null, null,
+                                    STATE_PENDING);
+  let updates = getLocalUpdateString(patches, null, null, null,
+                                     Services.appinfo.version, null);
+
+  yield runUpdateProcessingTest(updates, [
+    {
+      // if we have only an invalid patch, then something's wrong and we don't have an automatic
+      // way to fix it, so show the manual update doorhanger.
+      notificationId: "update-manual",
+      button: "button",
+      beforeClick() {
+        let whatsNewLink = document.getElementById("update-manual-whats-new");
+        is(whatsNewLink.hidden, false,
+           "We should have a what's new link if we had an update.");
+        is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+           "The what's new link points to the release notes for the update version.");
+      },
+      *cleanup() {
+        yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+        is(gBrowser.selectedBrowser.currentURI.spec,
+           URL_MANUAL_UPDATE, "Landed on manual update page.")
+        gBrowser.removeTab(gBrowser.selectedTab);
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesCompletePatchWithBadCompleteSize.js
@@ -0,0 +1,25 @@
+add_task(function* testCompletePatchWithBadCompleteSize() {
+  let updateParams = "completePatchOnly=1&invalidCompleteSize=1";
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      // if we have only an invalid patch, then something's wrong and we don't have an automatic
+      // way to fix it, so show the manual update doorhanger.
+      notificationId: "update-manual",
+      button: "button",
+      beforeClick() {
+        let whatsNewLink = document.getElementById("update-manual-whats-new");
+        is(whatsNewLink.hidden, false,
+           "We should have a what's new link if we had an update.");
+        is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+           "The what's new link points to the release notes for the update version.");
+      },
+      *cleanup() {
+        yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+        is(gBrowser.selectedBrowser.currentURI.spec,
+           URL_MANUAL_UPDATE, "Landed on manual update page.")
+        gBrowser.removeTab(gBrowser.selectedTab);
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesDownloadReprompt.js
@@ -0,0 +1,31 @@
+add_task(function* testMalformedXml() {
+  const maxBackgroundErrors = 1;
+  pushPref("Int", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors);
+  pushPref("Int", PREF_APP_UPDATE_REPROMPTDOWNLOADWAITTIME, 0);
+
+  let updateParams = "xmlMalformed=1";
+
+  yield runUpdateTest(updateParams, maxBackgroundErrors, [
+    {
+      notificationId: "update-manual",
+      button: "button",
+    },
+    {
+      // since we set the reprompt time to 0, we should see this pop up immediately
+      notificationId: "update-manual",
+      button: "secondarybutton",
+      beforeClick() {
+        let whatsNewLink = document.getElementById("update-manual-whats-new");
+        is(whatsNewLink.hidden, true,
+           "The what's new link should be hidden since we don't know the version.");
+      },
+      *cleanup() {
+        yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+        is(gBrowser.selectedBrowser.currentURI.spec,
+           URL_MANUAL_UPDATE, "Landed on manual update page.")
+        gBrowser.removeTab(gBrowser.selectedTab);
+        gMenuButtonUpdateBadge.reset();
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesLaunchManualInstaller.js
@@ -0,0 +1,37 @@
+add_task(function* testMalformedXml() {
+  const maxBackgroundErrors = 1;
+  pushPref("Int", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors);
+  pushPref("Int", PREF_APP_UPDATE_LAUNCHINSTALLERPROMPTWAITTIME, 0);
+  pushPref("Int", PREF_APP_SECURITY_DIALOGENABLEDELAY, 0);
+
+  let updateParams = "xmlMalformed=1";
+
+  yield runUpdateTest(updateParams, maxBackgroundErrors, [
+    {
+      notificationId: "update-manual",
+      button: "button",
+      *cleanup() {
+        yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+        yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
+          content.document.getElementById('download-link').click();
+        });
+
+        let win = yield waitForWindow("chrome://mozapps/content/downloads/unknownContentType.xul");
+        executeSoon(() => win.document.documentElement.getButton("accept").click());
+      }
+    },
+    {
+      notificationId: "update-launch",
+      button: "secondarybutton",
+      beforeClick() {
+        let whatsNewLink = document.getElementById("update-manual-whats-new");
+        is(whatsNewLink.hidden, true,
+           "The what's new link should be hidden since we don't know the version.");
+      },
+      cleanup() {
+        gBrowser.removeTab(gBrowser.selectedTab);
+        gMenuButtonUpdateBadge.reset();
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesMalformedXml.js
@@ -0,0 +1,27 @@
+add_task(function* testMalformedXml() {
+  const maxBackgroundErrors = 10;
+  pushPref("Int", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors);
+
+  let updateParams = "xmlMalformed=1";
+
+  yield runUpdateTest(updateParams, maxBackgroundErrors, [
+    {
+      // if we fail 10 check attempts, then we want to just show the user a manual update
+      // workflow.
+      notificationId: "update-manual",
+      button: "button",
+      beforeClick() {
+        let whatsNewLink = document.getElementById("update-manual-whats-new");
+        is(whatsNewLink.hidden, true,
+           "The what's new link should be hidden since we don't know the version.");
+      },
+      *cleanup() {
+        yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+        is(gBrowser.selectedBrowser.currentURI.spec,
+           URL_MANUAL_UPDATE, "Landed on manual update page.")
+        gBrowser.removeTab(gBrowser.selectedTab);
+        gMenuButtonUpdateBadge.reset();
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesPartialPatchApplyFailure.js
@@ -0,0 +1,29 @@
+add_task(function* testPartialPatchApplyFailure() {
+  let patches = getLocalPatchString("partial", null, null, null, null, null,
+                                    STATE_PENDING);
+  let updates = getLocalUpdateString(patches, null, null, null,
+                                     Services.appinfo.version, null,
+                                     null, null, null, null, "false");
+
+  yield runUpdateProcessingTest(updates, [
+    {
+      // if we have only an invalid patch, then something's wrong and we don't have an automatic
+      // way to fix it, so show the manual update doorhanger.
+      notificationId: "update-manual",
+      button: "button",
+      beforeClick() {
+        let whatsNewLink = document.getElementById("update-manual-whats-new");
+        is(whatsNewLink.hidden, false,
+           "We should have a what's new link if we had an update.");
+        is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+           "The what's new link points to the release notes for the update version.");
+      },
+      *cleanup() {
+        yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+        is(gBrowser.selectedBrowser.currentURI.spec,
+           URL_MANUAL_UPDATE, "Landed on manual update page.")
+        gBrowser.removeTab(gBrowser.selectedTab);
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesPartialPatchApplyFailureWithCompleteAvailable.js
@@ -0,0 +1,22 @@
+add_task(function* testPartialPatchApplyFailureWithCompleteAvailable() {
+  let patches = getLocalPatchString("partial", null, null, null, null, null,
+                                    STATE_PENDING) +
+                getLocalPatchString("complete", SERVICE_URL, null, null,
+                                    null, "false");
+
+  let promptWaitTime = "0";
+  let updates = getLocalUpdateString(patches, null, null, null,
+                                     Services.appinfo.version, null,
+                                     null, null, null, null, "false",
+                                     null, null, null, null, promptWaitTime);
+
+  yield runUpdateProcessingTest(updates, [
+    {
+      notificationId: "update-restart",
+      button: "secondarybutton",
+      cleanup() {
+        PanelUI.removeNotification(/.*/);
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesPartialPatchApplyFailureWithCompleteValidationFailure.js
@@ -0,0 +1,33 @@
+add_task(function* testPartialPatchApplyFailureWithCompleteValidationFailure() {
+  let patches = getLocalPatchString("partial", null, null, null, null, null,
+                                    STATE_PENDING) +
+                getLocalPatchString("complete", SERVICE_URL, "MD5",
+                                    null, "1234",
+                                    "false");
+
+  let updates = getLocalUpdateString(patches, null, null, null,
+                                     Services.appinfo.version, null,
+                                     null, null, null, null, "false");
+
+  yield runUpdateProcessingTest(updates, [
+    {
+      // if we have only an invalid patch, then something's wrong and we don't have an automatic
+      // way to fix it, so show the manual update doorhanger.
+      notificationId: "update-manual",
+      button: "button",
+      beforeClick() {
+        let whatsNewLink = document.getElementById("update-manual-whats-new");
+        is(whatsNewLink.hidden, false,
+           "We should have a what's new link if we had an update.");
+        is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+           "The what's new link points to the release notes for the update version.");
+      },
+      *cleanup() {
+        yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+        is(gBrowser.selectedBrowser.currentURI.spec,
+           URL_MANUAL_UPDATE, "Landed on manual update page.")
+        gBrowser.removeTab(gBrowser.selectedTab);
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/browser_updatesPartialPatchWithBadPartialSize.js
@@ -0,0 +1,25 @@
+add_task(function* testPartialPatchWithBadPartialSize() {
+  let updateParams = "partialPatchOnly=1&invalidPartialSize=1";
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      // if we have only an invalid patch, then something's wrong and we don't have an automatic
+      // way to fix it, so show the manual update doorhanger.
+      notificationId: "update-manual",
+      button: "button",
+      beforeClick() {
+        let whatsNewLink = document.getElementById("update-manual-whats-new");
+        is(whatsNewLink.hidden, false,
+           "We should have a what's new link if we had an update.");
+        is(whatsNewLink.href, "http://example.com/" + Services.appinfo.version,
+           "The what's new link points to the release notes for the update version.");
+      },
+      *cleanup() {
+        yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+        is(gBrowser.selectedBrowser.currentURI.spec,
+           URL_MANUAL_UPDATE, "Landed on manual update page.")
+        gBrowser.removeTab(gBrowser.selectedTab);
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/downloadPage.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Download page</title>
+</head>
+<body>
+<!-- just use simple.mar since we have it available and it will result in a download dialog -->
+<a id="download-link" href="http://example.com/browser/browser/base/content/test/simpleUpdate/simple.mar" data-link-type="download">
+  Download
+</a>
+</body>
+</html>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/head.js
@@ -0,0 +1,696 @@
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const IS_MACOSX = ("nsILocalFileMac" in Ci);
+const IS_WIN = ("@mozilla.org/windows-registry-key;1" in Cc);
+
+const KEY_UPDROOT         = "UpdRootD";
+
+const PREF_APP_UPDATE_URL                            = "app.update.url";
+const PREF_APP_UPDATE_DOORHANGER                     = "app.update.doorhanger";
+const PREF_APP_UPDATE_CHANNEL                        = "app.update.channel";
+const PREF_APP_UPDATE_ENABLED                        = "app.update.enabled";
+const PREF_APP_UPDATE_IDLETIME                       = "app.update.idletime";
+const PREF_APP_RELEASE_NOTES_URL                     = "app.releaseNotesURL";
+const PREF_APP_UPDATE_URL_MANUAL                     = "app.update.url.manual";
+const PREF_APP_UPDATE_STAGING_ENABLED                = "app.update.staging.enabled";
+const PREF_APP_UPDATE_BACKGROUNDMAXERRORS            = "app.update.backgroundMaxErrors";
+const PREF_APP_UPDATE_REPROMPTDOWNLOADWAITTIME       = "app.update.repromptDownloadWaitTime";
+const PREF_APP_UPDATE_LAUNCHINSTALLERPROMPTWAITTIME  = "app.update.launchInstallerPromptWaitTime";
+const PREF_APP_SECURITY_DIALOGENABLEDELAY            = "security.dialog_enable_delay";
+
+const PREFBRANCH_APP_PARTNER         = "app.partner.";
+const PREF_DISTRIBUTION_ID           = "distribution.id";
+const PREF_DISTRIBUTION_VERSION      = "distribution.version";
+const PREF_TOOLKIT_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
+
+const NS_APP_PROFILE_DIR_STARTUP   = "ProfDS";
+const NS_APP_USER_PROFILE_50_DIR   = "ProfD";
+const NS_GRE_DIR                   = "GreD";
+const NS_GRE_BIN_DIR               = "GreBinD";
+const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD";
+const XRE_EXECUTABLE_FILE          = "XREExeF";
+const XRE_UPDATE_ROOT_DIR          = "UpdRootD";
+
+const DIR_PATCH        = "0";
+const DIR_TOBEDELETED  = "tobedeleted";
+const DIR_UPDATES      = "updates";
+const DIR_UPDATED      = IS_MACOSX ? "Updated.app" : "updated";
+
+const BIN_SUFFIX = (IS_WIN ? ".exe" : "");
+const FILE_UPDATER_BIN = "updater" + (IS_MACOSX ? ".app" : BIN_SUFFIX);
+const FILE_UPDATER_BIN_BAK = FILE_UPDATER_BIN + ".bak";
+
+const FILE_ACTIVE_UPDATE_XML         = "active-update.xml";
+const FILE_APPLICATION_INI           = "application.ini";
+const FILE_BACKUP_UPDATE_LOG         = "backup-update.log";
+const FILE_LAST_UPDATE_LOG           = "last-update.log";
+const FILE_UPDATE_SETTINGS_INI       = "update-settings.ini";
+const FILE_UPDATE_SETTINGS_INI_BAK   = "update-settings.ini.bak";
+const FILE_UPDATER_INI               = "updater.ini";
+const FILE_UPDATES_XML               = "updates.xml";
+const FILE_UPDATE_LOG                = "update.log";
+const FILE_UPDATE_MAR                = "update.mar";
+const FILE_UPDATE_STATUS             = "update.status";
+const FILE_UPDATE_TEST               = "update.test";
+const FILE_UPDATE_VERSION            = "update.version";
+
+const PERMS_FILE      = FileUtils.PERMS_FILE;
+const PERMS_DIRECTORY = FileUtils.PERMS_DIRECTORY;
+
+const UPDATE_SETTINGS_CONTENTS = "[Settings]\n" +
+                                 "ACCEPTED_MAR_CHANNEL_IDS=xpcshell-test\n";
+
+const PR_RDWR        = 0x04;
+const PR_CREATE_FILE = 0x08;
+const PR_TRUNCATE    = 0x20;
+
+const DEFAULT_UPDATE_VERSION = "999999.0";
+
+const URL_HOST = "http://example.com"
+const URL_HTTP_UPDATE_XML = "http://example.com/browser/browser/base/content/test/simpleUpdate/update.sjs";
+
+const URL_MANUAL_UPDATE = "http://example.com/browser/browser/base/content/test/simpleUpdate/downloadPage.html";
+
+var gURLData = "http://example.com/browser/browser/base/content/test/simpleUpdate/";
+
+const DATA_URI_SPEC = "chrome://mochitests/content/browser/browser/base/content/test/simpleUpdate/";
+
+Services.scriptloader.loadSubScript(DATA_URI_SPEC + "sharedUpdateXML.js", this);
+
+const REL_PATH_DATA = "browser/browser/base/content/test/simpleUpdate/";
+const SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + FILE_SIMPLE_MAR;
+
+XPCOMUtils.defineLazyGetter(this, "gUpdateService", function test_gAUS() {
+  return Cc["@mozilla.org/updates/update-service;1"].
+         getService(Ci.nsIApplicationUpdateService).
+         QueryInterface(Ci.nsITimerCallback).
+         QueryInterface(Ci.nsIObserver).
+         QueryInterface(Ci.nsIUpdateCheckListener);
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "gUpdateManager",
+                                   "@mozilla.org/updates/update-manager;1",
+                                   "nsIUpdateManager");
+
+XPCOMUtils.defineLazyGetter(this, "gDefaultPrefBranch", function test_gDPB() {
+  return Services.prefs.getDefaultBranch(null);
+});
+
+let gRembemberedPrefs = [];
+
+/**
+ * Sets the app.update.url default preference.
+ *
+ * @param  aURL
+ *         The update url. If not specified 'URL_HOST + "/update.xml"' will be
+ *         used.
+ */
+function setUpdateURL(aURL) {
+  let url = aURL ? aURL : URL_HOST + "/update.xml";
+  pushPref("Char", PREF_APP_UPDATE_URL, url, gDefaultPrefBranch);
+}
+
+/**
+ * Gets the update version info for the update url parameters to send to
+ * update.sjs.
+ *
+ * @param  aAppVersion (optional)
+ *         The application version for the update snippet. If not specified the
+ *         current application version will be used.
+ * @return The url parameters for the application and platform version to send
+ *         to update.sjs.
+ */
+function getVersionParams(aAppVersion) {
+  let appInfo = Services.appinfo;
+  return "&appVersion=" + (aAppVersion ? aAppVersion : appInfo.version);
+}
+
+/**
+ * Removes the updates.xml file, active-update.xml file, and all files and
+ * sub-directories in the updates directory except for the "0" sub-directory.
+ * This prevents some tests from failing due to files being left behind when the
+ * tests are interrupted.
+ */
+function removeUpdateDirsAndFiles() {
+  let file = getUpdatesXMLFile(true);
+  try {
+    if (file.exists()) {
+      file.remove(false);
+    }
+  } catch (e) {
+    logTestInfo("Unable to remove file. Path: " + file.path +
+                ", Exception: " + e);
+  }
+
+  file = getUpdatesXMLFile(false);
+  try {
+    if (file.exists()) {
+      file.remove(false);
+    }
+  } catch (e) {
+    logTestInfo("Unable to remove file. Path: " + file.path +
+                ", Exception: " + e);
+  }
+
+  // This fails sporadically on Mac OS X so wrap it in a try catch
+  let updatesDir = getUpdatesDir();
+  try {
+    cleanUpdatesDir(updatesDir);
+  } catch (e) {
+    logTestInfo("Unable to remove files / directories from directory. Path: " +
+                updatesDir.path + ", Exception: " + e);
+  }
+}
+
+/**
+ * Removes all files and sub-directories in the updates directory except for
+ * the "0" sub-directory.
+ *
+ * @param  aDir
+ *         nsIFile for the directory to be deleted.
+ */
+function cleanUpdatesDir(aDir) {
+  if (!aDir.exists()) {
+    return;
+  }
+
+  let dirEntries = aDir.directoryEntries;
+  while (dirEntries.hasMoreElements()) {
+    let entry = dirEntries.getNext().QueryInterface(Ci.nsIFile);
+
+    if (entry.isDirectory()) {
+      if (entry.leafName == DIR_PATCH && entry.parent.leafName == DIR_UPDATES) {
+        cleanUpdatesDir(entry);
+        entry.permissions = PERMS_DIRECTORY;
+      } else {
+        try {
+          entry.remove(true);
+          return;
+        } catch (e) {
+        }
+        cleanUpdatesDir(entry);
+        entry.permissions = PERMS_DIRECTORY;
+        try {
+          entry.remove(true);
+        } catch (e) {
+          logTestInfo("cleanUpdatesDir: unable to remove directory. Path: " +
+                      entry.path + ", Exception: " + e);
+          throw (e);
+        }
+      }
+    } else {
+      entry.permissions = PERMS_FILE;
+      try {
+        entry.remove(false);
+      } catch (e) {
+        logTestInfo("cleanUpdatesDir: unable to remove file. Path: " +
+                    entry.path + ", Exception: " + e);
+        throw (e);
+      }
+    }
+  }
+}
+
+/**
+ * Deletes a directory and its children. First it tries nsIFile::Remove(true).
+ * If that fails it will fall back to recursing, setting the appropriate
+ * permissions, and deleting the current entry.
+ *
+ * @param  aDir
+ *         nsIFile for the directory to be deleted.
+ */
+function removeDirRecursive(aDir) {
+  if (!aDir.exists()) {
+    return;
+  }
+
+  try {
+    debugDump("attempting to remove directory. Path: " + aDir.path);
+    aDir.remove(true);
+    return;
+  } catch (e) {
+    logTestInfo("non-fatal error removing directory. Exception: " + e);
+  }
+
+  let dirEntries = aDir.directoryEntries;
+  while (dirEntries.hasMoreElements()) {
+    let entry = dirEntries.getNext().QueryInterface(Ci.nsIFile);
+
+    if (entry.isDirectory()) {
+      removeDirRecursive(entry);
+    } else {
+      entry.permissions = PERMS_FILE;
+      try {
+        debugDump("attempting to remove file. Path: " + entry.path);
+        entry.remove(false);
+      } catch (e) {
+        logTestInfo("error removing file. Exception: " + e);
+        throw (e);
+      }
+    }
+  }
+
+  aDir.permissions = PERMS_DIRECTORY;
+  aDir.remove(true);
+}
+
+/* Reloads the update metadata from disk */
+function reloadUpdateManagerData() {
+  gUpdateManager.QueryInterface(Ci.nsIObserver).
+  observe(null, "um-reload-update-data", "");
+}
+
+/**
+ * Returns the Gecko Runtime Engine directory where files other than executable
+ * binaries are located. On Mac OS X this will be <bundle>/Contents/Resources/
+ * and the installation directory on all other platforms.
+ *
+ * @return nsIFile for the Gecko Runtime Engine directory.
+ */
+function getGREDir() {
+  return Services.dirsvc.get(NS_GRE_DIR, Ci.nsIFile);
+}
+
+/**
+ * Gets the application base directory.
+ *
+ * @return  nsIFile object for the application base directory.
+ */
+function getAppBaseDir() {
+  return Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile).parent;
+}
+
+/**
+ * Logs TEST-INFO messages when DEBUG_AUS_TEST evaluates to true.
+ *
+ * @param  aText
+ *         The text to log.
+ * @param  aCaller (optional)
+ *         An optional Components.stack.caller. If not specified
+ *         Components.stack.caller will be used.
+ */
+function debugDump(aText, aCaller) {
+  if (DEBUG_AUS_TEST) {
+    let caller = aCaller ? aCaller : Components.stack.caller;
+    logTestInfo(aText, caller);
+  }
+}
+
+/**
+ * Returns either the active or regular update database XML file.
+ *
+ * @param  isActiveUpdate
+ *         If true this will return the active-update.xml otherwise it will
+ *         return the updates.xml file.
+ */
+function getUpdatesXMLFile(aIsActiveUpdate) {
+  let file = getUpdatesRootDir();
+  file.append(aIsActiveUpdate ? FILE_ACTIVE_UPDATE_XML : FILE_UPDATES_XML);
+  return file;
+}
+
+/**
+ * Gets the root directory for the updates directory.
+ *
+ * @return nsIFile for the updates root directory.
+ */
+function getUpdatesRootDir() {
+  return Services.dirsvc.get(XRE_UPDATE_ROOT_DIR, Ci.nsIFile);
+}
+
+/**
+ * Gets the updates directory.
+ *
+ * @return nsIFile for the updates directory.
+ */
+function getUpdatesDir() {
+  let dir = getUpdatesRootDir();
+  dir.append(DIR_UPDATES);
+  return dir;
+}
+
+/**
+ * Gets the directory for update patches.
+ *
+ * @return nsIFile for the updates directory.
+ */
+function getUpdatesPatchDir() {
+  let dir = getUpdatesDir();
+  dir.append(DIR_PATCH);
+  return dir;
+}
+
+/**
+ * Logs TEST-INFO messages.
+ *
+ * @param  aText
+ *         The text to log.
+ * @param  aCaller (optional)
+ *         An optional Components.stack.caller. If not specified
+ *         Components.stack.caller will be used.
+ */
+function logTestInfo(aText, aCaller) {
+  let caller = aCaller ? aCaller : Components.stack.caller;
+  let now = new Date();
+  let hh = now.getHours();
+  let mm = now.getMinutes();
+  let ss = now.getSeconds();
+  let ms = now.getMilliseconds();
+  let time = (hh < 10 ? "0" + hh : hh) + ":" +
+             (mm < 10 ? "0" + mm : mm) + ":" +
+             (ss < 10 ? "0" + ss : ss) + ":";
+  if (ms < 10) {
+    time += "00";
+  } else if (ms < 100) {
+    time += "0";
+  }
+  time += ms;
+  let msg = time + " | TEST-INFO | " + caller.filename + " | [" + caller.name +
+            " : " + caller.lineNumber + "] " + aText;
+  info(msg);
+}
+
+/**
+ * Clean up updates list and the updates directory.
+ */
+function cleanUpUpdates() {
+  gUpdateManager.activeUpdate = null;
+  gUpdateManager.saveUpdates();
+
+  removeUpdateDirsAndFiles();
+}
+
+function sleep(milliseconds) {
+  return new Promise(resolve => setTimeout(resolve, milliseconds));
+}
+
+function runUpdateTest(updateParams, checkAttempts, steps) {
+  return Task.spawn(function*() {
+    registerCleanupFunction(() => {
+      popPrefs();
+      gMenuButtonUpdateBadge.uninit();
+      gMenuButtonUpdateBadge.init();
+    });
+
+    pushPref("Bool", PREF_APP_UPDATE_ENABLED, true);
+    pushPref("Int", PREF_APP_UPDATE_IDLETIME, 0);
+    pushPref("Char", PREF_APP_RELEASE_NOTES_URL, "http://example.com/{targetVersion}");
+    pushPref("Char", PREF_APP_UPDATE_URL_MANUAL, URL_MANUAL_UPDATE);
+
+    let url = URL_HTTP_UPDATE_XML +
+              "?" + updateParams +
+              getVersionParams();
+
+    setUpdateURL(url);
+
+    executeSoon(() => {
+      Task.spawn(function*() {
+        gUpdateService.checkForBackgroundUpdates();
+        for (var i = 0; i < checkAttempts - 1; i++) {
+          yield waitForEvent("update-error", "check-attempt-failed");
+          gUpdateService.checkForBackgroundUpdates();
+        }
+      });
+    });
+
+    for (let step of steps) {
+      yield processStep(step);
+    }
+
+    cleanUpUpdates();
+  });
+}
+
+function runUpdateProcessingTest(updates, steps) {
+  return Task.spawn(function*() {
+    pushPref("Bool", PREF_APP_UPDATE_ENABLED, true);
+    pushPref("Int", PREF_APP_UPDATE_IDLETIME, 0);
+    pushPref("Char", PREF_APP_RELEASE_NOTES_URL, "http://example.com/{targetVersion}");
+    pushPref("Char", PREF_APP_UPDATE_URL_MANUAL, URL_MANUAL_UPDATE);
+
+    registerCleanupFunction(() => {
+      popPrefs();
+      gMenuButtonUpdateBadge.reset();
+    });
+
+    pushPref("Bool", PREF_APP_UPDATE_ENABLED, true);
+
+    writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+
+    writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+    writeStatusFile(STATE_FAILED_CRC_ERROR);
+    reloadUpdateManagerData();
+
+    testPostUpdateProcessing();
+
+    for (let step of steps) {
+      yield processStep(step);
+    }
+
+    cleanUpUpdates();
+  });
+}
+
+function removeUpdateFile(name) {
+  let versionFile = FileUtils.getDir(KEY_UPDROOT, ["updates", "0"], true).clone();
+  versionFile.append(name);
+
+  if (versionFile.exists()) {
+    versionFile.remove(false);
+  }
+}
+
+/**
+ * Writes the updates specified to either the active-update.xml or the
+ * updates.xml.
+ *
+ * @param  aContent
+ *         The updates represented as a string to write to the XML file.
+ * @param  isActiveUpdate
+ *         If true this will write to the active-update.xml otherwise it will
+ *         write to the updates.xml file.
+ */
+function writeUpdatesToXMLFile(aContent, aIsActiveUpdate) {
+  writeStringToFile(getUpdatesXMLFile(aIsActiveUpdate), aContent);
+}
+
+/**
+ * Writes the current update operation/state to a file in the patch
+ * directory, indicating to the patching system that operations need
+ * to be performed.
+ *
+ * @param  aStatus
+ *         The status value to write.
+ */
+function writeStatusFile(aStatus) {
+  let file = getUpdatesPatchDir();
+  file.append(FILE_UPDATE_STATUS);
+  writeStringToFile(file, aStatus + "\n");
+}
+
+/**
+ * Writes a string of text to a file.  A newline will be appended to the data
+ * written to the file.  This function only works with ASCII text.
+ */
+function writeStringToFile(file, text) {
+  let fos = FileUtils.openSafeFileOutputStream(file);
+  text += "\n";
+  try {
+    fos.write(text, text.length);
+  } catch (e) {
+    throw new Error(`Error writing file: [${file.path}]. Message: ${e}`);
+  }
+  FileUtils.closeSafeFileOutputStream(fos);
+}
+
+function processStep({notificationId, button, beforeClick, cleanup}) {
+  return Task.spawn(function*() {
+    yield waitForEvent(`panelUI-${notificationId}`, "doorhanger-shown");
+
+    let notification = document.getElementById(`PanelUI-${notificationId}-notification`);
+    is(notification.hidden, false, `${notificationId} notification is showing`);
+    if (beforeClick) {
+      yield Task.spawn(beforeClick);
+    }
+
+    let buttonEl = document.getAnonymousElementByAttribute(notification, "anonid", button);
+
+    buttonEl.click();
+
+    if (cleanup) {
+      yield Task.spawn(cleanup);
+    }
+  });
+}
+
+/**
+ * Sets a preference's value in a way that it is remembered and can be set back to what it
+ * was previously.
+ *
+ * @param  type
+ *         {Int, Char, Bool, etc.} as in getCharPref.
+ * @param  name
+ *         The name of the preference.
+ * @param  value
+ *         The value to temporarily give the preference.
+ * @param  prefBranch
+ *         Optional pref branch to use.
+ */
+function pushPref(type, name, value, prefBranch) {
+  if (!prefBranch) {
+    prefBranch = Services.prefs;
+  }
+
+  let oldValue;
+  try {
+    oldValue = prefBranch[`get${type}Pref`](name);
+  } catch (e) {
+    oldValue = null;
+  }
+  gRembemberedPrefs.push({type, name, oldValue, prefBranch});
+  prefBranch[`set${type}Pref`](name, value);
+}
+
+function popPrefs() {
+  for (let {type, name, oldValue, prefBranch} of gRembemberedPrefs.reverse()) {
+    if (oldValue === null) {
+      prefBranch.clearUserPref(name);
+    } else {
+      prefBranch[`set${type}Pref`](name, oldValue);
+    }
+  }
+
+  gRembemberedPrefs = [];
+}
+
+function popPref(name) {
+  let remembered = gRembemberedPrefs.reverse().find(p => p.name == name);
+  if (remembered) {
+    let {type, oldValue} = remembered;
+
+    if (oldValue === null) {
+      Services.prefs.clearUserPref(name);
+    } else {
+      Services.prefs[`set${type}Pref`](name, oldValue);
+    }
+
+    gRembemberedPrefs = gRembemberedPrefs.slice(gRembemberedPrefs.indexOf(remembered), 1);
+  }
+}
+
+/**
+ * Waits for the specified topic and (optionally) status.
+ * @param  topic
+ *         String representing the topic to wait for.
+ * @param  status
+ *         Optional String representing the status on said topic to wait for.
+ * @return A promise which will resolve the first time an event occurs on the specified
+ *         topic, and (optionally) with the specified status.
+ */
+function waitForEvent(topic, status = null) {
+  return new Promise(resolve => Services.obs.addObserver({
+    observe(subject, innerTopic, innerStatus) {
+      if (!status || status == innerStatus) {
+        Services.obs.removeObserver(this, topic);
+        resolve();
+      }
+    }
+  }, topic, false))
+}
+
+/* Triggers post-update processing */
+function testPostUpdateProcessing() {
+  gUpdateService.observe(null, "test-post-update-processing", "");
+}
+
+function addWindowListener(aURL, aCallback) {
+}
+
+/**
+ * Waits for a window with the specified url to load.
+ *
+ * @param  url
+ *         The url to wait for.
+ * @return A promise which will resolve when a window with the specified url is loaded.
+ *         If another url is loaded, this will result in an assertion failure.
+ */
+function waitForWindow(url) {
+  return new Promise(resolve => {
+    Services.wm.addListener({
+      onOpenWindow(xulWindow) {
+        info("window opened, waiting for focus");
+        Services.wm.removeListener(this);
+
+        var domwindow = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                                 .getInterface(Ci.nsIDOMWindow);
+        waitForFocus(function() {
+          is(domwindow.document.location.href, url, "should have seen the right window open");
+          resolve(domwindow);
+        }, domwindow);
+      },
+      onCloseWindow(xulWindow) { },
+      onWindowTitleChange(xulWindow, newTitle) { }
+    });
+  })
+}
+
+/**
+ * Constructs a string representing an update element for a local update xml
+ * file. See getUpdateString for parameter information not provided below.
+ *
+ * @param  aPatches
+ *         String representing the application update patches.
+ * @param  aServiceURL (optional)
+ *         The update's xml url.
+ *         If not specified it will default to 'http://test_service/'.
+ * @param  aIsCompleteUpdate (optional)
+ *         The string 'true' if this update was a complete update or the string
+ *         'false' if this update was a partial update.
+ *         If not specified it will default to 'true'.
+ * @param  aChannel (optional)
+ *         The update channel name.
+ *         If not specified it will default to the default preference value of
+ *         app.update.channel.
+ * @param  aForegroundDownload (optional)
+ *         The string 'true' if this update was manually downloaded or the
+ *         string 'false' if this update was automatically downloaded.
+ *         If not specified it will default to 'true'.
+ * @param  aPreviousAppVersion (optional)
+ *         The application version prior to applying the update.
+ *         If not specified it will not be present.
+ * @return The string representing an update element for an update xml file.
+ */
+function getLocalUpdateString(aPatches, aType, aName, aDisplayVersion,
+                              aAppVersion, aBuildID, aDetailsURL, aServiceURL,
+                              aInstallDate, aStatusText, aIsCompleteUpdate,
+                              aChannel, aForegroundDownload, aShowPrompt,
+                              aShowNeverForVersion, aPromptWaitTime,
+                              aBackgroundInterval, aPreviousAppVersion,
+                              aCustom1, aCustom2) {
+  let serviceURL = aServiceURL ? aServiceURL : "http://test_service/";
+  let installDate = aInstallDate ? aInstallDate : "1238441400314";
+  let statusText = aStatusText ? aStatusText : "Install Pending";
+  let isCompleteUpdate =
+    typeof aIsCompleteUpdate == "string" ? aIsCompleteUpdate : "true";
+  let channel = aChannel ? aChannel
+                         : gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL);
+  let foregroundDownload =
+    typeof aForegroundDownload == "string" ? aForegroundDownload : "true";
+  let previousAppVersion = aPreviousAppVersion ? "previousAppVersion=\"" +
+                                                 aPreviousAppVersion + "\" "
+                                               : "";
+  return getUpdateString(aType, aName, aDisplayVersion, aAppVersion, aBuildID,
+                         aDetailsURL, aShowPrompt, aShowNeverForVersion,
+                         aPromptWaitTime, aBackgroundInterval, aCustom1, aCustom2) +
+                   " " +
+                   previousAppVersion +
+                   "serviceURL=\"" + serviceURL + "\" " +
+                   "installDate=\"" + installDate + "\" " +
+                   "statusText=\"" + statusText + "\" " +
+                   "isCompleteUpdate=\"" + isCompleteUpdate + "\" " +
+                   "channel=\"" + channel + "\" " +
+                   "foregroundDownload=\"" + foregroundDownload + "\">" +
+              aPatches +
+         "  </update>";
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/sharedUpdateXML.js
@@ -0,0 +1,277 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Helper functions for creating xml strings used by application update tests.
+ *
+ * !IMPORTANT - This file contains everything needed (along with dependencies)
+ * by the updates.sjs file used by the mochitest-chrome tests. Since xpcshell
+ * used by the http server is launched with -v 170 this file must not use
+ * features greater than JavaScript 1.7.
+ */
+
+/* eslint-disable no-undef */
+
+const FILE_SIMPLE_MAR = "simple.mar";
+const SIZE_SIMPLE_MAR = "1031";
+const MD5_HASH_SIMPLE_MAR    = "1f8c038577bb6845d94ccec4999113ee";
+const SHA1_HASH_SIMPLE_MAR   = "5d49a672c87f10f31d7e326349564a11272a028b";
+const SHA256_HASH_SIMPLE_MAR = "1aabbed5b1dd6e16e139afc5b43d479e254e0c26" +
+                               "3c8fb9249c0a1bb93071c5fb";
+const SHA384_HASH_SIMPLE_MAR = "26615014ea034af32ef5651492d5f493f5a7a1a48522e" +
+                               "d24c366442a5ec21d5ef02e23fb58d79729b8ca2f9541" +
+                               "99dd53";
+const SHA512_HASH_SIMPLE_MAR = "922e5ae22081795f6e8d65a3c508715c9a314054179a8" +
+                               "bbfe5f50dc23919ad89888291bc0a07586ab17dd0304a" +
+                               "b5347473601127571c66f61f5080348e05c36b";
+
+const STATE_NONE            = "null";
+const STATE_DOWNLOADING     = "downloading";
+const STATE_PENDING         = "pending";
+const STATE_PENDING_SVC     = "pending-service";
+const STATE_APPLYING        = "applying";
+const STATE_APPLIED         = "applied";
+const STATE_APPLIED_SVC     = "applied-service";
+const STATE_SUCCEEDED       = "succeeded";
+const STATE_DOWNLOAD_FAILED = "download-failed";
+const STATE_FAILED          = "failed";
+
+const LOADSOURCE_ERROR_WRONG_SIZE      = 2;
+const CRC_ERROR                        = 4;
+const READ_ERROR                       = 6;
+const WRITE_ERROR                      = 7;
+const MAR_CHANNEL_MISMATCH_ERROR       = 22;
+const VERSION_DOWNGRADE_ERROR          = 23;
+const INVALID_APPLYTO_DIR_STAGED_ERROR = 72;
+const INVALID_APPLYTO_DIR_ERROR        = 74;
+
+const STATE_FAILED_DELIMETER = ": ";
+
+const STATE_FAILED_LOADSOURCE_ERROR_WRONG_SIZE =
+  STATE_FAILED + STATE_FAILED_DELIMETER + LOADSOURCE_ERROR_WRONG_SIZE;
+const STATE_FAILED_CRC_ERROR =
+  STATE_FAILED + STATE_FAILED_DELIMETER + CRC_ERROR;
+const STATE_FAILED_READ_ERROR =
+  STATE_FAILED + STATE_FAILED_DELIMETER + READ_ERROR;
+const STATE_FAILED_WRITE_ERROR =
+  STATE_FAILED + STATE_FAILED_DELIMETER + WRITE_ERROR;
+const STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR =
+  STATE_FAILED + STATE_FAILED_DELIMETER + MAR_CHANNEL_MISMATCH_ERROR;
+const STATE_FAILED_VERSION_DOWNGRADE_ERROR =
+  STATE_FAILED + STATE_FAILED_DELIMETER + VERSION_DOWNGRADE_ERROR;
+const STATE_FAILED_INVALID_APPLYTO_DIR_STAGED_ERROR =
+  STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_APPLYTO_DIR_STAGED_ERROR;
+const STATE_FAILED_INVALID_APPLYTO_DIR_ERROR =
+  STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_APPLYTO_DIR_ERROR;
+
+/**
+ * Constructs a string representing a remote update xml file.
+ *
+ * @param  aUpdates
+ *         The string representing the update elements.
+ * @return The string representing a remote update xml file.
+ */
+function getRemoteUpdatesXMLString(aUpdates) {
+  return "<?xml version=\"1.0\"?>\n" +
+         "<updates>\n" +
+           aUpdates +
+         "</updates>\n";
+}
+
+/**
+ * Constructs a string representing an update element for a remote update xml
+ * file. See getUpdateString for parameter information not provided below.
+ *
+ * @param  aPatches
+ *         String representing the application update patches.
+ * @return The string representing an update element for an update xml file.
+ */
+function getRemoteUpdateString(aPatches, aType, aName, aDisplayVersion,
+                               aAppVersion, aBuildID, aDetailsURL, aShowPrompt,
+                               aShowNeverForVersion, aPromptWaitTime,
+                               aBackgroundInterval, aCustom1, aCustom2) {
+  return getUpdateString(aType, aName, aDisplayVersion, aAppVersion,
+                         aBuildID, aDetailsURL, aShowPrompt,
+                         aShowNeverForVersion, aPromptWaitTime,
+                         aBackgroundInterval, aCustom1, aCustom2) + ">\n" +
+              aPatches +
+         "  </update>\n";
+}
+
+/**
+ * Constructs a string representing a patch element for a remote update xml
+ * file. See getPatchString for parameter information not provided below.
+ *
+ * @return The string representing a patch element for a remote update xml file.
+ */
+function getRemotePatchString(aType, aURL, aHashFunction, aHashValue, aSize) {
+  return getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) +
+         "/>\n";
+}
+
+/**
+ * Constructs a string representing a local update xml file.
+ *
+ * @param  aUpdates
+ *         The string representing the update elements.
+ * @return The string representing a local update xml file.
+ */
+function getLocalUpdatesXMLString(aUpdates) {
+  if (!aUpdates || aUpdates == "") {
+    return "<updates xmlns=\"http://www.mozilla.org/2005/app-update\"/>";
+  }
+  return ("<updates xmlns=\"http://www.mozilla.org/2005/app-update\">" +
+            aUpdates +
+          "</updates>").replace(/>\s+\n*</g, '><');
+}
+
+/**
+ * Constructs a string representing a patch element for a local update xml file.
+ * See getPatchString for parameter information not provided below.
+ *
+ * @param  aSelected (optional)
+ *         Whether this patch is selected represented or not. The string 'true'
+ *         denotes selected and the string 'false' denotes not selected.
+ *         If not specified it will default to the string 'true'.
+ * @param  aState (optional)
+ *         The patch's state.
+ *         If not specified it will default to STATE_SUCCEEDED.
+ * @return The string representing a patch element for a local update xml file.
+ */
+function getLocalPatchString(aType, aURL, aHashFunction, aHashValue, aSize,
+                             aSelected, aState) {
+  let selected = typeof aSelected == "string" ? aSelected : "true";
+  let state = aState ? aState : STATE_SUCCEEDED;
+  return getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) + " " +
+         "selected=\"" + selected + "\" " +
+         "state=\"" + state + "\"/>\n";
+}
+
+/**
+ * Constructs a string representing an update element for a remote update xml
+ * file.
+ *
+ * @param  aType (optional)
+ *         The update's type which should be major or minor. If not specified it
+ *         will default to 'major'.
+ * @param  aName (optional)
+ *         The update's name.
+ *         If not specified it will default to 'App Update Test'.
+ * @param  aDisplayVersion (optional)
+ *         The update's display version.
+ *         If not specified it will default to 'version #' where # is the value
+ *         of DEFAULT_UPDATE_VERSION.
+ * @param  aAppVersion (optional)
+ *         The update's application version.
+ *         If not specified it will default to the value of
+ *         DEFAULT_UPDATE_VERSION.
+ * @param  aBuildID (optional)
+ *         The update's build id.
+ *         If not specified it will default to '20080811053724'.
+ * @param  aDetailsURL (optional)
+ *         The update's details url.
+ *         If not specified it will default to 'http://test_details/' due to due
+ *         to bug 470244.
+ * @param  aShowPrompt (optional)
+ *         Whether to show the prompt for the update when auto update is
+ *         enabled.
+ *         If not specified it will not be present and the update service will
+ *         default to false.
+ * @param  aShowNeverForVersion (optional)
+ *         Whether to show the 'No Thanks' button in the update prompt.
+ *         If not specified it will not be present and the update service will
+ *         default to false.
+ * @param  aPromptWaitTime (optional)
+ *         Override for the app.update.promptWaitTime preference.
+ * @param  aBackgroundInterval (optional)
+ *         Override for the app.update.download.backgroundInterval preference.
+ * @param  aCustom1 (optional)
+ *         A custom attribute name and attribute value to add to the xml.
+ *         Example: custom1_attribute="custom1 value"
+ *         If not specified it will not be present.
+ * @param  aCustom2 (optional)
+ *         A custom attribute name and attribute value to add to the xml.
+ *         Example: custom2_attribute="custom2 value"
+ *         If not specified it will not be present.
+ * @return The string representing an update element for an update xml file.
+ */
+function getUpdateString(aType, aName, aDisplayVersion, aAppVersion, aBuildID,
+                         aDetailsURL, aShowPrompt, aShowNeverForVersion,
+                         aPromptWaitTime, aBackgroundInterval, aCustom1,
+                         aCustom2) {
+  let type = aType ? aType : "major";
+  let name = aName ? aName : "App Update Test";
+  let displayVersion = aDisplayVersion ? "displayVersion=\"" +
+                                         aDisplayVersion + "\" "
+                                       : "";
+  let appVersion = "appVersion=\"" +
+                   (aAppVersion ? aAppVersion : DEFAULT_UPDATE_VERSION) +
+                   "\" ";
+  let buildID = aBuildID ? aBuildID : "20080811053724";
+  // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244
+//   let detailsURL = aDetailsURL ? "detailsURL=\"" + aDetailsURL + "\" " : "";
+  let detailsURL = "detailsURL=\"" +
+                   (aDetailsURL ? aDetailsURL
+                                : "http://test_details/") + "\" ";
+  let showPrompt = aShowPrompt ? "showPrompt=\"" + aShowPrompt + "\" " : "";
+  let showNeverForVersion = aShowNeverForVersion ? "showNeverForVersion=\"" +
+                                                   aShowNeverForVersion + "\" "
+                                                 : "";
+  let promptWaitTime = aPromptWaitTime ? "promptWaitTime=\"" + aPromptWaitTime +
+                                         "\" "
+                                       : "";
+  let backgroundInterval = aBackgroundInterval ? "backgroundInterval=\"" +
+                                                 aBackgroundInterval + "\" "
+                                               : "";
+  let custom1 = aCustom1 ? aCustom1 + " " : "";
+  let custom2 = aCustom2 ? aCustom2 + " " : "";
+  return "  <update type=\"" + type + "\" " +
+                   "name=\"" + name + "\" " +
+                    displayVersion +
+                    appVersion +
+                    detailsURL +
+                    showPrompt +
+                    showNeverForVersion +
+                    promptWaitTime +
+                    backgroundInterval +
+                    custom1 +
+                    custom2 +
+                   "buildID=\"" + buildID + "\"";
+}
+
+/**
+ * Constructs a string representing a patch element for an update xml file.
+ *
+ * @param  aType (optional)
+ *         The patch's type which should be complete or partial.
+ *         If not specified it will default to 'complete'.
+ * @param  aURL (optional)
+ *         The patch's url to the mar file.
+ *         If not specified it will default to the value of:
+ *         gURLData + FILE_SIMPLE_MAR
+ * @param  aHashFunction (optional)
+ *         The patch's hash function used to verify the mar file.
+ *         If not specified it will default to 'MD5'.
+ * @param  aHashValue (optional)
+ *         The patch's hash value used to verify the mar file.
+ *         If not specified it will default to the value of MD5_HASH_SIMPLE_MAR
+ *         which is the MD5 hash value for the file specified by FILE_SIMPLE_MAR.
+ * @param  aSize (optional)
+ *         The patch's file size for the mar file.
+ *         If not specified it will default to the file size for FILE_SIMPLE_MAR
+ *         specified by SIZE_SIMPLE_MAR.
+ * @return The string representing a patch element for an update xml file.
+ */
+function getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) {
+  let type = aType ? aType : "complete";
+  let url = aURL ? aURL : gURLData + FILE_SIMPLE_MAR;
+  let hashFunction = aHashFunction ? aHashFunction : "MD5";
+  let hashValue = aHashValue ? aHashValue : MD5_HASH_SIMPLE_MAR;
+  let size = aSize ? aSize : SIZE_SIMPLE_MAR;
+  return "    <patch type=\"" + type + "\" " +
+                     "URL=\"" + url + "\" " +
+                     "hashFunction=\"" + hashFunction + "\" " +
+                     "hashValue=\"" + hashValue + "\" " +
+                     "size=\"" + size + "\"";
+}
new file mode 100644
index 0000000000000000000000000000000000000000..b2ccbd8d2552c0ed4c7e941ad543fd8157d5cace
GIT binary patch
literal 1031
zc%1Wf3^HV3V2)=10~U55!3e>O49eg9y`xiZt^4&r=*)b!e+hFL!w<1qzPYNh?ZY*5
z!EYO-LsY_hRGWUqEcpFI|MtZf;_FpT?Y`7}Fx>Z(d7<s+Y_=;*Y`gdIzmh9(z54ED
z$%E6Yqa=36%NkCdKD#vN#HF)OPDBbw7<RsTmGVy7F<_hI-?J;;o<F9Q$LwjiN55;&
z8TDK571R|ce6Bew@oLktZ;3f2Rgc#OJz_cGa-)TtN%dI8`^ys~j)y((E6^^m;`%<h
z_fM6KsKYhe+$aB|1+}f_KAI7}{+yGt>qpUCz2~hu=5>=b=Pomu+rGJL=6vn;%eQjp
zuWhcfNLp-_ZeIMQWO})R$+RxhYdbyIs}oZ-CRAlZ!y*GjR}>@{XQbxj=$51wmoR8C
zP#ZW!Wmp<IMydrzp54B80t16fcmsn#0MkMSF^2VO_Z1WvN+d-tm}HztbCKwhWOHkF
zo7pkvkk^Vw&cB$-oD?NwIGQ(lEh#9?u3X?6BpZ~;A;l`ZU~P}P>tS{sy}}9Wu4ObA
z2^dOPzDlY46MItRp_8-Oy>AW!Y-vh??{5YM{_8+X85kOYmM&8}&!E7dxbhX_#Dx=<
z`S>&|3m=&!D$6U*5n3@fV?o2idv<zHx(XI5oOm$%8^W9=2a4t}Ffh3T%~51vV^CA^
z0h(gSaDkC2rOIfYQ>9MM7v^2lkKUQ+C!(ThAhR4PuuoV06=7h|z7F*_8_3^4%^|8v
z8MbN;HnW<7U8Q&#%oaRc-?4Pgf_#NVU(Rs^iEcmJvS5uuN1pn5-`&%q+YAqwsN4*_
z{4$iC)4?fcIzPlFqkH>ZK{2}on0h*ZbSaQtQd*FbSdv<1q?enRmzf4iN<h)SXtKu0
zvP?xl!UG{24@!XgMX9;@WvMB;X_+~x#UNF?fP^)=s!%XH1juwuNpS-z1*!ZBB(gv%
Q3yM;c^K%PwQcF@9080a99smFU
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/simpleUpdate/update.sjs
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Server side http server script for application update tests.
+ */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const REL_PATH_DATA = "browser/browser/base/content/test/simpleUpdate/";
+
+function getTestDataFile(aFilename) {
+  let file = Cc["@mozilla.org/file/directory_service;1"].
+            getService(Ci.nsIProperties).get("CurWorkD", Ci.nsILocalFile);
+  let pathParts = REL_PATH_DATA.split("/");
+  for (let i = 0; i < pathParts.length; ++i) {
+    file.append(pathParts[i]);
+  }
+  if (aFilename) {
+    file.append(aFilename);
+  }
+  return file;
+}
+
+function loadHelperScript() {
+  let scriptFile = getTestDataFile("sharedUpdateXML.js");
+  let io = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService2);
+  let scriptSpec = io.newFileURI(scriptFile).spec;
+  let scriptloader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+                     getService(Ci.mozIJSSubScriptLoader);
+  scriptloader.loadSubScript(scriptSpec, this);
+}
+loadHelperScript();
+
+const URL_HOST = "http://example.com";
+const URL_PATH_UPDATE_XML = "/browser/browser/base/content/test/simpleUpdate/update.sjs";
+const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML;
+const SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + FILE_SIMPLE_MAR;
+
+const SLOW_MAR_DOWNLOAD_INTERVAL = 100;
+var gTimer;
+
+function handleRequest(aRequest, aResponse) {
+
+  let params = { };
+  if (aRequest.queryString) {
+    params = parseQueryString(aRequest.queryString);
+  }
+
+  let statusCode = params.statusCode ? parseInt(params.statusCode) : 200;
+  let statusReason = params.statusReason ? params.statusReason : "OK";
+  aResponse.setStatusLine(aRequest.httpVersion, statusCode, statusReason);
+  aResponse.setHeader("Cache-Control", "no-cache", false);
+
+  // When a mar download is started by the update service it can finish
+  // downloading before the ui has loaded. By specifying a serviceURL for the
+  // update patch that points to this file and has a slowDownloadMar param the
+  // mar will be downloaded asynchronously which will allow the ui to load
+  // before the download completes.
+  if (params.slowDownloadMar) {
+    aResponse.processAsync();
+    aResponse.setHeader("Content-Type", "binary/octet-stream");
+    aResponse.setHeader("Content-Length", SIZE_SIMPLE_MAR);
+    var continueFile = getTestDataFile("continue");
+    var contents = readFileBytes(getTestDataFile(FILE_SIMPLE_MAR));
+    gTimer = Cc["@mozilla.org/timer;1"].
+             createInstance(Ci.nsITimer);
+    gTimer.initWithCallback(function(aTimer) {
+      if (continueFile.exists()) {
+        gTimer.cancel();
+        aResponse.write(contents);
+        aResponse.finish();
+      }
+    }, SLOW_MAR_DOWNLOAD_INTERVAL, Ci.nsITimer.TYPE_REPEATING_SLACK);
+    return;
+  }
+
+  if (params.uiURL) {
+    let remoteType = "";
+    if (!params.remoteNoTypeAttr && params.uiURL == "BILLBOARD") {
+      remoteType = " " + params.uiURL.toLowerCase() + "=\"1\"";
+    }
+    aResponse.write("<html><head><meta http-equiv=\"content-type\" content=" +
+                    "\"text/html; charset=utf-8\"></head><body" +
+                    remoteType + ">" + params.uiURL +
+                    "<br><br>this is a test mar that will not affect your " +
+                    "build.</body></html>");
+    return;
+  }
+
+  if (params.xmlMalformed) {
+    aResponse.write("xml error");
+    return;
+  }
+
+  if (params.noUpdates) {
+    aResponse.write(getRemoteUpdatesXMLString(""));
+    return;
+  }
+
+  if (params.unsupported) {
+    aResponse.write(getRemoteUpdatesXMLString("  <update type=\"major\" " +
+                                              "unsupported=\"true\" " +
+                                              "detailsURL=\"" + URL_HOST +
+                                              "\"></update>\n"));
+    return;
+  }
+
+
+  let size;
+  let patches = "";
+  if (!params.partialPatchOnly) {
+    size = SIZE_SIMPLE_MAR + (params.invalidCompleteSize ? "1" : "");
+    patches += getRemotePatchString("complete", SERVICE_URL, "SHA512",
+                                    SHA512_HASH_SIMPLE_MAR, size);
+  }
+
+  if (!params.completePatchOnly) {
+    size = SIZE_SIMPLE_MAR + (params.invalidPartialSize ? "1" : "");
+    patches += getRemotePatchString("partial", SERVICE_URL, "SHA512",
+                                    SHA512_HASH_SIMPLE_MAR, size);
+  }
+
+  let type = params.type ? params.type : "major";
+  let name = params.name ? params.name : "App Update Test";
+  let appVersion = params.appVersion ? params.appVersion : "999999.9";
+  let displayVersion = params.displayVersion ? params.displayVersion
+                                             : "version " + appVersion;
+  let buildID = params.buildID ? params.buildID : "01234567890123";
+  // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244
+//  let detailsURL = params.showDetails ? URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS" : null;
+  let detailsURL = URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS";
+  let showPrompt = params.showPrompt ? "true" : null;
+  let showNever = params.showNever ? "true" : null;
+  let promptWaitTime = params.promptWaitTime ? params.promptWaitTime : null;
+
+  let updates = getRemoteUpdateString(patches, type, "App Update Test",
+                                      displayVersion, appVersion, buildID,
+                                      detailsURL, showPrompt, showNever,
+                                      promptWaitTime);
+  aResponse.write(getRemoteUpdatesXMLString(updates));
+}
+
+/**
+ * Helper function to create a JS object representing the url parameters from
+ * the request's queryString.
+ *
+ * @param  aQueryString
+ *         The request's query string.
+ * @return A JS object representing the url parameters from the request's
+ *         queryString.
+ */
+function parseQueryString(aQueryString) {
+  let paramArray = aQueryString.split("&");
+  let regex = /^([^=]+)=(.*)$/;
+  let params = {};
+  for (let i = 0, sz = paramArray.length; i < sz; i++) {
+    let match = regex.exec(paramArray[i]);
+    if (!match) {
+      throw "Bad parameter in queryString!  '" + paramArray[i] + "'";
+    }
+    params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+  }
+
+  return params;
+}
+
+/**
+ * Reads the binary contents of a file and returns it as a string.
+ *
+ * @param  aFile
+ *         The file to read from.
+ * @return The contents of the file as a string.
+ */
+function readFileBytes(aFile) {
+  let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+            createInstance(Ci.nsIFileInputStream);
+  fis.init(aFile, -1, -1, false);
+  let bis = Cc["@mozilla.org/binaryinputstream;1"].
+            createInstance(Ci.nsIBinaryInputStream);
+  bis.setInputStream(fis);
+  let data = [];
+  let count = fis.available();
+  while (count > 0) {
+    let bytes = bis.readByteArray(Math.min(65535, count));
+    data.push(String.fromCharCode.apply(null, bytes));
+    count -= bytes.length;
+    if (bytes.length == 0) {
+      throw "Nothing read from input stream!";
+    }
+  }
+  data.join('');
+  fis.close();
+  return data.toString();
+}
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -17,16 +17,17 @@ MOCHITEST_CHROME_MANIFESTS += [
 BROWSER_CHROME_MANIFESTS += [
     'content/test/alerts/browser.ini',
     'content/test/captivePortal/browser.ini',
     'content/test/general/browser.ini',
     'content/test/newtab/browser.ini',
     'content/test/plugins/browser.ini',
     'content/test/popupNotifications/browser.ini',
     'content/test/referrer/browser.ini',
+    'content/test/simpleUpdate/browser.ini',
     'content/test/social/browser.ini',
     'content/test/tabcrashed/browser.ini',
     'content/test/tabPrompts/browser.ini',
     'content/test/tabs/browser.ini',
     'content/test/urlbar/browser.ini',
     'content/test/webrtc/browser.ini',
 ]
 
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -11,19 +11,31 @@
        noautofocus="true">
   <panelmultiview id="PanelUI-multiView" mainViewId="PanelUI-mainView">
     <panelview id="PanelUI-mainView" context="customizationPanelContextMenu">
       <vbox id="PanelUI-contents-scroller">
         <vbox id="PanelUI-contents" class="panelUI-grid"/>
       </vbox>
 
       <footer id="PanelUI-footer">
-        <toolbarbutton id="PanelUI-update-status"
-                       oncommand="gMenuButtonUpdateBadge.onMenuPanelCommand(event);"
+        <toolbarbutton id="PanelUI-update-available-menu-item"
+                       wrap="true"
+                       label="&updateAvailable.panelUI.label;"
+                       hidden="true"/>
+        <toolbarbutton id="PanelUI-update-manual-menu-item"
                        wrap="true"
+                       label="&updateManual.panelUI.label;"
+                       hidden="true"/>
+        <toolbarbutton id="PanelUI-update-launch-menu-item"
+                       wrap="true"
+                       label="&updateLaunch.panelUI.label;"
+                       hidden="true"/>
+        <toolbarbutton id="PanelUI-update-restart-menu-item"
+                       wrap="true"
+                       label="&updateRestart.panelUI.label;"
                        hidden="true"/>
         <hbox id="PanelUI-footer-fxa">
           <hbox id="PanelUI-fxa-status"
                 defaultlabel="&fxaSignIn.label;"
                 signedinTooltiptext="&fxaSignedIn.tooltip;"
                 tooltiptext="&fxaSignedIn.tooltip;"
                 errorlabel="&fxaSignInError.label;"
                 unverifiedlabel="&fxaUnverified.label;"
@@ -400,8 +412,86 @@
       <description>&panicButton.thankyou.msg1;</description>
       <description>&panicButton.thankyou.msg2;</description>
     </vbox>
   </hbox>
   <button label="&panicButton.thankyou.buttonlabel;"
           id="panic-button-success-closebutton"
           oncommand="PanicButtonNotifier.close()"/>
 </panel>
+
+<panel id="PanelUI-notification-popup"
+       class="popup-notification-panel"
+       type="arrow"
+       position="after_start"
+       hidden="true"
+       orient="vertical"
+       noautofocus="true"
+       noautohide="true"
+       role="alert"/>
+
+<popupnotification id="PanelUI-update-available-notification"
+                   popupid="update-available"
+                   label="&updateAvailable.header.message;"
+                   buttonlabel="&updateAvailable.acceptButton.label;"
+                   buttonaccesskey="&updateAvailable.acceptButton.accesskey;"
+                   closebuttonhidden="true"
+                   secondarybuttonlabel="&updateAvailable.cancelButton.label;"
+                   secondarybuttonaccesskey="&updateAvailable.cancelButton.accesskey;"
+                   dropmarkerhidden="true"
+                   checkboxhidden="true"
+                   hidden="true">
+  <popupnotificationcontent id="update-available-notification-content" orient="vertical">
+    <description id="update-available-description">&updateAvailable.message;
+      <label id="update-available-whats-new" class="text-link" value="&updateAvailable.whatsnew.label;" />
+    </description>
+  </popupnotificationcontent>
+</popupnotification>
+
+<popupnotification id="PanelUI-update-manual-notification"
+                   popupid="update-manual"
+                   label="&updateManual.header.message;"
+                   buttonlabel="&updateManual.acceptButton.label;"
+                   buttonaccesskey="&updateManual.acceptButton.accesskey;"
+                   closebuttonhidden="true"
+                   secondarybuttonlabel="&updateManual.cancelButton.label;"
+                   secondarybuttonaccesskey="&updateManual.cancelButton.accesskey;"
+                   dropmarkerhidden="true"
+                   checkboxhidden="true"
+                   hidden="true">
+  <popupnotificationcontent id="update-manual-notification-content" orient="vertical">
+    <description id="update-manual-description">&updateManual.message;
+      <label id="update-manual-whats-new" class="text-link" value="&updateManual.whatsnew.label;" />
+    </description>
+  </popupnotificationcontent>
+</popupnotification>
+
+<popupnotification id="PanelUI-update-launch-notification"
+                   popupid="update-launch"
+                   label="&updateLaunch.header.message;"
+                   buttonlabel="&updateLaunch.acceptButton.label;"
+                   buttonaccesskey="&updateLaunch.acceptButton.accesskey;"
+                   closebuttonhidden="true"
+                   secondarybuttonlabel="&updateLaunch.cancelButton.label;"
+                   secondarybuttonaccesskey="&updateLaunch.cancelButton.accesskey;"
+                   dropmarkerhidden="true"
+                   checkboxhidden="true"
+                   hidden="true">
+  <popupnotificationcontent id="update-launch-notification-content" orient="vertical">
+    <description id="update-launch-description">&updateLaunch.message;</description>
+  </popupnotificationcontent>
+</popupnotification>
+
+<popupnotification id="PanelUI-update-restart-notification"
+                   popupid="update-restart"
+                   label="&updateRestart.header.message;"
+                   buttonlabel="&updateRestart.acceptButton.label;"
+                   buttonaccesskey="&updateRestart.acceptButton.accesskey;"
+                   closebuttonhidden="true"
+                   secondarybuttonlabel="&updateRestart.cancelButton.label;"
+                   secondarybuttonaccesskey="&updateRestart.cancelButton.accesskey;"
+                   dropmarkerhidden="true"
+                   checkboxhidden="true"
+                   hidden="true">
+  <popupnotificationcontent id="update-restart-notification-content" orient="vertical">
+    <description id="update-restart-description">&updateRestart.message;</description>
+  </popupnotificationcontent>
+</popupnotification>
\ No newline at end of file
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -27,37 +27,46 @@ const PanelUI = {
   get kElements() {
     return {
       contents: "PanelUI-contents",
       mainView: "PanelUI-mainView",
       multiView: "PanelUI-multiView",
       helpView: "PanelUI-helpView",
       menuButton: "PanelUI-menu-button",
       panel: "PanelUI-popup",
-      scroller: "PanelUI-contents-scroller"
+      notificationPanel: "PanelUI-notification-popup",
+      scroller: "PanelUI-contents-scroller",
+      footer: "PanelUI-footer"
     };
   },
 
   _initialized: false,
   init() {
     for (let [k, v] of Object.entries(this.kElements)) {
       // Need to do fresh let-bindings per iteration
       let getKey = k;
       let id = v;
       this.__defineGetter__(getKey, function() {
         delete this[getKey];
         return this[getKey] = document.getElementById(id);
       });
     }
 
+    this.notifications = [];
     this.menuButton.addEventListener("mousedown", this);
     this.menuButton.addEventListener("keypress", this);
     this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this);
+    window.addEventListener("fullscreen", this);
     window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn);
     CustomizableUI.addListener(this);
+
+    for (let event of this.kEvents) {
+      this.notificationPanel.addEventListener(event, this);
+    }
+
     this._initialized = true;
   },
 
   _eventListenersAdded: false,
   _ensureEventListenersAdded() {
     if (this._eventListenersAdded)
       return;
     this._addEventListeners();
@@ -70,16 +79,17 @@ const PanelUI = {
 
     this.helpView.addEventListener("ViewShowing", this._onHelpViewShow);
     this._eventListenersAdded = true;
   },
 
   uninit() {
     for (let event of this.kEvents) {
       this.panel.removeEventListener(event, this);
+      this.notificationPanel.removeEventListener(event, this);
     }
     this.helpView.removeEventListener("ViewShowing", this._onHelpViewShow);
     this.menuButton.removeEventListener("mousedown", this);
     this.menuButton.removeEventListener("keypress", this);
     window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn);
     CustomizableUI.removeListener(this);
     this._overlayScrollListenerBoundFn = null;
   },
@@ -152,68 +162,135 @@ const PanelUI = {
           anchor = aEvent.target;
         }
 
         this.panel.addEventListener("popupshown", function onPopupShown() {
           this.removeEventListener("popupshown", onPopupShown);
           resolve();
         });
 
-        let iconAnchor =
-          document.getAnonymousElementByAttribute(anchor, "class",
-                                                  "toolbarbutton-icon");
-        this.panel.openPopup(iconAnchor || anchor);
+        anchor = this._getPanelAnchor(anchor);
+        this.panel.openPopup(anchor);
       }, (reason) => {
         console.error("Error showing the PanelUI menu", reason);
       });
     });
   },
 
+  showNotification(id, mainAction, secondaryActions = [], options = {}) {
+    let notification = new PanelUINotification(id, mainAction, secondaryActions, options);
+    let existingIndex = this.notifications.findIndex(n => n.id == id);
+    if (existingIndex != -1) {
+      this.notifications.splice(existingIndex, 1);
+    }
+
+    // we don't want to clobber doorhanger notifications just to show a badge,
+    // so don't dismiss any of them and the badge will show once the doorhanger
+    // gets resolved.
+    if (!options.badgeOnly && !options.dismissed) {
+      this.notifications.forEach(n => { n.dismissed = true; });
+    }
+
+    // since notifications are generally somewhat pressing, the ideal case is that
+    // we never have two notifications at once. However, in the event that we do,
+    // it's more likely that the older notification has been sitting around for a
+    // bit, and so we don't want to hide the new notification behind it. Thus,
+    // we want our notifications to behave like a stack instead of a queue.
+    this.notifications.unshift(notification);
+    this._updateNotifications();
+    return notification;
+  },
+
+  showBadgeOnlyNotification(id) {
+    return this.showNotification(id, null, null, { badgeOnly: true });
+  },
+
+  removeNotification(id) {
+    let notifications;
+    if (typeof id == "string") {
+      notifications = this.notifications.filter(n => n.id == id);
+    } else {
+      // if it's not a string, assume RegExp
+      notifications = this.notifications.filter(n => id.test(n.id));
+    }
+
+    notifications.forEach(n => {
+      this._removeNotification(n);
+    });
+    this._updateNotifications();
+  },
+
   /**
    * If the menu panel is being shown, hide it.
    */
   hide() {
     if (document.documentElement.hasAttribute("customizing")) {
       return;
     }
 
     this.panel.hidePopup();
   },
 
   handleEvent(aEvent) {
+    if (aEvent.target == this.notificationPanel) {
+      let doorhanger = this.notifications.find(n => !n.dismissed && !n.options.badgeOnly);
+      switch (aEvent.type) {
+        case "popupshowing":
+          this._notify(doorhanger.id, "doorhanger-showing");
+          break;
+        case "popupshown":
+          this._notify(doorhanger.id, "doorhanger-shown");
+          break;
+      }
+    }
+
     // Ignore context menus and menu button menus showing and hiding:
     if (aEvent.type.startsWith("popup") &&
         aEvent.target != this.panel) {
       return;
     }
     switch (aEvent.type) {
       case "popupshowing":
         this._adjustLabelsForAutoHyphens();
         // Fall through
       case "popupshown":
         // Fall through
       case "popuphiding":
         // Fall through
       case "popuphidden":
+        this._updateNotifications();
         this._updatePanelButton(aEvent.target);
         break;
       case "mousedown":
         if (aEvent.button == 0)
           this.toggle(aEvent);
         break;
       case "keypress":
         this.toggle(aEvent);
         break;
+      case "fullscreen":
+        this._updateNotifications();
+        break;
     }
   },
 
   get isReady() {
     return !!this._isReady;
   },
 
+  get isNotificationPanelOpen() {
+    let panelState = this.notificationPanel.state;
+
+    return panelState == "showing" || panelState == "open";
+  },
+
+  get activeNotification() {
+    return this._activeNotification || null;
+  },
+
   /**
    * Registering the menu panel is done lazily for performance reasons. This
    * method is exposed so that CustomizationMode can force panel-readyness in the
    * event that customization mode is started before the panel has been opened
    * by the user.
    *
    * @param aCustomizing (optional) set to true if this was called while entering
    *        customization mode. If that's the case, we trust that customization
@@ -389,24 +466,23 @@ const PanelUI = {
         panelRemover();
         return;
       }
 
       viewShown = true;
       CustomizableUI.addPanelCloseListeners(tempPanel);
       tempPanel.addEventListener("popuphidden", panelRemover);
 
-      let iconAnchor =
-        document.getAnonymousElementByAttribute(aAnchor, "class",
-                                                "toolbarbutton-icon");
+      let anchor = this._getPanelAnchor(aAnchor);
 
-      if (iconAnchor && aAnchor.id) {
-        iconAnchor.setAttribute("consumeanchor", aAnchor.id);
+      if (aAnchor != anchor && aAnchor.id) {
+        anchor.setAttribute("consumeanchor", aAnchor.id);
       }
-      tempPanel.openPopup(iconAnchor || aAnchor, "bottomcenter topright");
+
+      tempPanel.openPopup(anchor, "bottomcenter topright");
     }
   }),
 
   /**
    * NB: The enable- and disableSingleSubviewPanelAnimations methods only
    * affect the hiding/showing animations of single-subview panels (tempPanel
    * in the showSubView method).
    */
@@ -534,16 +610,225 @@ const PanelUI = {
     quitButton.setAttribute("tooltiptext", tooltipString);
   },
 
   _overlayScrollListenerBoundFn: null,
   _overlayScrollListener(aMQL) {
     ScrollbarSampler.resetSystemScrollbarWidth();
     this._scrollWidth = null;
   },
+
+
+  _updateNotifications() {
+    if (!this.notifications.length) {
+      this._clearAllNotifications();
+      this.notificationPanel.hidePopup();
+      this._activeNotification = null;
+      return;
+    }
+
+    if (window.fullScreen) {
+      this.notificationPanel.hidePopup();
+      return;
+    }
+
+    let doorhangers =
+      this.notifications.filter(n => !n.dismissed && !n.options.badgeOnly);
+
+    if (this.panel.state == "showing" || this.panel.state == "open") {
+      // if the menu is already showing, then we need to dismiss all notifications
+      // since we don't want their doorhangers competing for attention
+      doorhangers.forEach(n => { n.dismissed = true; })
+      this.notificationPanel.hidePopup();
+      this._clearBadges();
+      if (!this.notifications[0].options.badgeOnly) {
+        this._showMenuItem(this.notifications[0]);
+      }
+    } else if (doorhangers.length > 0) {
+      this._clearBadges();
+      this._showNotificationPanel(doorhangers[0]);
+    } else {
+      this.notificationPanel.hidePopup();
+      this._showBadge(this.notifications[0]);
+      this._showMenuItem(this.notifications[0]);
+    }
+  },
+
+  _showNotificationPanel(notification) {
+    this._refreshNotificationPanel(notification);
+
+    if (this.isNotificationPanelOpen) {
+      return;
+    }
+
+    let anchor = this._getPanelAnchor(this.menuButton);
+
+    this.notificationPanel.hidden = false;
+    this.notificationPanel.openPopup(anchor, "bottomcenter topright");
+
+    this._activeNotification = notification;
+  },
+
+  _clearNotificationPanel() {
+    for (let popupnotification of this.notificationPanel.children) {
+      popupnotification.hidden = true;
+      popupnotification.notification = null;
+    }
+  },
+
+  _clearAllNotifications() {
+    this._clearNotificationPanel();
+    this._clearBadges();
+    this._clearMenuItems();
+  },
+
+  _refreshNotificationPanel(notification) {
+    this._clearNotificationPanel();
+
+    let popupnotificationID = this._getPopupId(notification);
+    let popupnotification = document.getElementById(popupnotificationID);
+
+    popupnotification.setAttribute("id", popupnotificationID);
+    popupnotification.setAttribute("buttoncommand", "PanelUI._onNotificationButtonEvent(event, 'buttoncommand');");
+    popupnotification.setAttribute("secondarybuttoncommand", "PanelUI._onNotificationButtonEvent(event, 'secondarybuttoncommand');");
+
+    popupnotification.notification = notification;
+
+    this.notificationPanel.appendChild(popupnotification);
+    popupnotification.hidden = false;
+  },
+
+  _showBadge(notification) {
+    let badgeStatus = this._getBadgeStatus(notification);
+    this.menuButton.setAttribute("badge-status", badgeStatus);
+    this._notify(notification.id, "badge-shown");
+
+    this._activeNotification = notification;
+  },
+
+  _showMenuItem(notification) {
+    this._clearMenuItems();
+
+    let menuItemId = this._getMenuItemId(notification);
+    let menuItem = document.getElementById(menuItemId);
+    if (menuItem) {
+      menuItem.notification = notification;
+      menuItem.setAttribute("oncommand", "PanelUI._onNotificationMenuItemSelected(event)");
+      menuItem.classList.add("PanelUI-notification-menu-item");
+      menuItem.hidden = false;
+      menuItem.fromPanelUINotifications = true;
+      this._notify(notification.id, "menu-item-shown");
+    }
+
+    this._activeNotification = notification;
+  },
+
+  _clearBadges() {
+    this.menuButton.removeAttribute("badge-status");
+  },
+
+  _clearMenuItems() {
+    for (let child of this.footer.children) {
+      if (child.fromPanelUINotifications) {
+        child.notification = null;
+        child.hidden = true;
+      }
+    }
+  },
+
+  _removeNotification(notification) {
+    // This notification may already be removed, in which case let's just fail
+    // silently.
+    let notifications = this.notifications;
+    if (!notifications)
+      return;
+
+    var index = notifications.indexOf(notification);
+    if (index == -1)
+      return;
+
+    // remove the notification
+    notifications.splice(index, 1);
+  },
+
+  _onNotificationButtonEvent(event, type) {
+    let notificationEl = getNotificationFromElement(event.originalTarget);
+
+    if (!notificationEl)
+      throw "PanelUI._onNotificationButtonEvent: couldn't find notification element";
+
+    if (!notificationEl.notification)
+      throw "PanelUI._onNotificationButtonEvent: couldn't find notification";
+
+    let notification = notificationEl.notification;
+
+    let action = notification.mainAction;
+
+    if (type == "secondarybuttoncommand") {
+      action = notification.secondaryActions[0];
+    }
+
+    let dismiss = true;
+    if (action) {
+      try {
+        action.callback();
+        if (action === notification.mainAction) {
+          this._notify(notification.id, "main-action");
+        } else {
+          this._notify(notification.id, "secondary-action");
+        }
+      } catch (error) {
+        Cu.reportError(error);
+      }
+
+      dismiss = action.dismiss;
+    }
+
+    if (dismiss) {
+      notification.dismissed = true;
+    } else {
+      this._removeNotification(notification);
+    }
+    this._updateNotifications();
+  },
+
+  _onNotificationMenuItemSelected(event) {
+    let target = event.originalTarget;
+    if (!target.notification)
+      throw "menucommand target has no associated action/notification";
+
+    event.stopPropagation();
+
+    try {
+      target.notification.mainAction.callback();
+      this._notify(notification.id, "main-action");
+    } catch (error) {
+      Cu.reportError(error);
+    }
+
+    this._removeNotification(target.notification);
+    this._updateNotifications();
+  },
+
+  _getPopupId(notification) { return "PanelUI-" + notification.id + "-notification"; },
+
+  _getBadgeStatus(notification) { return notification.id; },
+
+  _getMenuItemId(notification) { return "PanelUI-" + notification.id + "-menu-item"; },
+
+  _getPanelAnchor(candidate) {
+    let iconAnchor =
+      document.getAnonymousElementByAttribute(candidate, "class",
+                                              "toolbarbutton-icon");
+    return iconAnchor || candidate;
+  },
+
+  _notify(topic, status) {
+    Services.obs.notifyObservers(null, "panelUI-" + topic, status);
+  }
 };
 
 XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
 
 /**
  * Gets the currently selected locale for display.
  * @return  the selected locale or "en-US" if none is selected
  */
@@ -551,8 +836,29 @@ function getLocale() {
   try {
     let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]
                            .getService(Ci.nsIXULChromeRegistry);
     return chromeRegistry.getSelectedLocale("browser");
   } catch (ex) {
     return "en-US";
   }
 }
+
+function PanelUINotification(id, mainAction, secondaryActions = [], options = {}) {
+  this.id = id;
+  this.mainAction = mainAction;
+  this.secondaryActions = secondaryActions;
+  this.options = options;
+  this.dismissed = this.options.dismissed || false;
+}
+
+function getNotificationFromElement(aElement) {
+  // Need to find the associated notification object, which is a bit tricky
+  // since it isn't associated with the element directly - this is kind of
+  // gross and very dependent on the structure of the popupnotification
+  // binding's content.
+  let notificationEl;
+  let parent = aElement;
+  while (parent && (parent = aElement.ownerDocument.getBindingParent(parent))) {
+    notificationEl = parent;
+  }
+  return notificationEl;
+}
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -145,10 +145,11 @@ tags = fullscreen
 skip-if = os == "mac"
 [browser_1087303_button_preferences.js]
 [browser_1089591_still_customizable_after_reset.js]
 [browser_1096763_seen_widgets_post_reset.js]
 [browser_1161838_inserted_new_default_buttons.js]
 [browser_bootstrapped_custom_toolbar.js]
 [browser_customizemode_contextmenu_menubuttonstate.js]
 [browser_panel_toggle.js]
+[browser_panelUINotifications.js]
 [browser_switch_to_customize_mode.js]
 [browser_check_tooltips_in_navbar.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_panelUINotifications.js
@@ -0,0 +1,272 @@
+"use strict";
+
+/**
+ * Tests that when we click on the main call-to-action of the doorhanger, the provided
+ * action is called, and the doorhanger removed.
+ */
+add_task(function* testMainActionCalled() {
+  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+    let doc = browser.ownerDocument;
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    let mainActionCalled = false;
+    let mainAction = {
+      callback: () => { mainActionCalled = true; }
+    };
+    PanelUI.showNotification("update-manual", mainAction);
+
+    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+    let notifications = PanelUI.notificationPanel.children;
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+    let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+    mainActionButton.click();
+
+    ok(mainActionCalled, "Main action callback was called");
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+});
+
+/**
+ * This tests that when we click the secondary action for a notification,
+ * it will display the badge for that notification on the PanelUI menu button.
+ * Once we click on this button, we should see an item in the menu which will
+ * call our main action.
+ */
+add_task(function* testSecondaryActionWorkflow() {
+  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+    let doc = browser.ownerDocument;
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+    let mainActionCalled = false;
+    let mainAction = {
+      callback: () => { mainActionCalled = true; },
+    };
+    PanelUI.showNotification("update-manual", mainAction);
+
+    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+    let notifications = PanelUI.notificationPanel.children;
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+    let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+    secondaryActionButton.click();
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+    is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
+
+    yield PanelUI.show();
+    isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is hidden on PanelUI button.");
+    let menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
+    is(menuItem.hidden, false, "update-manual menu item is showing.");
+
+    yield PanelUI.hide();
+    is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is shown on PanelUI button.");
+
+    yield PanelUI.show();
+    menuItem.click();
+    ok(mainActionCalled, "Main action callback was called");
+
+    PanelUI.removeNotification(/.*/);
+  });
+});
+
+/**
+ * We want to ensure a few things with this:
+ * - Adding a doorhanger will make a badge disappear
+ * - once the notification for the doorhanger is resolved (removed, not just dismissed),
+ *   then we display any other badges that are remaining.
+ */
+add_task(function* testInteractionWithBadges() {
+  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+    let doc = browser.ownerDocument;
+
+    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+    let mainActionCalled = false;
+    let mainAction = {
+      callback: () => { mainActionCalled = true; },
+    };
+    PanelUI.showNotification("update-manual", mainAction);
+
+    isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
+    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+    let notifications = PanelUI.notificationPanel.children;
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+    let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+    secondaryActionButton.click();
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+    is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is displaying on PanelUI button.");
+
+    yield PanelUI.show();
+    isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "Badge is hidden on PanelUI button.");
+    let menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
+    is(menuItem.hidden, false, "update-manual menu item is showing.");
+
+    menuItem.click();
+    ok(mainActionCalled, "Main action callback was called");
+
+    is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
+    PanelUI.removeNotification(/.*/);
+    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+});
+
+/**
+ * This tests that adding a badge will not dismiss any existing doorhangers.
+ */
+add_task(function* testAddingBadgeWhileDoorhangerIsShowing() {
+  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+    let doc = browser.ownerDocument;
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    let mainActionCalled = false;
+    let mainAction = {
+      callback: () => { mainActionCalled = true; }
+    };
+    PanelUI.showNotification("update-manual", mainAction);
+    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+
+    isnot(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is hidden on PanelUI button.");
+    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+    let notifications = PanelUI.notificationPanel.children;
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    let doorhanger = notifications[0];
+    is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+    let mainActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "button");
+    mainActionButton.click();
+
+    ok(mainActionCalled, "Main action callback was called");
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Fxa badge is shown on PanelUI button.");
+    PanelUI.removeNotification(/.*/);
+    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+});
+
+/**
+ * Tests that badges operate like a stack.
+ */
+add_task(function* testMultipleBadges() {
+  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+    let doc = browser.ownerDocument;
+    let menuButton = doc.getElementById("PanelUI-menu-button");
+
+    is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+    is(menuButton.hasAttribute("badge"), false, "Should not have the badge attribute set");
+
+    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
+
+    PanelUI.showBadgeOnlyNotification("update-succeeded");
+    is(menuButton.getAttribute("badge-status"), "update-succeeded", "Should have update-succeeded badge status (update > fxa)");
+
+    PanelUI.showBadgeOnlyNotification("update-failed");
+    is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
+
+    PanelUI.showBadgeOnlyNotification("download-severe");
+    is(menuButton.getAttribute("badge-status"), "download-severe", "Should have download-severe badge status");
+
+    PanelUI.showBadgeOnlyNotification("download-warning");
+    is(menuButton.getAttribute("badge-status"), "download-warning", "Should have download-warning badge status");
+
+    PanelUI.removeNotification(/^download-/);
+    is(menuButton.getAttribute("badge-status"), "update-failed", "Should have update-failed badge status");
+
+    PanelUI.removeNotification(/^update-/);
+    is(menuButton.getAttribute("badge-status"), "fxa-needs-authentication", "Should have fxa-needs-authentication badge status");
+
+    PanelUI.removeNotification(/^fxa-/);
+    is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+
+    yield PanelUI.show();
+    is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status (Hamburger menu opened)");
+    PanelUI.hide();
+
+    PanelUI.showBadgeOnlyNotification("fxa-needs-authentication");
+    PanelUI.showBadgeOnlyNotification("update-succeeded");
+    PanelUI.removeNotification(/.*/);
+    is(menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+});
+
+/**
+ * Tests that non-badges also operate like a stack.
+ */
+add_task(function* testMultipleNonBadges() {
+  yield BrowserTestUtils.withNewTab("about:blank", function*(browser) {
+    let doc = browser.ownerDocument;
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+
+    let updateManualAction = {
+        called: false,
+        callback: () => { updateManualAction.called = true; },
+    };
+    let updateRestartAction = {
+        called: false,
+        callback: () => { updateRestartAction.called = true; },
+    };
+
+    PanelUI.showNotification("update-manual", updateManualAction);
+
+    let notifications;
+    let doorhanger;
+
+    isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
+    notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    doorhanger = notifications[0];
+    is(doorhanger.id, "PanelUI-update-manual-notification", "PanelUI is displaying the update-manual notification.");
+
+    PanelUI.showNotification("update-restart", updateRestartAction);
+
+    isnot(PanelUI.notificationPanel.state, "closed", "Doorhanger is showing.");
+    notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+    is(notifications.length, 1, "PanelUI doorhanger is only displaying one notification.");
+    doorhanger = notifications[0];
+    is(doorhanger.id, "PanelUI-update-restart-notification", "PanelUI is displaying the update-restart notification.");
+
+    let secondaryActionButton = doc.getAnonymousElementByAttribute(doorhanger, "anonid", "secondarybutton");
+    secondaryActionButton.click();
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.getAttribute("badge-status"), "update-restart", "update-restart badge is displaying on PanelUI button.");
+
+    let menuItem;
+
+    yield PanelUI.show();
+    isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-restart", "update-restart badge is hidden on PanelUI button.");
+    menuItem = doc.getElementById("PanelUI-update-restart-menu-item");
+    is(menuItem.hidden, false, "update-restart menu item is showing.");
+
+    menuItem.click();
+    ok(updateRestartAction.called, "update-restart main action callback was called");
+
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "update-manual badge is displaying on PanelUI button.");
+
+    yield PanelUI.show();
+    isnot(PanelUI.menuButton.getAttribute("badge-status"), "update-manual", "update-manual badge is hidden on PanelUI button.");
+    menuItem = doc.getElementById("PanelUI-update-manual-menu-item");
+    is(menuItem.hidden, false, "update-manual menu item is showing.");
+
+    menuItem.click();
+    ok(updateManualAction.called, "update-manual main action callback was called");
+  });
+});
+
--- a/browser/components/downloads/content/indicator.js
+++ b/browser/components/downloads/content/indicator.js
@@ -470,23 +470,23 @@ const DownloadsIndicatorView = {
       // Check if the downloads button is in the menu panel, to determine which
       // button needs to get a badge.
       let widgetGroup = CustomizableUI.getWidget("downloads-button");
       let inMenu = widgetGroup.areaType == CustomizableUI.TYPE_MENU_PANEL;
 
       if (aValue == DownloadsCommon.ATTENTION_NONE) {
         this.indicator.removeAttribute("attention");
         if (inMenu) {
-          gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD);
+          PanelUI.removeNotification(/^download-/);
         }
       } else {
         this.indicator.setAttribute("attention", aValue);
         if (inMenu) {
           let badgeClass = "download-" + aValue;
-          gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_DOWNLOAD, badgeClass);
+          PanelUI.showBadgeOnlyNotification(badgeClass);
         }
       }
     }
     return aValue;
   },
   _attention: DownloadsCommon.ATTENTION_NONE,
 
   //////////////////////////////////////////////////////////////////////////////
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -884,8 +884,45 @@ you can use these alternative items. Oth
 <!ENTITY panicButton.view.forgetButton            "Forget!">
 
 <!ENTITY panicButton.thankyou.msg1                "Your recent history is cleared.">
 <!ENTITY panicButton.thankyou.msg2                "Safe browsing!">
 <!ENTITY panicButton.thankyou.buttonlabel         "Thanks!">
 
 <!ENTITY emeLearnMoreContextMenu.label            "Learn more about DRM…">
 <!ENTITY emeLearnMoreContextMenu.accesskey        "D">
+
+<!ENTITY updateAvailable.message "Update your &brandShorterName; for the latest in speed and privacy.">
+<!ENTITY updateAvailable.whatsnew.label "See what's new.">
+<!ENTITY updateAvailable.whatsnew.href "http://www.mozilla.org/">
+<!ENTITY updateAvailable.header.message "A new &brandShorterName; update is available.">
+<!ENTITY updateAvailable.acceptButton.label "Download Update">
+<!ENTITY updateAvailable.acceptButton.accesskey "D">
+<!ENTITY updateAvailable.cancelButton.label "Not Now">
+<!ENTITY updateAvailable.cancelButton.accesskey "N">
+<!ENTITY updateAvailable.panelUI.label "Download &brandShorterName; update">
+
+<!ENTITY updateManual.message "Download a fresh copy of &brandShorterName; and we'll help you to install it.">
+<!ENTITY updateManual.whatsnew.label "See what's new.">
+<!ENTITY updateManual.whatsnew.href "http://www.mozilla.org/">
+<!ENTITY updateManual.header.message "&brandShorterName; can't update to the latest version.">
+<!ENTITY updateManual.acceptButton.label "Download &brandShorterName;">
+<!ENTITY updateManual.acceptButton.accesskey "D">
+<!ENTITY updateManual.cancelButton.label "Not Now">
+<!ENTITY updateManual.cancelButton.accesskey "N">
+<!ENTITY updateManual.panelUI.label "Download a fresh copy of &brandShorterName;">
+
+<!ENTITY updateRestart.message "After a quick restart, &brandShorterName; will restore all your open tabs and windows.">
+<!ENTITY updateRestart.header.message "Restart &brandShorterName; to apply update.">
+<!ENTITY updateRestart.acceptButton.label "Restart and Restore">
+<!ENTITY updateRestart.acceptButton.accesskey "R">
+<!ENTITY updateRestart.cancelButton.label "Not Now">
+<!ENTITY updateRestart.cancelButton.accesskey "N">
+<!ENTITY updateRestart.panelUI.label "Restart &brandShorterName; to apply update">
+
+<!ENTITY updateLaunch.message "Once you launch the installer, you'll have the latest version of &brandShorterName;.">
+<!ENTITY updateLaunch.header.message "Launch the &brandShorterName; installer you just downloaded.">
+<!ENTITY updateLaunch.acceptButton.label "Launch Installer">
+<!ENTITY updateLaunch.acceptButton.accesskey "L">
+<!ENTITY updateLaunch.cancelButton.label "Not Now">
+<!ENTITY updateLaunch.cancelButton.accesskey "N">
+<!ENTITY updateLaunch.panelUI.label "Launch &brandShorterName; installer">
+
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -636,25 +636,16 @@ flashHang.helpButton.accesskey = L
 # be replaced with a hyperlink containing the text defined in customizeTips.tip0.learnMore.
 customizeTips.tip0 = %1$S: You can customize %2$S to work the way you do. Simply drag any of the above to the menu or toolbar. %3$S about customizing %2$S.
 customizeTips.tip0.hint = Hint
 customizeTips.tip0.learnMore = Learn more
 
 # LOCALIZATION NOTE (customizeMode.tabTitle): %S is brandShortName
 customizeMode.tabTitle = Customize %S
 
-# LOCALIZATION NOTE(appmenu.*.description, appmenu.*.label): these are used for
-# the appmenu labels and buttons that appear when an update is staged for
-# installation or a background update has failed and a manual download is required.
-# %S is brandShortName
-appmenu.restartNeeded.description = Restart %S to apply updates
-appmenu.updateFailed.description = Background update failed, please download update
-appmenu.restartBrowserButton.label = Restart %S
-appmenu.downloadUpdateButton.label = Download Update
-
 # LOCALIZATION NOTE : FILE Reader View is a feature name and therefore typically used as a proper noun.
 
 readingList.promo.firstUse.readerView.title = Reader View
 readingList.promo.firstUse.readerView.body = Remove clutter so you can focus exactly on what you want to read.
 
 # LOCALIZATION NOTE (appMenuRemoteTabs.mobilePromo.text2):
 # %1$S will be replaced with a link, the text of which is
 # appMenuRemoteTabs.mobilePromo.android and the link will be to
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -105,24 +105,44 @@
   background-size: contain;
   border: none;
 }
 
 #PanelUI-menu-button[badge-status="download-success"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
   display: none;
 }
 
-#PanelUI-menu-button[badge-status="update-succeeded"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
+#PanelUI-menu-button[badge-status="update-available"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
+#PanelUI-menu-button[badge-status="update-manual"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
+#PanelUI-menu-button[badge-status="update-launch"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
+#PanelUI-menu-button[badge-status="update-restart"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
   background: #74BF43 url(chrome://browser/skin/update-badge.svg) no-repeat center;
-  height: 13px;
+  border-radius: 50%;
+  box-shadow: none;
+  border: 1px solid -moz-dialog;
+  /* "!important" is necessary to override the rule in toolbarbutton.css */
+  margin: -9px 0 0 !important;
+  margin-inline-end: -6px !important;
+  min-width: 16px;
+  min-height: 16px;
 }
 
-#PanelUI-menu-button[badge-status="update-failed"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
-  background: #D90000 url(chrome://browser/skin/update-badge-failed.svg) no-repeat center;
-  height: 13px;
+#PanelUI-update-restart-menu-item::after,
+#PanelUI-update-download-menu-item::after,
+#PanelUI-update-launch-menu-item::after,
+#PanelUI-update-manual-menu-item::after {
+  background: #74BF43 url(chrome://browser/skin/update-badge.svg) no-repeat center;
+  border-radius: 50%;
+}
+
+#PanelUI-update-restart-menu-item,
+#PanelUI-update-download-menu-item,
+#PanelUI-update-launch-menu-item,
+#PanelUI-update-manual-menu-item {
+  list-style-image: url(chrome://branding/content/icon16.png);
 }
 
 #PanelUI-menu-button[badge-status="download-warning"] > .toolbarbutton-badge-stack > .toolbarbutton-badge,
 #PanelUI-menu-button[badge-status="fxa-needs-authentication"] > .toolbarbutton-badge-stack > .toolbarbutton-badge {
   box-shadow: none;
   filter: drop-shadow(0 1px 0 hsla(206, 50%, 10%, .15));
 }
 
@@ -432,25 +452,25 @@ toolbaritem[cui-areatype="menu-panel"][s
 toolbaritem[cui-areatype="menu-panel"][sdkstylewidget="true"] > iframe {
   margin: 4px auto;
 }
 
 #PanelUI-multiView[viewtype="subview"] > .panel-viewcontainer > .panel-viewstack > .panel-mainview >  #PanelUI-mainView {
   background-color: var(--arrowpanel-dimmed);
 }
 
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-contents-scroller > #PanelUI-contents > .panel-wide-item,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-contents-scroller > #PanelUI-contents > .toolbarbutton-1:not([panel-multiview-anchor="true"]),
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-update-status,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-status > #PanelUI-fxa-avatar,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-status > #PanelUI-fxa-label,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-fxa > #PanelUI-fxa-icon,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > toolbarseparator,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > #PanelUI-customize,
-#PanelUI-multiView[viewtype="subview"] #PanelUI-mainView > #PanelUI-footer > #PanelUI-footer-inner > #PanelUI-help:not([panel-multiview-anchor="true"]) {
+#PanelUI-multiView[viewtype="subview"] #PanelUI-contents > .panel-wide-item,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-contents > .toolbarbutton-1:not([panel-multiview-anchor="true"]),
+#PanelUI-multiView[viewtype="subview"] .PanelUI-notification-menu-item,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-fxa-avatar,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-fxa-label,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-fxa-icon,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-footer-inner > toolbarseparator,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-customize,
+#PanelUI-multiView[viewtype="subview"] #PanelUI-help:not([panel-multiview-anchor="true"]) {
   opacity: .5;
 }
 
 /*
  * XXXgijs: this is a workaround for a layout issue that was caused by these iframes,
  * which was affecting subview display. Because of this, we're hiding the iframe *only*
  * when displaying a subview. The discerning user might notice this, but it's not nearly
  * as bad as the brokenness.
@@ -556,37 +576,24 @@ toolbarpaletteitem[place="palette"] > to
   width: 47px;
   padding-top: 1px;
   display: block;
   text-align: center;
   position: relative;
   top: 25%;
 }
 
-#PanelUI-update-status[update-status]::after {
+.PanelUI-notification-menu-item::after {
   content: "";
-  width: 14px;
-  height: 14px;
+  width: 16px;
+  height: 16px;
   margin-inline-end: 16.5px;
-  box-shadow: 0px 1px 0px rgba(255,255,255,.2) inset, 0px -1px 0px rgba(0,0,0,.1) inset, 0px 1px 0px rgba(12,27,38,.2);
-  border-radius: 2px;
-  background-size: contain;
   display: -moz-box;
 }
 
-#PanelUI-update-status[update-status="succeeded"]::after {
-  background-image: url(chrome://browser/skin/update-badge.svg);
-  background-color: #74BF43;
-}
-
-#PanelUI-update-status[update-status="failed"]::after {
-  background-image: url(chrome://browser/skin/update-badge-failed.svg);
-  background-color: #D90000;
-}
-
 #PanelUI-fxa-status {
   display: flex;
   flex: 1 1 0%;
   width: 1px;
 }
 
 #PanelUI-footer-inner,
 #PanelUI-footer-fxa:not([hidden]) {
@@ -607,17 +614,17 @@ toolbarpaletteitem[place="palette"] > to
   -moz-appearance: none;
 }
 
 #PanelUI-footer-inner:hover > toolbarseparator,
 #PanelUI-footer-fxa:hover > toolbarseparator {
   margin: 0;
 }
 
-#PanelUI-update-status,
+.PanelUI-notification-menu-item,
 #PanelUI-help,
 #PanelUI-fxa-label,
 #PanelUI-fxa-icon,
 #PanelUI-customize,
 #PanelUI-quit {
   margin: 0;
   padding: 11px 0;
   box-sizing: border-box;
@@ -625,49 +632,46 @@ toolbarpaletteitem[place="palette"] > to
   -moz-appearance: none;
   box-shadow: none;
   border: none;
   border-radius: 0;
   transition: background-color;
   -moz-box-orient: horizontal;
 }
 
-#PanelUI-update-status {
+.PanelUI-notification-menu-item {
   border-top: 1px solid var(--panel-separator-color);
-}
-
-#PanelUI-update-status {
   border-bottom: 1px solid transparent;
   margin-bottom: -1px;
 }
 
-#PanelUI-update-status > .toolbarbutton-text {
+.PanelUI-notification-menu-item > .toolbarbutton-text {
   width: 0; /* Fancy cropping solution for flexbox. */
 }
 
 #PanelUI-help,
 #PanelUI-quit {
   min-width: 46px;
 }
 
-#PanelUI-update-status > .toolbarbutton-text,
+.PanelUI-notification-menu-item > .toolbarbutton-text,
 #PanelUI-fxa-label > .toolbarbutton-text,
 #PanelUI-customize > .toolbarbutton-text {
   margin: 0;
   padding: 0 6px;
   text-align: start;
 }
 
 #PanelUI-help > .toolbarbutton-text,
 #PanelUI-quit > .toolbarbutton-text,
 #PanelUI-fxa-avatar > .toolbarbutton-text {
   display: none;
 }
 
-#PanelUI-update-status > .toolbarbutton-icon,
+.PanelUI-notification-menu-item > .toolbarbutton-icon,
 #PanelUI-fxa-label > .toolbarbutton-icon,
 #PanelUI-fxa-icon > .toolbarbutton-icon,
 #PanelUI-customize > .toolbarbutton-icon,
 #PanelUI-help > .toolbarbutton-icon,
 #PanelUI-quit > .toolbarbutton-icon {
   margin-inline-end: 0;
 }
 
@@ -683,26 +687,24 @@ toolbarpaletteitem[place="palette"] > to
   border-inline-start-style: none;
 }
 
 #PanelUI-footer-fxa[fxaprofileimage="set"] > #PanelUI-fxa-status > #PanelUI-fxa-label,
 #PanelUI-footer-fxa[fxaprofileimage="enabled"]:not([fxastatus="error"]) > #PanelUI-fxa-status > #PanelUI-fxa-label {
   padding-inline-start: 0px;
 }
 
-#PanelUI-update-status {
+/* descend from #PanelUI-footer to add specificity, or else the
+   padding-inline-start will be overridden */
+#PanelUI-footer > .PanelUI-notification-menu-item {
   width: calc(@menuPanelWidth@ + 30px);
   padding-inline-start: 15px;
   border-inline-start-style: none;
 }
 
-#PanelUI-update-status {
-  list-style-image: url(chrome://branding/content/icon16.png);
-}
-
 #PanelUI-fxa-label,
 #PanelUI-fxa-icon {
   list-style-image: url(chrome://browser/skin/sync-horizontalbar.png);
 }
 
 #PanelUI-remotetabs {
   --panel-ui-sync-illustration-height: 157.5px;
 }
@@ -939,44 +941,29 @@ toolbarpaletteitem[place="palette"] > to
   background-color: hsl(42,94%,85%);
 }
 
 #PanelUI-footer-fxa[fxastatus="error"] > #PanelUI-fxa-status:hover:active {
   background-color: hsl(42,94%,82%);
   box-shadow: 0 1px 0 hsla(210,4%,10%,.05) inset;
 }
 
-#PanelUI-update-status {
+.PanelUI-notification-menu-item {
   color: black;
-}
-
-#PanelUI-update-status[update-status="succeeded"] {
   background-color: hsla(96,65%,75%,.5);
 }
 
-#PanelUI-update-status[update-status="succeeded"]:not([disabled]):hover {
+.PanelUI-notification-menu-item:not([disabled]):hover {
   background-color: hsla(96,65%,75%,.8);
 }
 
-#PanelUI-update-status[update-status="succeeded"]:not([disabled]):hover:active {
+.PanelUI-notification-menu-item:not([disabled]):hover:active {
   background-color: hsl(96,65%,75%);
 }
 
-#PanelUI-update-status[update-status="failed"] {
-  background-color: hsla(359,69%,84%,.5);
-}
-
-#PanelUI-update-status[update-status="failed"]:not([disabled]):hover {
-  background-color: hsla(359,69%,84%,.8);
-}
-
-#PanelUI-update-status[update-status="failed"]:not([disabled]):hover:active {
-  background-color: hsl(359,69%,84%);
-}
-
 #PanelUI-quit:not([disabled]):hover {
   background-color: #d94141;
   outline-color: #c23a3a;
 }
 
 #PanelUI-quit:not([disabled]):hover:active {
   background-color: #ad3434;
   outline-color: #992e2e;
@@ -1670,17 +1657,20 @@ menuitem[checked="true"].subviewbutton >
   }
 
   #PanelUI-help[panel-multiview-anchor="true"]:-moz-locale-dir(rtl)::after,
   toolbarbutton[panel-multiview-anchor="true"]:-moz-locale-dir(rtl) {
     background-image: url(chrome://browser/skin/customizableui/subView-arrow-back-inverted-rtl@2x.png),
                       linear-gradient(rgba(255,255,255,0.3), transparent);
   }
 
-  #PanelUI-update-status {
+  #PanelUI-update-restart-menu-item,
+  #PanelUI-update-download-menu-item,
+  #PanelUI-update-launch-menu-item,
+  #PanelUI-update-manual-menu-item {
     list-style-image: url(chrome://branding/content/icon32.png);
   }
 
   #PanelUI-fxa-label,
   #PanelUI-fxa-icon {
     list-style-image: url(chrome://browser/skin/sync-horizontalbar@2x.png);
   }
 
@@ -1707,17 +1697,17 @@ menuitem[checked="true"].subviewbutton >
   #PanelUI-fxa-label,
   #PanelUI-fxa-icon,
   #PanelUI-customize,
   #PanelUI-help,
   #PanelUI-quit {
     -moz-image-region: rect(0, 32px, 32px, 0);
   }
 
-  #PanelUI-update-status > .toolbarbutton-icon,
+  .PanelUI-notification-menu-item > .toolbarbutton-icon,
   #PanelUI-fxa-label > .toolbarbutton-icon,
   #PanelUI-fxa-icon > .toolbarbutton-icon,
   #PanelUI-customize > .toolbarbutton-icon,
   #PanelUI-help > .toolbarbutton-icon,
   #PanelUI-quit > .toolbarbutton-icon {
     width: 16px;
   }
 
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -253,17 +253,17 @@ html|*#webRTC-previewVideo {
 
 .plugin-icon.plugin-blocked {
   list-style-image: url(chrome://browser/skin/notification-icons.svg#plugin-blocked);
 }
 
 #notification-popup-box[hidden] {
   /* Override display:none to make the pluginBlockedNotification animation work
      when showing the notification repeatedly. */
-  display: -moz-box;
+  display: -moz-box;j
   visibility: collapse;
 }
 
 #plugins-notification-icon.plugin-blocked[showing] {
   animation: pluginBlockedNotification 500ms ease 0s 5 alternate both;
 }
 
 @keyframes pluginBlockedNotification {
@@ -315,8 +315,17 @@ html|*#webRTC-previewVideo {
     -moz-image-region: rect(0px, 32px, 32px, 0px);
   }
 
   .translation-icon.in-use {
     -moz-image-region: rect(0px, 64px, 32px, 32px);
   }
 }
 %endif
+
+/* UPDATE */
+.popup-notification-icon[popupid="update-available"],
+.popup-notification-icon[popupid="update-manual"],
+.popup-notification-icon[popupid="update-launch"],
+.popup-notification-icon[popupid="update-restart"] {
+  background: #74BF43 url(chrome://browser/skin/notification-icons.svg#update) no-repeat center;
+  border-radius: 50%;
+}
\ No newline at end of file
--- a/browser/themes/shared/notification-icons.svg
+++ b/browser/themes/shared/notification-icons.svg
@@ -40,16 +40,22 @@
     }
 
     #camera-indicator,
     #microphone-indicator,
     #screen-indicator {
       fill: white;
       fill-opacity: 1;
     }
+
+    #update-icon {
+      stroke: #fff;
+      stroke-width: 3px;
+      stroke-linecap: round;
+    }
   </style>
 
   <defs>
     <path id="camera-icon" d="m 2,23 a 3,3 0 0 0 3,3 l 14,0 a 3,3 0 0 0 3,-3 l 0,-4 6,5.5 c 0.5,0.5 1,0.7 2,0.5 l 0,-18 c -1,-0.2 -1.5,0 -2,0.5 l -6,5.5 0,-4 a 3,3 0 0 0 -3,-3 l -14,0 a 3,3 0 0 0 -3,3 z" />
     <path id="desktop-notification-icon" d="m 2,20 a 4,4 0 0 0 4,4 l 13,0 7,7 0,-7 a 4,4 0 0 0 4,-4 l 0,-12 a 4,4 0 0 0 -4,-4 l -20,0 a 4,4 0 0 0 -4,4 z m 5,-2 a 1,1 0 1 1 0,-2 l 10,0 a 1,1 0 1 1 0,2 z m 0,-4 a 1,1 0 1 1 0,-2 l 14,0 a 1,1 0 1 1 0,2 z m 0,-4 a 1,1 0 1 1 0,-2 l 18,0 a 1,1 0 1 1 0,2 z" />
     <path id="geo-linux-icon" d="m 2,15.9 a 14,14 0 1 1 0,0.2 z m 4,2.1 a 10,10 0 0 0 8,8 l 0,-4 4,0 0,4 a 10,10 0 0 0 8,-8 l -4,0 0,-4 4,0 a 10,10 0 0 0 -8,-8 l 0,4 -4,0 0,-4 a 10,10 0 0 0 -8,8 l 4,0 0,4 z" />
     <path id="geo-linux-detailed-icon" d="m 2,15.9 a 14,14 0 1 1 0,0.2 z m 3,2.1 a 11,11 0 0 0 9,9 l 1,-5 2,0 1,5 a 11,11 0 0 0 9,-9 l -5,-1 0,-2 5,-1 a 11,11 0 0 0 -9,-9 l -1,5 -2,0 -1,-5 a 11,11 0 0 0 -9,9 l 5,1 0,2 z" />
     <path id="geo-osx-icon" d="m 0,16 16,0 0,16 12,-28 z" />
@@ -58,16 +64,17 @@
     <path id="indexedDB-icon" d="m 2,24 a 4,4 0 0 0 4,4 l 2,0 0,-4 -2,0 0,-16 20,0 0,16 -2,0 0,4 2,0 a 4,4 0 0 0 4,-4 l 0,-16 a 4,4 0 0 0 -4,-4 l -20,0 a 4,4 0 0 0 -4,4 z m 8,-2 6,7 6,-7 -4,0 0,-8 -4,0 0,8 z" />
     <path id="login-icon" d="m 2,26 0,4 6,0 0,-2 2,0 0,-2 1,0 0,-1 2,0 0,-3 2,0 2.5,-2.5 1.5,1.5 3,-3 a 8,8 0 1 0 -8,-8 l -3,3 2,2 z m 20,-18.1 a 2,2 0 1 1 0,0.2 z" />
     <path id="login-detailed-icon" d="m 1,27 0,3.5 a 0.5,0.5 0 0 0 0.5,0.5 l 5,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1.5 1.5,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1.5 1,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-1 1,0 a 0.5,0.5 0 0 0 0.5,-0.5 l 0,-2 2,0 2.5,-2.5 q 0.5,-0.5 1,0 l 1,1 c 0.5,0.5 1,0.5 1.5,-0.5 l 1,-2 a 9,9 0 1 0 -8,-8 l -2,1 c -1,0.5 -1,1 -0.5,1.5 l 1.5,1.5 q 0.5,0.5 0,1 z m 21,-19.1 a 2,2 0 1 1 0,0.2 z" />
     <path id="microphone-icon" d="m 8,14 0,4 a 8,8 0 0 0 6,7.7 l 0,2.3 -2,0 a 2,2 0 0 0 -2,2 l 12,0 a 2,2 0 0 0 -2,-2 l -2,0 0,-2.3 a 8,8 0 0 0 6,-7.7 l 0,-4 -2,0 0,4 a 6,6 0 0 1 -12,0 l 0,-4 z m 4,4 a 4,4 0 0 0 8,0 l 0,-12 a 4,4 0 0 0 -8,0 z" />
     <path id="microphone-detailed-icon" d="m 8,18 a 8,8 0 0 0 6,7.7 l 0,2.3 -1,0 a 3,2 0 0 0 -3,2 l 12,0 a 3,2 0 0 0 -3,-2 l -1,0 0,-2.3 a 8,8 0 0 0 6,-7.7 l 0,-4 a 1,1 0 0 0 -2,0 l 0,4 a 6,6 0 0 1 -12,0 l 0,-4 a 1,1 0 0 0 -2,0 z m 4,0 a 4,4 0 0 0 8,0 l 0,-12 a 4,4 0 0 0 -8,0 z" />
     <path id="plugin-icon" d="m 2,26 a 2,2 0 0 0 2,2 l 24,0 a 2,2 0 0 0 2,-2 l 0,-16 a 2,2 0 0 0 -2,-2 l -24,0 a 2,2 0 0 0 -2,2 z m 2,-20 10,0 0,-2 a 2,2 0 0 0 -2,-2 l -6,0 a 2,2 0 0 0 -2,2 z m 14,0 10,0 0,-2 a 2,2 0 0 0 -2,-2 l -6,0 a 2,2 0 0 0 -2,2 z" />
     <path id="popup-icon" d="m 2,24 a 4,4 0 0 0 4,4 l 8,0 a 10,10 0 0 1 -2,-4 l -4,0 a 2,2 0 0 1 -2,-2 l 0,-12 18,0 0,2 a 10,10 0 0 1 4,2 l 0,-8 a 4,4 0 0 0 -4,-4 l -18,0 a 4,4 0 0 0 -4,4 z m 12,-2.1 a 8,8 0 1 1 0,0.2 m 10.7,-4.3 a 5,5 0 0 0 -6.9,6.9 z m -5.4,8.4 a 5,5 0 0 0 6.9,-6.9 z" />
     <path id="screen-icon" d="m 2,18 a 2,2 0 0 0 2,2 l 2,0 0,-6 a 4,4 0 0 1 4,-4 l 14,0 0,-6 a 2,2 0 0 0 -2,-2 l -18,0 a 2,2 0 0 0 -2,2 z m 6,10 a 2,2 0 0 0 2,2 l 18,0 a 2,2 0 0 0 2,-2 l 0,-14 a 2,2 0 0 0 -2,-2 l -18,0 a 2,2 0 0 0 -2,2 z" />
+    <path id="update-icon" d="M 16,9 L 16,24 M 16,9 L 11,14 M 16,9 L 21,14" />
 
     <clipPath id="clip">
       <path d="m 0,0 0,31 31,-31 z m 6,32 26,0 0,-26 z"/>
     </clipPath>
   </defs>
 
   <use id="camera" xlink:href="#camera-icon" />
   <use id="camera-sharing" xlink:href="#camera-icon"/>
@@ -95,11 +102,12 @@
   <use id="microphone-detailed" xlink:href="#microphone-detailed-icon" />
   <use id="plugin" xlink:href="#plugin-icon" />
   <use id="plugin-blocked" class="blocked" xlink:href="#plugin-icon" />
   <use id="popup" xlink:href="#popup-icon" />
   <use id="screen" xlink:href="#screen-icon" />
   <use id="screen-sharing" xlink:href="#screen-icon"/>
   <use id="screen-indicator" xlink:href="#screen-icon"/>
   <use id="screen-blocked" class="blocked" xlink:href="#screen-icon" />
+  <use id="update" xlink:href="#update-icon" />
 
   <path id="strikeout" d="m 2,28 2,2 26,-26 -2,-2 z"/>
 </svg>
--- a/browser/themes/shared/update-badge.svg
+++ b/browser/themes/shared/update-badge.svg
@@ -1,6 +1,8 @@
 <!-- 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/. -->
 <svg xmlns="http://www.w3.org/2000/svg" width="10px" height="10px">
-  <polygon points="4,9 4,5 2,5 5,1 8,5 6,5 6,9" fill="#fff"/>
+  <line x1="5" x2="5" y1="9" y2="2" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
+  <line x1="5" x2="2" y1="2" y2="5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
+  <line x1="5" x2="8" y1="2" y2="5" stroke="#fff" stroke-width="1.5" stroke-linecap="round"/>
 </svg>
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -33,16 +33,17 @@ const PREF_APP_UPDATE_ENABLED           
 const PREF_APP_UPDATE_IDLETIME             = "app.update.idletime";
 const PREF_APP_UPDATE_LOG                  = "app.update.log";
 const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED  = "app.update.notifiedUnsupported";
 const PREF_APP_UPDATE_POSTUPDATE           = "app.update.postupdate";
 const PREF_APP_UPDATE_PROMPTWAITTIME       = "app.update.promptWaitTime";
 const PREF_APP_UPDATE_SERVICE_ENABLED      = "app.update.service.enabled";
 const PREF_APP_UPDATE_SERVICE_ERRORS       = "app.update.service.errors";
 const PREF_APP_UPDATE_SERVICE_MAXERRORS    = "app.update.service.maxErrors";
+const PREF_APP_UPDATE_DOORHANGER           = "app.update.doorhanger";
 const PREF_APP_UPDATE_SILENT               = "app.update.silent";
 const PREF_APP_UPDATE_SOCKET_MAXERRORS     = "app.update.socket.maxErrors";
 const PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT  = "app.update.socket.retryTimeout";
 const PREF_APP_UPDATE_STAGING_ENABLED      = "app.update.staging.enabled";
 const PREF_APP_UPDATE_URL                  = "app.update.url";
 const PREF_APP_UPDATE_URL_DETAILS          = "app.update.url.details";
 
 const PREFBRANCH_APP_UPDATE_NEVER = "app.update.never.";
@@ -1185,16 +1186,19 @@ function handleUpdateFailure(update, err
     Cc["@mozilla.org/updates/update-prompt;1"].
       createInstance(Ci.nsIUpdatePrompt).
       showUpdateError(update);
     writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING);
     return true;
   }
 
   if (update.errorCode == ELEVATION_CANCELED) {
+
+    Services.obs.notifyObservers(update, "update-error", "elevation-canceled");
+
     let cancelations = getPref("getIntPref", PREF_APP_UPDATE_CANCELATIONS, 0);
     cancelations++;
     Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS, cancelations);
     if (AppConstants.platform == "macosx") {
       let osxCancelations = getPref("getIntPref",
                                   PREF_APP_UPDATE_CANCELATIONS_OSX, 0);
       osxCancelations++;
       Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS_OSX,
@@ -1208,16 +1212,17 @@ function handleUpdateFailure(update, err
         cleanupActiveUpdate();
       } else {
         writeStatusFile(getUpdatesDir(),
                         update.state = STATE_PENDING_ELEVATE);
       }
       update.statusText = gUpdateBundle.GetStringFromName("elevationFailure");
       update.QueryInterface(Ci.nsIWritablePropertyBag);
       update.setProperty("patchingFailed", "elevationFailure");
+
       let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
                  createInstance(Ci.nsIUpdatePrompt);
       prompter.showUpdateError(update);
     } else {
       writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING);
     }
     return true;
   }
@@ -1279,16 +1284,18 @@ function handleFallbackToCompleteUpdate(
     LOG("handleFallbackToCompleteUpdate - install of partial patch " +
         "failed, downloading complete patch");
     var status = Cc["@mozilla.org/updates/update-service;1"].
                  getService(Ci.nsIApplicationUpdateService).
                  downloadUpdate(update, !postStaging);
     if (status == STATE_NONE)
       cleanupActiveUpdate();
   } else {
+    Services.obs.notifyObservers(update, "update-error", "unknown");
+
     LOG("handleFallbackToCompleteUpdate - install of complete or " +
         "only one patch offered failed.");
   }
   update.QueryInterface(Ci.nsIWritablePropertyBag);
   update.setProperty("patchingFailed", oldType);
 }
 
 function pingStateAndStatusCodes(aUpdate, aStartup, aStatus) {
@@ -2159,19 +2166,22 @@ UpdateService.prototype = {
     errCount++;
     Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, errCount);
     // Don't allow the preference to set a value greater than 20 for max errors.
     let maxErrors = Math.min(getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, 10), 20);
 
     if (errCount >= maxErrors) {
       let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
                      createInstance(Ci.nsIUpdatePrompt);
+      Services.obs.notifyObservers(update, "update-error", "check-attempts-exceeded");
       prompter.showUpdateError(update);
       AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_PROMPT);
     } else {
+      Services.obs.notifyObservers(update, "update-error", "check-attempt-failed");
+
       AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_SILENT);
     }
   },
 
   /**
    * Called when a connection should be resumed
    */
   _attemptResume: function AUS_attemptResume() {
@@ -2519,23 +2529,29 @@ UpdateService.prototype = {
     if (update.unsupported) {
       LOG("UpdateService:_selectAndInstallUpdate - update not supported for " +
           "this system");
       if (!getPref("getBoolPref", PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, false)) {
         LOG("UpdateService:_selectAndInstallUpdate - notifying that the " +
             "update is not supported for this system");
         this._showPrompt(update);
       }
+
+      Services.obs.notifyObservers(null, "update-available", "unsupported");
+
       AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNSUPPORTED);
       return;
     }
 
     if (!getCanApplyUpdates()) {
       LOG("UpdateService:_selectAndInstallUpdate - the user is unable to " +
           "apply updates... prompting");
+
+      Services.obs.notifyObservers(null, "update-available", "cant-apply");
+
       this._showPrompt(update);
       AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_APPLY);
       return;
     }
 
     /**
      * From this point on there are two possible outcomes:
      * 1. download and install the update automatically
@@ -2551,24 +2567,30 @@ UpdateService.prototype = {
      * Update Type   Outcome
      * Major         Notify
      * Minor         Auto Install
      */
     if (update.showPrompt) {
       LOG("UpdateService:_selectAndInstallUpdate - prompting because the " +
           "update snippet specified showPrompt");
       AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_SNIPPET);
+
+      Services.obs.notifyObservers(update, "update-available", "show-prompt");
+
       this._showPrompt(update);
       return;
     }
 
     if (!getPref("getBoolPref", PREF_APP_UPDATE_AUTO, true)) {
       LOG("UpdateService:_selectAndInstallUpdate - prompting because silent " +
           "install is disabled");
       AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_PREF);
+
+      Services.obs.notifyObservers(update, "update-available", "show-prompt");
+
       this._showPrompt(update);
       return;
     }
 
     LOG("UpdateService:_selectAndInstallUpdate - download the update");
     let status = this.downloadUpdate(update, true);
     if (status == STATE_NONE) {
       cleanupActiveUpdate();
@@ -3104,17 +3126,17 @@ UpdateManager.prototype = {
     if (update.state == STATE_APPLIED && shouldUseService()) {
       writeStatusFile(getUpdatesDir(), update.state = STATE_APPLIED_SERVICE);
     }
 
     // Send an observer notification which the update wizard uses in
     // order to update its UI.
     LOG("UpdateManager:refreshUpdateStatus - Notifying observers that " +
         "the update was staged. state: " + update.state + ", status: " + status);
-    Services.obs.notifyObservers(null, "update-staged", update.state);
+    Services.obs.notifyObservers(update, "update-staged", update.state);
 
     if (AppConstants.platform == "gonk") {
       // Do this after everything else, since it will likely cause the app to
       // shut down.
       if (update.state == STATE_APPLIED) {
         // Notify the user that an update has been staged and is ready for
         // installation (i.e. that they should restart the application). We do
         // not notify on failed update attempts.
@@ -3233,18 +3255,16 @@ Checker.prototype = {
   /**
    * See nsIUpdateService.idl
    */
   checkForUpdates: function UC_checkForUpdates(listener, force) {
     LOG("Checker: checkForUpdates, force: " + force);
     if (!listener)
       throw Cr.NS_ERROR_NULL_POINTER;
 
-    Services.obs.notifyObservers(null, "update-check-start", null);
-
     var url = this.getUpdateURL(force);
     if (!url || (!this.enabled && !force))
       return;
 
     this._request = new XMLHttpRequest();
     this._request.open("GET", url, true);
     this._request.channel.notificationCallbacks = new gCertUtils.BadCertHandler(false);
     // Prevent the request from reading from the cache.
@@ -3339,16 +3359,17 @@ Checker.prototype = {
    *          The nsIDOMEvent for the load
    */
   onLoad: function UC_onLoad(event) {
     LOG("Checker:onLoad - request completed downloading document");
 
     try {
       // Analyze the resulting DOM and determine the set of updates.
       var updates = this._updates;
+
       LOG("Checker:onLoad - number of updates available: " + updates.length);
 
       if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
         Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
       }
 
       // Tell the callback about the updates
       this._callback.onCheckComplete(event.target, updates, updates.length);
@@ -4135,16 +4156,19 @@ Downloader.prototype = {
           cleanupActiveUpdate();
         } else {
           allFailed = false;
         }
       }
 
       if (allFailed) {
         LOG("Downloader:onStopRequest - all update patch downloads failed");
+
+        Services.obs.notifyObservers(this._update, "update-error", "all-downloads-failed");
+
         // If the update UI is not open (e.g. the user closed the window while
         // downloading) and if at any point this was a foreground download
         // notify the user about the error. If the update was a background
         // update there is no notification since the user won't be expecting it.
         if (!Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME)) {
           this._update.QueryInterface(Ci.nsIWritablePropertyBag);
           if (this._update.getProperty("foregroundDownload") == "true") {
             let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
@@ -4260,33 +4284,39 @@ UpdatePrompt.prototype = {
                  null, null);
   },
 
   /**
    * See nsIUpdateService.idl
    */
   showUpdateAvailable: function UP_showUpdateAvailable(update) {
     if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) ||
+        getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false) ||
         this._getUpdateWindow() || this._getAltUpdateWindow()) {
       return;
     }
 
     this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null,
                            UPDATE_WINDOW_NAME, "updatesavailable", update);
   },
 
   /**
    * See nsIUpdateService.idl
    */
   showUpdateDownloaded: function UP_showUpdateDownloaded(update, background) {
     if (background && getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false)) {
       return;
     }
-    // Trigger the display of the hamburger menu badge.
-    Services.obs.notifyObservers(null, "update-downloaded", update.state);
+
+    // Trigger the display of the hamburger doorhanger.
+    Services.obs.notifyObservers(update, "update-downloaded", update.state);
+
+    if (getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false)) {
+      return;
+    }
 
     if (this._getAltUpdateWindow())
       return;
 
     if (background) {
       this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null,
                               UPDATE_WINDOW_NAME, "finishedBackground", update);
     } else {
@@ -4295,16 +4325,17 @@ UpdatePrompt.prototype = {
     }
   },
 
   /**
    * See nsIUpdateService.idl
    */
   showUpdateError: function UP_showUpdateError(update) {
     if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) ||
+        getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false) ||
         this._getAltUpdateWindow())
       return;
 
     // In some cases, we want to just show a simple alert dialog.
     // Replace with Array.prototype.includes when it has stabilized.
     if (update.state == STATE_FAILED &&
         (WRITE_ERRORS.indexOf(update.errorCode) != -1 ||
          update.errorCode == FILESYSTEM_MOUNT_READWRITE_ERROR ||
--- a/toolkit/mozapps/update/tests/chrome/utils.js
+++ b/toolkit/mozapps/update/tests/chrome/utils.js
@@ -809,16 +809,17 @@ function setupPrefs() {
   if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_STAGING_ENABLED)) {
     gAppUpdateStagingEnabled = Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED);
   }
   Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false);
 
   Services.prefs.setIntPref(PREF_APP_UPDATE_IDLETIME, 0);
   Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 0);
   Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, false);
+  Services.prefs.setBoolPref(PREF_APP_UPDATE_DOORHANGER, false);
 }
 
 /**
  * Restores files that were backed up for the tests and general file cleanup.
  */
 function resetFiles() {
   // Restore the backed up updater-settings.ini if it exists.
   let baseAppDir = getGREDir();
@@ -902,16 +903,20 @@ function resetPrefs() {
   if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
     Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
   }
 
   if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDMAXERRORS)) {
     Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDMAXERRORS);
   }
 
+  if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_DOORHANGER)) {
+    Services.prefs.clearUserPref(PREF_APP_UPDATE_DOORHANGER);
+  }
+
   try {
     Services.prefs.deleteBranch(PREFBRANCH_APP_UPDATE_NEVER);
   } catch (e) {
   }
 }
 
 function setupTimer(aTestTimeout) {
   gTestTimeout = aTestTimeout;
--- a/toolkit/mozapps/update/tests/data/shared.js
+++ b/toolkit/mozapps/update/tests/data/shared.js
@@ -19,16 +19,18 @@ const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTE
 const PREF_APP_UPDATE_PROMPTWAITTIME       = "app.update.promptWaitTime";
 const PREF_APP_UPDATE_RETRYTIMEOUT         = "app.update.socket.retryTimeout";
 const PREF_APP_UPDATE_SERVICE_ENABLED      = "app.update.service.enabled";
 const PREF_APP_UPDATE_SILENT               = "app.update.silent";
 const PREF_APP_UPDATE_SOCKET_MAXERRORS     = "app.update.socket.maxErrors";
 const PREF_APP_UPDATE_STAGING_ENABLED      = "app.update.staging.enabled";
 const PREF_APP_UPDATE_URL                  = "app.update.url";
 const PREF_APP_UPDATE_URL_DETAILS          = "app.update.url.details";
+const PREF_APP_UPDATE_DOORHANGER           = "app.update.doorhanger";
+
 
 const PREFBRANCH_APP_UPDATE_NEVER = "app.update.never.";
 
 const PREFBRANCH_APP_PARTNER         = "app.partner.";
 const PREF_DISTRIBUTION_ID           = "distribution.id";
 const PREF_DISTRIBUTION_VERSION      = "distribution.version";
 const PREF_TOOLKIT_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";