Bug 893505 - Simplify the application update UI draft
authorDoug Thayer <dougathayer@gmail.com>
Tue, 21 Mar 2017 13:50:09 -0700
changeset 558689 98d687b1df5bb7cab3d812f73e4ea52a1c4737d7
parent 558673 35c7be9c2db288d1d449e3cc586c4164d642c5fd
child 623253 d785b2501aa8b8bf9efb21e33860be024309d85e
push id52937
push userbmo:dothayer@mozilla.com
push dateFri, 07 Apr 2017 22:57:42 +0000
bugs893505
milestone55.0a1
Bug 893505 - Simplify the application update UI There's quite a few changes in here. At a high level, all we're trying to do is to replace the old update popup with a less intrusive and more modern doorhanger (set of doorhangers) for various update failure conditions. MozReview-Commit-ID: 24sESMTosNX
browser/app/profile/firefox.js
browser/base/content/browser-addons.js
browser/base/content/browser-fullScreenAndPointerLock.js
browser/base/content/browser-fxaccounts.js
browser/base/content/browser.css
browser/base/content/browser.js
browser/base/content/test/appUpdate/.eslintrc.js
browser/base/content/test/appUpdate/browser.ini
browser/base/content/test/appUpdate/browser_updatesBasicPrompt.js
browser/base/content/test/appUpdate/browser_updatesBasicPromptNoStaging.js
browser/base/content/test/appUpdate/browser_updatesCompleteAndPartialPatchesWithBadCompleteSize.js
browser/base/content/test/appUpdate/browser_updatesCompleteAndPartialPatchesWithBadPartialSize.js
browser/base/content/test/appUpdate/browser_updatesCompleteAndPartialPatchesWithBadSizes.js
browser/base/content/test/appUpdate/browser_updatesCompletePatchApplyFailure.js
browser/base/content/test/appUpdate/browser_updatesCompletePatchWithBadCompleteSize.js
browser/base/content/test/appUpdate/browser_updatesDownloadFailures.js
browser/base/content/test/appUpdate/browser_updatesMalformedXml.js
browser/base/content/test/appUpdate/browser_updatesPartialPatchApplyFailure.js
browser/base/content/test/appUpdate/browser_updatesPartialPatchApplyFailureWithCompleteAvailable.js
browser/base/content/test/appUpdate/browser_updatesPartialPatchApplyFailureWithCompleteValidationFailure.js
browser/base/content/test/appUpdate/browser_updatesPartialPatchWithBadPartialSize.js
browser/base/content/test/appUpdate/downloadPage.html
browser/base/content/test/appUpdate/head.js
browser/base/content/test/appUpdate/testConstants.js
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_menuButtonBadgeManager.js
browser/base/moz.build
browser/branding/official/pref/firefox-branding.js
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
testing/talos/talos/config.py
toolkit/components/telemetry/Histograms.json
toolkit/mozapps/update/nsUpdateService.js
toolkit/mozapps/update/tests/chrome/chrome.ini
toolkit/mozapps/update/tests/chrome/testConstants.js
toolkit/mozapps/update/tests/chrome/update.sjs
toolkit/mozapps/update/tests/chrome/utils.js
toolkit/mozapps/update/tests/data/shared.js
toolkit/mozapps/update/tests/data/sharedUpdateXML.js
toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js
toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -110,30 +110,35 @@ 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 doorhanger application update UI.
+pref("app.update.doorhanger", true);
+
+// How many times we should let downloads fail before prompting the user to
+// download a fresh installer.
+pref("app.update.download.promptMaxAttempts", 2);
+
+// How many times we should let an elevation prompt fail before prompting the user to
+// download a fresh installer.
+pref("app.update.elevation.promptMaxAttempts", 2);
+
 // 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);
 
-// If set to true, the hamburger button will show badges for update events.
-#ifndef RELEASE_OR_BETA
-pref("app.update.badge", true);
-#else
-pref("app.update.badge", false);
-#endif
 // app.update.badgeWaitTime is in branding section
 
 // If set to true, the Update Service will apply updates in the background
 // when it finishes downloading them.
 pref("app.update.staging.enabled", true);
 
 // Update service URL:
 pref("app.update.url", "https://aus5.mozilla.org/update/6/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%SYSTEM_CAPABILITIES%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml");
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -499,20 +499,19 @@ const gExtensionsNotifications = {
     }
     ExtensionsUI.off("change", this.boundUpdate);
   },
 
   updateAlerts() {
     let sideloaded = ExtensionsUI.sideloaded;
     let updates = ExtensionsUI.updates;
     if (sideloaded.size + updates.size == 0) {
-      gMenuButtonBadgeManager.removeBadge(gMenuButtonBadgeManager.BADGEID_ADDONS);
+      PanelUI.removeNotification("addon-alert");
     } else {
-      gMenuButtonBadgeManager.addBadge(gMenuButtonBadgeManager.BADGEID_ADDONS,
-                                       "addon-alert");
+      PanelUI.showBadgeOnlyNotification("addon-alert");
     }
 
     let container = document.getElementById("PanelUI-footer-addons");
 
     while (container.firstChild) {
       container.firstChild.remove();
     }
 
--- a/browser/base/content/browser-fullScreenAndPointerLock.js
+++ b/browser/base/content/browser-fullScreenAndPointerLock.js
@@ -532,26 +532,31 @@ var FullScreen = {
   },
 
   _setPopupOpen(aEvent) {
     // Popups should only veto chrome collapsing if they were opened when the chrome was not collapsed.
     // Otherwise, they would not affect chrome and the user would expect the chrome to go away.
     // e.g. we wouldn't want the autoscroll icon firing this event, so when the user
     // toggles chrome when moving mouse to the top, it doesn't go away again.
     if (aEvent.type == "popupshown" && !FullScreen._isChromeCollapsed &&
-        aEvent.target.localName != "tooltip" && aEvent.target.localName != "window")
+        aEvent.target.localName != "tooltip" && aEvent.target.localName != "window" &&
+        aEvent.target.getAttribute("nopreventnavboxhide") != "true")
       FullScreen._isPopupOpen = true;
     else if (aEvent.type == "popuphidden" && aEvent.target.localName != "tooltip" &&
              aEvent.target.localName != "window") {
       FullScreen._isPopupOpen = false;
       // Try again to hide toolbar when we close the popup.
       FullScreen.hideNavToolbox(true);
     }
   },
 
+  get navToolboxHidden() {
+    return this._isChromeCollapsed;
+  },
+
   // Autohide helpers for the context menu item
   getAutohide(aItem) {
     aItem.setAttribute("checked", gPrefService.getBoolPref("browser.fullscreen.autohide"));
   },
   setAutohide() {
     gPrefService.setBoolPref("browser.fullscreen.autohide", !gPrefService.getBoolPref("browser.fullscreen.autohide"));
     // Try again to hide toolbar when we change the pref.
     FullScreen.hideNavToolbox(true);
@@ -574,16 +579,17 @@ var FullScreen = {
         bottom: rect.bottom,
         left: rect.left,
         right: rect.right
       };
       MousePosTracker.addListener(this);
     }
 
     this._isChromeCollapsed = false;
+    Services.obs.notifyObservers(null, "fullscreen-nav-toolbox", "shown");
   },
 
   hideNavToolbox(aAnimate = false) {
     if (this._isChromeCollapsed || !this._safeToCollapse())
       return;
 
     this._fullScrToggler.hidden = false;
 
@@ -597,16 +603,18 @@ var FullScreen = {
       };
       gNavToolbox.addEventListener("transitionend", listener, true);
       this._fullScrToggler.hidden = true;
     }
 
     gNavToolbox.style.marginTop =
       -gNavToolbox.getBoundingClientRect().height + "px";
     this._isChromeCollapsed = true;
+    Services.obs.notifyObservers(null, "fullscreen-nav-toolbox", "hidden");
+
     MousePosTracker.removeListener(this);
   },
 
   _updateToolbars(aEnterFS) {
     for (let el of document.querySelectorAll("toolbar[fullscreentoolbar=true]")) {
       if (aEnterFS) {
         // Give the main nav bar and the tab bar the fullscreen context menu,
         // otherwise remove context menu to prevent breakage
--- a/browser/base/content/browser-fxaccounts.js
+++ b/browser/base/content/browser-fxaccounts.js
@@ -212,19 +212,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
@@ -1178,17 +1178,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
@@ -1510,18 +1510,16 @@ var gBrowserInit = {
     gSyncUI.init();
     gFxAccounts.init();
 
     if (AppConstants.MOZ_DATA_REPORTING)
       gDataNotificationInfoBar.init();
 
     gBrowserThumbnails.init();
 
-    gMenuButtonBadgeManager.init();
-
     gMenuButtonUpdateBadge.init();
 
     gExtensionsNotifications.init();
 
     let wasMinimized = window.windowState == window.STATE_MINIMIZED;
     window.addEventListener("sizemodechange", () => {
       let isMinimized = window.windowState == window.STATE_MINIMIZED;
       if (wasMinimized != isMinimized) {
@@ -1688,18 +1686,16 @@ var gBrowserInit = {
     TrackingProtection.uninit();
 
     RefreshBlocker.uninit();
 
     CaptivePortalWatcher.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)
@@ -2744,195 +2740,227 @@ function UpdatePopupNotificationsVisibil
   PopupNotifications.anchorVisibilityChange();
 }
 
 function PageProxyClickHandler(aEvent) {
   if (aEvent.button == 1 && gPrefService.getBoolPref("middlemouse.paste"))
     middleMousePaste(aEvent);
 }
 
-var gMenuButtonBadgeManager = {
-  BADGEID_APPUPDATE: "update",
-  BADGEID_DOWNLOAD: "download",
-  BADGEID_FXA: "fxa",
-  BADGEID_ADDONS: "addons",
-
-  fxaBadge: null,
-  downloadBadge: null,
-  appUpdateBadge: null,
-  addonsBadge: null,
+// Setup the hamburger button badges for updates, if enabled.
+var gMenuButtonUpdateBadge = {
+  kTopics: [
+    "update-staged",
+    "update-downloaded",
+    "update-available",
+    "update-error",
+  ],
+
+  timeouts: [],
+
+  get enabled() {
+    return Services.prefs.getBoolPref("app.update.doorhanger", false);
+  },
+
+  get badgeWaitTime() {
+    return Services.prefs.getIntPref("app.update.badgeWaitTime", 4 * 24 * 3600); // 4 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 || this.addonsBadge;
-
-    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 if (badgeId == this.BADGEID_ADDONS) {
-      this.addonsBadge = 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() {
-    this.enabled = Services.prefs.getBoolPref("app.update.badge", false);
     if (this.enabled) {
-      this.badgeWaitTime = Services.prefs.getIntPref("app.update.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);
+      });
+    }
+
+    this.reset();
+  },
+
+  reset() {
+    PanelUI.removeNotification(/^update-/);
+    this.clearCallbacks();
+  },
+
+  clearCallbacks() {
+    this.timeouts.forEach(t => clearTimeout(t));
+    this.timeouts = [];
+  },
+
+  addTimeout(time, callback) {
+    this.timeouts.push(setTimeout(() => {
+      this.clearCallbacks();
+      callback();
+    }, time));
+  },
+
+  replaceReleaseNotes(update, whatsNewId) {
+    let whatsNewLink = document.getElementById(whatsNewId);
+    if (update && update.detailsURL) {
+      whatsNewLink.href = update.detailsURL;
+    } else {
+      whatsNewLink.href = Services.urlFormatter.formatURLPref("app.update.url.details");
+    }
+  },
+
+  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");
+  },
+
+  showUpdateNotification(type, dismissed, mainAction) {
+    let action = {
+      callback(fromDoorhanger) {
+        if (fromDoorhanger) {
+          Services.telemetry.getHistogramById("UPDATE_NOTIFICATION_MAIN_ACTION_DOORHANGER").add(type);
+        } else {
+          Services.telemetry.getHistogramById("UPDATE_NOTIFICATION_MAIN_ACTION_MENU").add(type);
+        }
+        mainAction();
       }
-    } else {
-      // open the page for manual update
-      let url = Services.urlFormatter.formatURLPref("app.update.url.manual");
-      openUILinkIn(url, "tab");
+    };
+
+    let secondaryAction = {
+      callback() {
+        Services.telemetry.getHistogramById("UPDATE_NOTIFICATION_DISMISSED").add(type);
+      },
+      dismiss: true
+    };
+
+    PanelUI.showNotification("update-" + type, action, [secondaryAction], { dismissed });
+    Services.telemetry.getHistogramById("UPDATE_NOTIFICATION_SHOWN").add(type);
+  },
+
+  showRestartNotification(dismissed) {
+    this.showUpdateNotification("restart", dismissed, () => gMenuButtonUpdateBadge.requestRestart());
+  },
+
+  showUpdateAvailableNotification(update, dismissed) {
+    this.replaceReleaseNotes(update, "update-available-whats-new");
+    this.showUpdateNotification("available", dismissed, () => {
+      let updateService = Cc["@mozilla.org/updates/update-service;1"]
+                          .getService(Ci.nsIApplicationUpdateService);
+      updateService.downloadUpdate(update, true);
+    });
+  },
+
+  showManualUpdateNotification(update, dismissed) {
+    this.replaceReleaseNotes(update, "update-manual-whats-new");
+
+    this.showUpdateNotification("manual", dismissed, () => gMenuButtonUpdateBadge.openManualUpdateUrl());
+  },
+
+  handleUpdateError(update, status) {
+    switch (status) {
+      case "download-attempt-failed":
+        this.clearCallbacks();
+        this.showUpdateAvailableNotification(update, false);
+        break;
+      case "download-attempts-exceeded":
+        this.clearCallbacks();
+        this.showManualUpdateNotification(update, false);
+        break;
+      case "elevation-attempt-failed":
+        this.clearCallbacks();
+        this.showRestartNotification(update, false);
+        break;
+      case "elevation-attempts-exceeded":
+        this.clearCallbacks();
+        this.showManualUpdateNotification(update, false);
+        break;
+      case "check-attempts-exceeded":
+      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;
+    }
+  },
+
+  handleUpdateStagedOrDownloaded(update, status) {
+    switch (status) {
+      case "applied":
+      case "pending":
+      case "applied-service":
+      case "pending-service":
+      case "success":
+        this.clearCallbacks();
+
+        let badgeWaitTimeMs = this.badgeWaitTime * 1000;
+        let doorhangerWaitTimeMs = update.promptWaitTime * 1000;
+
+        if (badgeWaitTimeMs < doorhangerWaitTimeMs) {
+          this.addTimeout(badgeWaitTimeMs, () => {
+            this.showRestartNotification(true);
+
+            // doorhangerWaitTimeMs is relative to when we initially received
+            // the event. Since we've already waited badgeWaitTimeMs, subtract
+            // that from doorhangerWaitTimeMs.
+            let remainingTime = doorhangerWaitTimeMs - badgeWaitTimeMs;
+            this.addTimeout(remainingTime, () => {
+              this.showRestartNotification(false);
+            });
+          });
+        } else {
+          this.addTimeout(doorhangerWaitTimeMs, () => {
+            this.showRestartNotification(false);
+          });
+        }
+        break;
+    }
+  },
+
+  handleUpdateAvailable(update, status) {
+    switch (status) {
+      case "show-prompt":
+        // If an update is available and had the showPrompt flag set, then
+        // show an update available doorhanger.
+        this.clearCallbacks();
+        this.showUpdateAvailableNotification(update, false);
+        break;
     }
   },
 
   observe(subject, topic, status) {
-    if (topic == "update-canceled") {
-      this.reset();
-      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);
+    if (!this.enabled) {
       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;
-  },
-
-  reset() {
-    gMenuButtonBadgeManager.removeBadge(
-      gMenuButtonBadgeManager.BADGEID_APPUPDATE);
-    let updateButton = document.getElementById("PanelUI-update-status");
-    updateButton.hidden = true;
-    this.uninit();
-    this.init();
+    let update = subject && subject.QueryInterface(Ci.nsIUpdate);
+
+    switch (topic) {
+      case "update-available":
+        this.handleUpdateAvailable(update, status);
+        break;
+      case "update-staged":
+      case "update-downloaded":
+        this.handleUpdateStagedOrDownloaded(update, status);
+        break;
+      case "update-error":
+        this.handleUpdateError(update, status);
+        break;
+    }
   }
 };
 
 // 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;
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+  "extends": [
+    "plugin:mozilla/browser-test"
+  ]
+};
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+tags = appupdate
+support-files =
+  head.js
+  downloadPage.html
+  testConstants.js
+
+[browser_updatesBasicPrompt.js]
+skip-if = asan
+reason = Bug 1168003
+[browser_updatesBasicPromptNoStaging.js]
+[browser_updatesCompleteAndPartialPatchesWithBadCompleteSize.js]
+[browser_updatesCompleteAndPartialPatchesWithBadPartialSize.js]
+[browser_updatesCompleteAndPartialPatchesWithBadSizes.js]
+[browser_updatesCompletePatchApplyFailure.js]
+[browser_updatesCompletePatchWithBadCompleteSize.js]
+[browser_updatesDownloadFailures.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/appUpdate/browser_updatesBasicPrompt.js
@@ -0,0 +1,28 @@
+add_task(function* testBasicPrompt() {
+  SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_STAGING_ENABLED, true]]});
+  let updateParams = "showPrompt=1&promptWaitTime=0";
+  gUseTestUpdater = true;
+
+  // Open a new window to make sure that it doesn't get in the way
+  // of the notification management.
+  let extraWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      notificationId: "update-available",
+      button: "button",
+      beforeClick() {
+        checkWhatsNewLink("update-available-whats-new");
+      }
+    },
+    {
+      notificationId: "update-restart",
+      button: "secondarybutton",
+      *cleanup() {
+        PanelUI.removeNotification(/.*/);
+      }
+    },
+  ]);
+
+  yield BrowserTestUtils.closeWindow(extraWindow);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesBasicPromptNoStaging.js
@@ -0,0 +1,22 @@
+add_task(function* testBasicPromptNoStaging() {
+  SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_STAGING_ENABLED, false]]});
+
+  let updateParams = "showPrompt=1&promptWaitTime=0";
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      notificationId: "update-available",
+      button: "button",
+      beforeClick() {
+        checkWhatsNewLink("update-available-whats-new");
+      }
+    },
+    {
+      notificationId: "update-restart",
+      button: "secondarybutton",
+      cleanup() {
+        PanelUI.removeNotification(/.*/);
+      }
+    },
+  ]);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/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/appUpdate/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/appUpdate/browser_updatesCompleteAndPartialPatchesWithBadSizes.js
@@ -0,0 +1,33 @@
+add_task(function* testCompleteAndPartialPatchesWithBadSizes() {
+  SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_DOWNLOADPROMPTMAXATTEMPTS, 2]]});
+  let updateParams = "invalidPartialSize=1&invalidCompleteSize=1";
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      // if we fail maxBackgroundErrors download attempts, then we want to
+      // first show the user an update available prompt.
+      notificationId: "update-available",
+      button: "button"
+    },
+    {
+      notificationId: "update-available",
+      button: "button"
+    },
+    {
+      // 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() {
+        checkWhatsNewLink("update-manual-whats-new");
+      },
+      *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/appUpdate/browser_updatesCompletePatchApplyFailure.js
@@ -0,0 +1,25 @@
+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() {
+        checkWhatsNewLink("update-manual-whats-new");
+      },
+      *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/appUpdate/browser_updatesCompletePatchWithBadCompleteSize.js
@@ -0,0 +1,34 @@
+add_task(function* testCompletePatchWithBadCompleteSize() {
+  SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_DOWNLOADPROMPTMAXATTEMPTS, 2]]});
+
+  let updateParams = "completePatchOnly=1&invalidCompleteSize=1";
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      // if we fail maxBackgroundErrors download attempts, then we want to
+      // first show the user an update available prompt.
+      notificationId: "update-available",
+      button: "button"
+    },
+    {
+      notificationId: "update-available",
+      button: "button"
+    },
+    {
+      // 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() {
+        checkWhatsNewLink("update-manual-whats-new");
+      },
+      *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/appUpdate/browser_updatesDownloadFailures.js
@@ -0,0 +1,37 @@
+add_task(function* testDownloadFailures() {
+  const maxBackgroundErrors = 5;
+  SpecialPowers.pushPrefEnv({set: [
+    [PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors],
+    [PREF_APP_UPDATE_DOWNLOADPROMPTMAXATTEMPTS, 2]
+  ]});
+  let updateParams = "badURL=1";
+
+  // Open a new window to make sure that our pref management isn't duplicated.
+  let extraWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      // if we fail maxBackgroundErrors download attempts, then we want to
+      // first show the user an update available prompt.
+      notificationId: "update-available",
+      button: "button"
+    },
+    {
+      notificationId: "update-available",
+      button: "button"
+    },
+    {
+      notificationId: "update-manual",
+      button: "button",
+      *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();
+      }
+    },
+  ]);
+
+  yield BrowserTestUtils.closeWindow(extraWindow);
+});
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/browser_updatesMalformedXml.js
@@ -0,0 +1,29 @@
+add_task(function* testMalformedXml() {
+  const updateDetailsUrl = "http://example.com/details";
+  const maxBackgroundErrors = 10;
+  SpecialPowers.pushPrefEnv({set: [
+    [PREF_APP_UPDATE_BACKGROUNDMAXERRORS, maxBackgroundErrors],
+    [PREF_APP_UPDATE_URL_DETAILS, updateDetailsUrl]
+  ]});
+
+  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() {
+        checkWhatsNewLink("update-manual-whats-new", updateDetailsUrl);
+      },
+      *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/appUpdate/browser_updatesPartialPatchApplyFailure.js
@@ -0,0 +1,26 @@
+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() {
+        checkWhatsNewLink("update-manual-whats-new");
+      },
+      *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/appUpdate/browser_updatesPartialPatchApplyFailureWithCompleteAvailable.js
@@ -0,0 +1,22 @@
+add_task(function* testPartialPatchApplyFailureWithCompleteAvailable() {
+  let patches = getLocalPatchString("partial", null, null, null, null, null,
+                                    STATE_PENDING) +
+                getLocalPatchString("complete", null, 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/appUpdate/browser_updatesPartialPatchApplyFailureWithCompleteValidationFailure.js
@@ -0,0 +1,33 @@
+add_task(function* testPartialPatchApplyFailureWithCompleteValidationFailure() {
+  // because of the way we're simulating failure, we have to just pretend we've already
+  // retried.
+  SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_DOWNLOADPROMPTMAXATTEMPTS, 0]]});
+  let patches = getLocalPatchString("partial", null, null, null, null, null,
+                                    STATE_PENDING) +
+                getLocalPatchString("complete", null, "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() {
+        checkWhatsNewLink("update-manual-whats-new");
+      },
+      *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/appUpdate/browser_updatesPartialPatchWithBadPartialSize.js
@@ -0,0 +1,33 @@
+add_task(function* testPartialPatchWithBadPartialSize() {
+  SpecialPowers.pushPrefEnv({set: [[PREF_APP_UPDATE_DOWNLOADPROMPTMAXATTEMPTS, 2]]});
+  let updateParams = "partialPatchOnly=1&invalidPartialSize=1";
+
+  yield runUpdateTest(updateParams, 1, [
+    {
+      // if we fail maxBackgroundErrors download attempts, then we want to
+      // first show the user an update available prompt.
+      notificationId: "update-available",
+      button: "button"
+    },
+    {
+      notificationId: "update-available",
+      button: "button"
+    },
+    {
+      // 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() {
+        checkWhatsNewLink("update-manual-whats-new");
+      },
+      *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/appUpdate/downloadPage.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>Download page</title>
+  <meta charset="utf-8">
+</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/appUpdate/simple.mar" data-link-type="download">
+  Download
+</a>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/head.js
@@ -0,0 +1,360 @@
+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 BIN_SUFFIX = (IS_WIN ? ".exe" : "");
+const FILE_UPDATER_BIN = "updater" + (IS_MACOSX ? ".app" : BIN_SUFFIX);
+const FILE_UPDATER_BIN_BAK = FILE_UPDATER_BIN + ".bak";
+
+let gRembemberedPrefs = [];
+
+const DATA_URI_SPEC =  "chrome://mochitests/content/browser/browser/base/content/test/appUpdate/";
+
+var DEBUG_AUS_TEST = true;
+var gUseTestUpdater = false;
+
+const LOG_FUNCTION = info;
+
+/* import-globals-from testConstants.js */
+Services.scriptloader.loadSubScript(DATA_URI_SPEC + "testConstants.js", this);
+/* import-globals-from ../../../../../toolkit/mozapps/update/tests/data/shared.js */
+Services.scriptloader.loadSubScript(DATA_URI_SPEC + "shared.js", this);
+
+var gURLData = URL_HOST + "/" + REL_PATH_DATA;
+const URL_MANUAL_UPDATE = gURLData + "downloadPage.html";
+
+const NOTIFICATIONS = [
+  "update-available",
+  "update-manual",
+  "update-restart"
+];
+
+/**
+ * Delay for a very short period. Useful for moving the code after this
+ * to the back of the event loop.
+ *
+ * @return A promise which will resolve after a very short period.
+ */
+function delay() {
+  return new Promise(resolve => executeSoon(resolve));
+}
+
+/**
+ * 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);
+}
+
+/**
+ * Clean up updates list and the updates directory.
+ */
+function cleanUpUpdates() {
+  gUpdateManager.activeUpdate = null;
+  gUpdateManager.saveUpdates();
+
+  removeUpdateDirsAndFiles();
+}
+
+/**
+ * Runs a typical update test. Will set various common prefs for using the
+ * updater doorhanger, runs the provided list of steps, and makes sure
+ * everything is cleaned up afterwards.
+ *
+ * @param  updateParams
+ *         URL-encoded params which will be sent to update.sjs.
+ * @param  checkAttempts
+ *         How many times to check for updates. Useful for testing the UI
+ *         for check failures.
+ * @param  steps
+ *         A list of test steps to perform, specifying expected doorhangers
+ *         and additional validation/cleanup callbacks.
+ * @return A promise which will resolve once all of the steps have been run
+ *         and cleanup has been performed.
+ */
+function runUpdateTest(updateParams, checkAttempts, steps) {
+  return Task.spawn(function*() {
+    registerCleanupFunction(() => {
+      gMenuButtonUpdateBadge.uninit();
+      gMenuButtonUpdateBadge.init();
+      cleanUpUpdates();
+    });
+    yield SpecialPowers.pushPrefEnv({
+      set: [
+        [PREF_APP_UPDATE_DOWNLOADPROMPTATTEMPTS, 0],
+        [PREF_APP_UPDATE_ENABLED, true],
+        [PREF_APP_UPDATE_IDLETIME, 0],
+        [PREF_APP_UPDATE_URL_MANUAL, URL_MANUAL_UPDATE],
+        [PREF_APP_UPDATE_LOG, DEBUG_AUS_TEST],
+      ]});
+
+    yield setupTestUpdater();
+
+    let url = URL_HTTP_UPDATE_SJS +
+              "?" + updateParams +
+              getVersionParams();
+
+    setUpdateURL(url);
+
+    executeSoon(() => {
+      Task.spawn(function*() {
+        gAUS.checkForBackgroundUpdates();
+        for (var i = 0; i < checkAttempts - 1; i++) {
+          yield waitForEvent("update-error", "check-attempt-failed");
+          gAUS.checkForBackgroundUpdates();
+        }
+      });
+    });
+
+    for (let step of steps) {
+      yield processStep(step);
+    }
+
+    yield finishTestRestoreUpdaterBackup();
+  });
+}
+
+/**
+ * Runs a test which processes an update. Similar to runUpdateTest.
+ *
+ * @param  updates
+ *         A list of updates to process.
+ * @param  steps
+ *         A list of test steps to perform, specifying expected doorhangers
+ *         and additional validation/cleanup callbacks.
+ * @return A promise which will resolve once all of the steps have been run
+ *         and cleanup has been performed.
+ */
+function runUpdateProcessingTest(updates, steps) {
+  return Task.spawn(function*() {
+    registerCleanupFunction(() => {
+      gMenuButtonUpdateBadge.reset();
+      cleanUpUpdates();
+    });
+
+    SpecialPowers.pushPrefEnv({
+      set: [
+        [PREF_APP_UPDATE_DOWNLOADPROMPTATTEMPTS, 0],
+        [PREF_APP_UPDATE_ENABLED, true],
+        [PREF_APP_UPDATE_IDLETIME, 0],
+        [PREF_APP_UPDATE_URL_MANUAL, URL_MANUAL_UPDATE],
+        [PREF_APP_UPDATE_LOG, DEBUG_AUS_TEST],
+      ]});
+
+    yield setupTestUpdater();
+
+    writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+
+    writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+    writeStatusFile(STATE_FAILED_CRC_ERROR);
+    reloadUpdateManagerData();
+
+    testPostUpdateProcessing();
+
+    for (let step of steps) {
+      yield processStep(step);
+    }
+
+    yield finishTestRestoreUpdaterBackup();
+  });
+}
+
+function processStep({notificationId, button, beforeClick, cleanup}) {
+  return Task.spawn(function*() {
+
+    yield BrowserTestUtils.waitForEvent(PanelUI.notificationPanel, "popupshown");
+    const shownNotification = PanelUI.activeNotification.id;
+
+    is(shownNotification, notificationId, "The right notification showed up.");
+    if (shownNotification != notificationId) {
+      if (cleanup) {
+        yield cleanup();
+      }
+      return;
+    }
+
+    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 cleanup();
+    }
+  });
+}
+
+/**
+ * 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(innerStatus);
+      }
+    }
+  }, topic, false))
+}
+
+/**
+ * Ensures that the "What's new" link with the provided ID is displayed and
+ * matches the url parameter provided. If no URL is provided, it will instead
+ * ensure that the link matches the default link URL.
+ *
+ * @param  id
+ *         The ID of the "What's new" link element.
+ * @param  url (optional)
+ *         The URL to check against. If none is provided, a default will be used.
+ */
+function checkWhatsNewLink(id, url) {
+  let whatsNewLink = document.getElementById(id);
+  is(whatsNewLink.href,
+     url || URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS",
+     "What's new link points to the test_details URL");
+  is(whatsNewLink.hidden, false, "What's new link is not hidden.");
+}
+
+/**
+ * For tests that use the test updater restores the backed up real updater if
+ * it exists and tries again on failure since Windows debug builds at times
+ * leave the file in use. After success moveRealUpdater is called to continue
+ * the setup of the test updater. For tests that don't use the test updater
+ * runTest will be called.
+ */
+function setupTestUpdater() {
+  return Task.spawn(function*() {
+    if (gUseTestUpdater) {
+      try {
+        restoreUpdaterBackup();
+      } catch (e) {
+        logTestInfo("Attempt to restore the backed up updater failed... " +
+                    "will try again, Exception: " + e);
+        yield delay();
+        yield setupTestUpdater();
+        return;
+      }
+      yield moveRealUpdater();
+    }
+  });
+}
+
+/**
+ * Backs up the real updater and tries again on failure since Windows debug
+ * builds at times leave the file in use. After success it will call
+ * copyTestUpdater to continue the setup of the test updater.
+ */
+function moveRealUpdater() {
+  return Task.spawn(function*() {
+    try {
+      // Move away the real updater
+      let baseAppDir = getAppBaseDir();
+      let updater = baseAppDir.clone();
+      updater.append(FILE_UPDATER_BIN);
+      updater.moveTo(baseAppDir, FILE_UPDATER_BIN_BAK);
+    } catch (e) {
+      logTestInfo("Attempt to move the real updater out of the way failed... " +
+                  "will try again, Exception: " + e);
+      yield delay();
+      yield moveRealUpdater();
+      return;
+    }
+
+    yield copyTestUpdater();
+  });
+}
+
+/**
+ * Copies the test updater so it can be used by tests and tries again on failure
+ * since Windows debug builds at times leave the file in use. After success it
+ * will call runTest to continue the test.
+ */
+function copyTestUpdater() {
+  return Task.spawn(function*() {
+    try {
+      // Copy the test updater
+      let baseAppDir = getAppBaseDir();
+      let testUpdaterDir = Services.dirsvc.get("CurWorkD", Ci.nsILocalFile);
+      let relPath = REL_PATH_DATA;
+      let pathParts = relPath.split("/");
+      for (let i = 0; i < pathParts.length; ++i) {
+        testUpdaterDir.append(pathParts[i]);
+      }
+
+      let testUpdater = testUpdaterDir.clone();
+      testUpdater.append(FILE_UPDATER_BIN);
+      testUpdater.copyToFollowingLinks(baseAppDir, FILE_UPDATER_BIN);
+    } catch (e) {
+      logTestInfo("Attempt to copy the test updater failed... " +
+                  "will try again, Exception: " + e);
+      yield delay();
+      yield copyTestUpdater();
+    }
+  });
+}
+
+/**
+ * Restores the updater that was backed up. This is called in setupTestUpdater
+ * before the backup of the real updater is done in case the previous test
+ * failed to restore the updater, in finishTestDefaultWaitForWindowClosed when
+ * the test has finished, and in test_9999_cleanup.xul after all tests have
+ * finished.
+ */
+function restoreUpdaterBackup() {
+  let baseAppDir = getAppBaseDir();
+  let updater = baseAppDir.clone();
+  let updaterBackup = baseAppDir.clone();
+  updater.append(FILE_UPDATER_BIN);
+  updaterBackup.append(FILE_UPDATER_BIN_BAK);
+  if (updaterBackup.exists()) {
+    if (updater.exists()) {
+      updater.remove(true);
+    }
+    updaterBackup.moveTo(baseAppDir, FILE_UPDATER_BIN);
+  }
+}
+
+/**
+ * When a test finishes this will repeatedly attempt to restore the real updater
+ * for tests that use the test updater and then call
+ * finishTestDefaultWaitForWindowClosed after the restore is successful.
+ */
+function finishTestRestoreUpdaterBackup() {
+  return Task.spawn(function*() {
+    if (gUseTestUpdater) {
+      try {
+        // Windows debug builds keep the updater file in use for a short period of
+        // time after the updater process exits.
+        restoreUpdaterBackup();
+      } catch (e) {
+        logTestInfo("Attempt to restore the backed up updater failed... " +
+                    "will try again, Exception: " + e);
+
+        yield delay();
+        yield finishTestRestoreUpdaterBackup();
+      }
+    }
+  });
+}
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/appUpdate/testConstants.js
@@ -0,0 +1,4 @@
+const REL_PATH_DATA = "browser/browser/base/content/test/appUpdate/";
+const URL_HOST = "http://example.com";
+const URL_PATH_UPDATE_XML = "/" + REL_PATH_DATA + "update.sjs";
+const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML;
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -633,17 +633,16 @@ tags = psm
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_contextmenu_childprocess.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_bug963945.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_domFullscreen_fullscreenMode.js]
 tags = fullscreen
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
-[browser_menuButtonBadgeManager.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_newTabDrop.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_newWindowDrop.js]
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
 [browser_newwindow_focus.js]
 skip-if = (os == "linux" && !e10s) # Bug 1263254 - Perma fails on Linux without e10s for some reason.
 # DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
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)");
-});
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -32,23 +32,33 @@ BROWSER_CHROME_MANIFESTS += [
     'content/test/tabcrashed/browser.ini',
     'content/test/tabPrompts/browser.ini',
     'content/test/tabs/browser.ini',
     'content/test/urlbar/browser.ini',
     'content/test/webextensions/browser.ini',
     'content/test/webrtc/browser.ini',
 ]
 
+if CONFIG['MOZ_UPDATER']:
+    BROWSER_CHROME_MANIFESTS += ['content/test/appUpdate/browser.ini']
+
 DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
 DEFINES['MOZ_APP_VERSION_DISPLAY'] = CONFIG['MOZ_APP_VERSION_DISPLAY']
 
 DEFINES['APP_LICENSE_BLOCK'] = '%s/content/overrides/app-license.html' % SRCDIR
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3', 'cocoa'):
     DEFINES['CONTEXT_COPY_IMAGE_CONTENTS'] = 1
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'cocoa'):
     DEFINES['CAN_DRAW_IN_TITLEBAR'] = 1
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'gtk2', 'gtk3'):
     DEFINES['MENUBAR_CAN_AUTOHIDE'] = 1
 
+TEST_HARNESS_FILES.testing.mochitest.browser.browser.base.content.test.appUpdate += [
+    '/toolkit/mozapps/update/tests/chrome/update.sjs',
+    '/toolkit/mozapps/update/tests/data/shared.js',
+    '/toolkit/mozapps/update/tests/data/sharedUpdateXML.js',
+    '/toolkit/mozapps/update/tests/data/simple.mar',
+]
+
 JAR_MANIFESTS += ['jar.mn']
--- a/browser/branding/official/pref/firefox-branding.js
+++ b/browser/branding/official/pref/firefox-branding.js
@@ -21,14 +21,14 @@ pref("app.update.url.manual", "https://w
 pref("app.update.url.details", "https://www.mozilla.org/%LOCALE%/firefox/notes");
 
 // The number of days a binary is permitted to be old
 // without checking for an update.  This assumes that
 // app.update.checkInstallTime is true.
 pref("app.update.checkInstallTime.days", 63);
 
 // Give the user x seconds to reboot before showing a badge on the hamburger
-// button. default=immediately
-pref("app.update.badgeWaitTime", 0);
+// button. default=4 days
+pref("app.update.badgeWaitTime", 345600);
 
 // Number of usages of the web console or scratchpad.
 // If this is less than 5, then pasting code into the web console or scratchpad is disabled
 pref("devtools.selfxss.count", 0);
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -12,19 +12,27 @@
   <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">
         <vbox id="PanelUI-footer-addons"></vbox>
-        <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-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;"
@@ -409,8 +417,71 @@
       <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"
+       nopreventnavboxhide="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-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>
+</panel>
--- a/browser/components/customizableui/content/panelUI.js
+++ b/browser/components/customizableui/content/panelUI.js
@@ -27,37 +27,51 @@ 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);
+
+    Services.obs.addObserver(this, "fullscreen-nav-toolbox", false);
+    Services.obs.addObserver(this, "panelUI-notification-main-action", false);
+    Services.obs.addObserver(this, "panelUI-notification-dismissed", false);
+
+    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,17 +84,24 @@ 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);
     }
+
+    Services.obs.removeObserver(this, "fullscreen-nav-toolbox");
+    Services.obs.removeObserver(this, "panelUI-notification-main-action");
+    Services.obs.removeObserver(this, "panelUI-notification-dismissed");
+
+    window.removeEventListener("fullscreen", 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;
   },
 
@@ -151,68 +172,160 @@ const PanelUI = {
         } else {
           anchor = aEvent.target;
         }
 
         this.panel.addEventListener("popupshown", function() {
           resolve();
         }, {once: true});
 
-        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();
+  },
+
+  dismissNotification(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 => n.dismissed = true);
+    this._updateNotifications();
+  },
+
   /**
    * If the menu panel is being shown, hide it.
    */
   hide() {
     if (document.documentElement.hasAttribute("customizing")) {
       return;
     }
 
     this.panel.hidePopup();
   },
 
+  observe(subject, topic, status) {
+    switch (topic) {
+      case "fullscreen-nav-toolbox":
+        this._updateNotifications();
+        break;
+      case "panelUI-notification-main-action":
+        if (subject != window) {
+          this.removeNotification(status);
+        }
+        break;
+      case "panelUI-notification-dismissed":
+        if (subject != window) {
+          this.dismissNotification(status);
+        }
+        break;
+    }
+  },
+
   handleEvent(aEvent) {
     // 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() {
+    if (this.notifications.length > 0) {
+      const doorhanger =
+        this.notifications.find(n => !n.dismissed && !n.options.badgeOnly);
+      return doorhanger || this.notifications[0];
+    }
+
+    return 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
@@ -388,24 +501,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).
    */
@@ -533,19 +645,246 @@ const PanelUI = {
     quitButton.setAttribute("tooltiptext", tooltipString);
   },
 
   _overlayScrollListenerBoundFn: null,
   _overlayScrollListener(aMQL) {
     ScrollbarSampler.resetSystemScrollbarWidth();
     this._scrollWidth = null;
   },
+
+  _hidePopup() {
+    if (this.isNotificationPanelOpen) {
+      this.notificationPanel.hidePopup();
+    }
+  },
+
+  _updateNotifications() {
+    if (!this.notifications.length) {
+      this._clearAllNotifications();
+      this._hidePopup();
+      return;
+    }
+
+    if (window.fullScreen && FullScreen.navToolboxHidden) {
+      this._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._hidePopup();
+      this._clearBadge();
+      if (!this.notifications[0].options.badgeOnly) {
+        this._showMenuItem(this.notifications[0]);
+      }
+    } else if (doorhangers.length > 0) {
+      this._clearBadge();
+      this._showNotificationPanel(doorhangers[0]);
+    } else {
+      this._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");
+  },
+
+  _clearNotificationPanel() {
+    for (let popupnotification of this.notificationPanel.children) {
+      popupnotification.hidden = true;
+      popupnotification.notification = null;
+    }
+  },
+
+  _clearAllNotifications() {
+    this._clearNotificationPanel();
+    this._clearBadge();
+    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;
+    popupnotification.hidden = false;
+  },
+
+  _showBadge(notification) {
+    let badgeStatus = this._getBadgeStatus(notification);
+    this.menuButton.setAttribute("badge-status", badgeStatus);
+  },
+
+  // "Menu item" here refers to an item in the hamburger panel menu. They will
+  // typically show up as a colored row near the bottom of the panel.
+  _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;
+    }
+  },
+
+  _clearBadge() {
+    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 {
+        if (action === notification.mainAction) {
+          action.callback(true);
+          this._notify(notification.id, "main-action");
+        } else {
+          action.callback();
+        }
+      } catch (error) {
+        Cu.reportError(error);
+      }
+
+      dismiss = action.dismiss;
+    }
+
+    if (dismiss) {
+      notification.dismissed = true;
+      this._notify(notification.id, "dismissed");
+    } 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(false);
+      this._notify(target.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(status, topic) {
+    Services.obs.notifyObservers(window, "panelUI-notification-" + topic, status);
+  }
 };
 
 XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
 
 /**
  * Gets the currently selected locale for display.
  * @return  the selected locale
  */
 function getLocale() {
   return Services.locale.getAppLocaleAsLangTag();
 }
+
+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,11 +145,12 @@ 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_exit_background_customize_mode.js]
 [browser_panel_toggle.js]
+[browser_panelUINotifications.js]
 [browser_switch_to_customize_mode.js]
 [browser_synced_tabs_menu.js]
 [browser_check_tooltips_in_navbar.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_panelUINotifications.js
@@ -0,0 +1,305 @@
+"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() {
+  let options = {
+    gBrowser: window.gBrowser,
+    url: "about:blank"
+  };
+
+  let extraWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+  yield BrowserTestUtils.withNewTab(options, 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);
+
+    let extraMainActionCalled = false;
+    let extraMainAction = {
+      callback: () => { extraMainActionCalled = true; }
+    };
+    extraWindow.PanelUI.showNotification("update-manual", extraMainAction)
+
+    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+    let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+    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");
+    isnot(extraMainActionCalled, true, "Extra window's main action callback was not called");
+    is(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is closed.");
+    is(extraWindow.PanelUI.notificationPanel.state, "closed", "Extra window's update-manual doorhanger is closed.");
+    is(PanelUI.menuButton.hasAttribute("badge-status"), false, "Should not have a badge status");
+  });
+
+  yield BrowserTestUtils.closeWindow(extraWindow);
+});
+
+/**
+ * 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() {
+  let options = {
+    gBrowser: window.gBrowser,
+    url: "about:blank"
+  };
+
+  let extraWindow = yield BrowserTestUtils.openNewBrowserWindow();
+
+  yield BrowserTestUtils.withNewTab(options, 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);
+
+    let extraMainActionCalled = false;
+    let extraMainAction = {
+      callback: () => { extraMainActionCalled = true; }
+    };
+    extraWindow.PanelUI.showNotification("update-manual", extraMainAction)
+
+    isnot(PanelUI.notificationPanel.state, "closed", "update-manual doorhanger is showing.");
+    let notifications = [...PanelUI.notificationPanel.children].filter(n => !n.hidden);
+    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(extraWindow.PanelUI.notificationPanel.state, "closed", "Extra window's 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");
+    isnot(extraMainActionCalled, true, "Extra window's main action callback was not called");
+
+    PanelUI.removeNotification(/.*/);
+  });
+
+  yield BrowserTestUtils.closeWindow(extraWindow);
+});
+
+/**
+ * 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].filter(n => !n.hidden);
+    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].filter(n => !n.hidden);
+    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
@@ -502,25 +502,23 @@ const DownloadsIndicatorView = {
     // progress in toolbar
     let suppressAttention = DownloadsCommon.arrowStyledIndicator && !inMenu &&
       this._attention == DownloadsCommon.ATTENTION_SUCCESS &&
       this._percentComplete >= 0;
 
     if (suppressAttention || this._attention == DownloadsCommon.ATTENTION_NONE) {
       this.indicator.removeAttribute("attention");
       if (inMenu) {
-        gMenuButtonBadgeManager.removeBadge(
-                                      gMenuButtonBadgeManager.BADGEID_DOWNLOAD);
+        PanelUI.removeNotification(/^download-/);
       }
     } else {
       this.indicator.setAttribute("attention", this._attention);
       if (inMenu) {
         let badgeClass = "download-" + this._attention;
-        gMenuButtonBadgeManager.addBadge(
-                          gMenuButtonBadgeManager.BADGEID_DOWNLOAD, badgeClass);
+        PanelUI.showBadgeOnlyNotification(badgeClass);
       }
     }
   },
   _attention: DownloadsCommon.ATTENTION_NONE,
 
   // User interface event functions
 
   onWindowUnload() {
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -893,8 +893,36 @@ 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">
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -756,25 +756,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,41 @@
   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-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-available-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-available-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));
 }
 
@@ -439,21 +456,21 @@ toolbaritem[cui-areatype="menu-panel"][s
 }
 
 #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-notification-menu-item,
 #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-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"]) {
   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*
@@ -561,38 +578,25 @@ 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-footer-addons > toolbarbutton::after {
+#PanelUI-footer-addons > toolbarbutton::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-footer-addons > toolbarbutton {
   background-color: #FFEFBF;
   /* Force border to override `#PanelUI-footer-addons > toolbarbutton` selector below */
   border-top: 1px solid hsl(45, 100%, 77%) !important;
   display: flex;
   flex: 1 1 0%;
   width: calc(@menuPanelWidth@ + 30px);
   padding-inline-start: 15px;
@@ -641,17 +645,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-footer-addons > toolbarbutton,
 #PanelUI-customize,
 #PanelUI-quit {
   margin: 0;
   padding: 11px 0;
@@ -660,50 +664,47 @@ 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-footer-addons > toolbarbutton > .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;
 }
 
@@ -720,26 +721,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;
 }
@@ -975,44 +974,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;
@@ -1707,17 +1691,19 @@ 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-available-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);
   }
 
@@ -1744,17 +1730,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
@@ -301,8 +301,16 @@ 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-restart"] {
+  background: #74BF43 url(chrome://browser/skin/notification-icons.svg#update) no-repeat center;
+  border-radius: 50%;
+}
--- a/browser/themes/shared/notification-icons.svg
+++ b/browser/themes/shared/notification-icons.svg
@@ -39,16 +39,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" />
@@ -57,16 +63,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="blocked-clipPath">
       <path d="m 0,0 0,31 31,-31 z m 6,32 26,0 0,-26 z"/>
     </clipPath>
 
     <mask id="i-mask" style="fill-opacity: 1;">
       <rect fill="white" width="32" height="32"/>
       <circle fill="black" cx="16" cy="9" r="2.5"/>
@@ -104,11 +111,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/testing/talos/talos/config.py
+++ b/testing/talos/talos/config.py
@@ -169,17 +169,16 @@ DEFAULTS = dict(
         'browser.webapps.checkForUpdates': 0,
         'browser.search.geoSpecificDefaults': False,
         'browser.snippets.enabled': False,
         'browser.snippets.syncPromo.enabled': False,
         'toolkit.telemetry.server': 'https://127.0.0.1/telemetry-dummy/',
         'experiments.manifest.uri':
             'https://127.0.0.1/experiments-dummy/manifest',
         'network.http.speculative-parallel-limit': 0,
-        'app.update.badge': False,
         'lightweightThemes.selectedThemeID': "",
         'devtools.webide.widget.enabled': False,
         'devtools.webide.widget.inNavbarByDefault': False,
         'devtools.chrome.enabled': False,
         'devtools.debugger.remote-enabled': False,
         'devtools.theme': "light",
         'devtools.timeline.enabled': False,
         'identity.fxaccounts.migrateToDevEdition': False,
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5147,16 +5147,52 @@
   "UPDATE_WIZ_LAST_PAGE_CODE": {
     "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 30,
     "releaseChannelCollection": "opt-out",
     "description": "Update: the update wizard page displayed when the UI was closed (mapped in toolkit/mozapps/update/UpdateTelemetry.jsm)"
   },
+  "UPDATE_NOTIFICATION_SHOWN": {
+    "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+    "expires_in_version": "never",
+    "kind": "categorical",
+    "bug_numbers": [893505],
+    "releaseChannelCollection": "opt-out",
+    "description": "Update: the application update doorhanger type that was displayed.",
+    "labels": ["restart", "available", "manual"]
+  },
+  "UPDATE_NOTIFICATION_DISMISSED": {
+    "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+    "expires_in_version": "never",
+    "kind": "categorical",
+    "bug_numbers": [893505],
+    "releaseChannelCollection": "opt-out",
+    "description": "Update: the dismiss action was executed for this application update doorhanger type.",
+    "labels": ["restart", "available", "manual"]
+  },
+  "UPDATE_NOTIFICATION_MAIN_ACTION_DOORHANGER": {
+    "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+    "expires_in_version": "never",
+    "kind": "categorical",
+    "bug_numbers": [893505],
+    "releaseChannelCollection": "opt-out",
+    "description": "Update: the main update action was initiated for this application update doorhanger type.",
+    "labels": ["restart", "available", "manual"]
+  },
+  "UPDATE_NOTIFICATION_MAIN_ACTION_MENU": {
+    "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+    "expires_in_version": "never",
+    "kind": "categorical",
+    "bug_numbers": [893505],
+    "releaseChannelCollection": "opt-out",
+    "description": "Update: the update action was initiated from the PanelUI application update menu item.",
+    "labels": ["restart", "available", "manual"]
+  },
   "THUNDERBIRD_GLODA_SIZE_MB": {
     "expires_in_version": "never",
     "kind": "linear",
     "high": 1000,
     "n_buckets": 40,
     "description": "Gloda: size of global-messages-db.sqlite (MB)"
   },
   "THUNDERBIRD_CONVERSATIONS_TIME_TO_2ND_GLODA_QUERY_MS": {
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -22,18 +22,23 @@ const UPDATESERVICE_CONTRACTID = "@mozil
 const PREF_APP_UPDATE_ALTWINDOWTYPE        = "app.update.altwindowtype";
 const PREF_APP_UPDATE_AUTO                 = "app.update.auto";
 const PREF_APP_UPDATE_BACKGROUNDINTERVAL   = "app.update.download.backgroundInterval";
 const PREF_APP_UPDATE_BACKGROUNDERRORS     = "app.update.backgroundErrors";
 const PREF_APP_UPDATE_BACKGROUNDMAXERRORS  = "app.update.backgroundMaxErrors";
 const PREF_APP_UPDATE_CANCELATIONS         = "app.update.cancelations";
 const PREF_APP_UPDATE_CANCELATIONS_OSX     = "app.update.cancelations.osx";
 const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max";
+const PREF_APP_UPDATE_DOORHANGER           = "app.update.doorhanger";
+const PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS    = "app.update.download.attempts";
+const PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS = "app.update.download.maxAttempts";
 const PREF_APP_UPDATE_ELEVATE_NEVER        = "app.update.elevate.never";
 const PREF_APP_UPDATE_ELEVATE_VERSION      = "app.update.elevate.version";
+const PREF_APP_UPDATE_ELEVATE_ATTEMPTS     = "app.update.elevate.attempts";
+const PREF_APP_UPDATE_ELEVATE_MAXATTEMPTS  = "app.update.elevate.maxAttempts";
 const PREF_APP_UPDATE_ENABLED              = "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";
@@ -1185,16 +1190,33 @@ 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) {
+    if (getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false)) {
+      let elevationAttempts = getPref("getIntPref", PREF_APP_UPDATE_ELEVATE_ATTEMPTS, 0);
+      elevationAttempts++;
+      Services.prefs.setIntPref(PREF_APP_UPDATE_ELEVATE_ATTEMPTS, elevationAttempts);
+      let maxAttempts = Math.min(getPref("getIntPref", PREF_APP_UPDATE_ELEVATE_MAXATTEMPTS, 2), 10);
+
+      if (elevationAttempts > maxAttempts) {
+        LOG("handleUpdateFailure - notifying observers of error. " +
+            "topic: update-error, status: elevation-attempts-exceeded");
+        Services.obs.notifyObservers(update, "update-error", "elevation-attempts-exceeded");
+      } else {
+        LOG("handleUpdateFailure - notifying observers of error. " +
+            "topic: update-error, status: elevation-attempt-failed");
+        Services.obs.notifyObservers(update, "update-error", "elevation-attempt-failed");
+      }
+    }
+
     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,
@@ -1280,17 +1302,21 @@ function handleFallbackToCompleteUpdate(
         "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 {
     LOG("handleFallbackToCompleteUpdate - install of complete or " +
-        "only one patch offered failed.");
+        "only one patch offered failed. Notifying observers. topic: " +
+        "update-error, status: unknown, " +
+        "update.patchCount: " + update.patchCount +
+        "oldType: " + oldType);
+    Services.obs.notifyObservers(update, "update-error", "unknown");
   }
   update.QueryInterface(Ci.nsIWritablePropertyBag);
   update.setProperty("patchingFailed", oldType);
 }
 
 function pingStateAndStatusCodes(aUpdate, aStartup, aStatus) {
   let patchType = AUSTLMY.PATCH_UNKNOWN;
   if (aUpdate && aUpdate.selectedPatch && aUpdate.selectedPatch.type) {
@@ -2073,16 +2099,18 @@ UpdateService.prototype = {
     if (status == STATE_SUCCEEDED) {
       update.statusText = gUpdateBundle.GetStringFromName("installSuccess");
 
       // Update the patch's metadata.
       um.activeUpdate = update;
 
       // Done with this update. Clean it up.
       cleanupActiveUpdate();
+
+      Services.prefs.setIntPref(PREF_APP_UPDATE_ELEVATE_ATTEMPTS, 0);
     } else if (status == STATE_PENDING_ELEVATE) {
       let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
                      createInstance(Ci.nsIUpdatePrompt);
       prompter.showUpdateElevationRequired();
     } else {
       // If there was an I/O error it is assumed that the patch is not invalid
       // and it is set to pending so an attempt to apply it again will happen
       // when the application is restarted.
@@ -2159,19 +2187,25 @@ 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);
+      LOG("UpdateService:onError - notifying observers of error. " +
+          "topic: update-error, status: check-attempts-exceeded");
+      Services.obs.notifyObservers(update, "update-error", "check-attempts-exceeded");
       prompter.showUpdateError(update);
       AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_PROMPT);
     } else {
+      LOG("UpdateService:onError - notifying observers of error. " +
+          "topic: update-error, status: check-attempt-failed");
+      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() {
@@ -2513,29 +2547,35 @@ UpdateService.prototype = {
 
     var update = this.selectUpdate(updates, updates.length);
     if (!update || update.elevationFailure) {
       return;
     }
 
     if (update.unsupported) {
       LOG("UpdateService:_selectAndInstallUpdate - update not supported for " +
-          "this system");
+          "this system. Notifying observers. topic: update-available, " +
+          "status: unsupported");
       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");
+          "apply updates... prompting. Notifying observers. " +
+          "topic: update-available, status: cant-apply");
+
+      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
@@ -2549,26 +2589,32 @@ UpdateService.prototype = {
      * If the update when it is first read does not have an appVersion attribute
      * the following deprecated behavior will occur:
      * Update Type   Outcome
      * Major         Notify
      * Minor         Auto Install
      */
     if (update.showPrompt) {
       LOG("UpdateService:_selectAndInstallUpdate - prompting because the " +
-          "update snippet specified showPrompt");
+          "update snippet specified showPrompt. Notifying observers. " +
+          "topic: update-available, status: 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");
+          "install is disabled. Notifying observers. topic: update-available, " +
+          "status: show-prompt");
       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();
@@ -3100,21 +3146,21 @@ UpdateManager.prototype = {
       if (!handleUpdateFailure(update, parts[1])) {
         handleFallbackToCompleteUpdate(update, true);
       }
     }
     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.
+    // Send an observer notification which the app update doorhanger uses to
+    // display a restart notification
     LOG("UpdateManager:refreshUpdateStatus - Notifying observers that " +
-        "the update was staged. state: " + update.state + ", status: " + status);
-    Services.obs.notifyObservers(null, "update-staged", update.state);
+        "the update was staged. topic: update-staged, status: " + 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.
@@ -3229,18 +3275,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.
@@ -3812,19 +3856,19 @@ Downloader.prototype = {
         }
       }
     }
 
     update.QueryInterface(Ci.nsIPropertyBag);
     let interval = this.background ? update.getProperty("backgroundInterval")
                                    : DOWNLOAD_FOREGROUND_INTERVAL;
 
+    LOG("Downloader:downloadUpdate - url: " + this._patch.URL + ", path: " +
+        patchFile.path + ", interval: " + interval);
     var uri = Services.io.newURI(this._patch.URL);
-    LOG("Downloader:downloadUpdate - url: " + uri.spec + ", path: " +
-        patchFile.path + ", interval: " + interval);
 
     this._request = Cc["@mozilla.org/network/incremental-download;1"].
                     createInstance(Ci.nsIIncrementalDownload);
     this._request.init(uri, patchFile, DOWNLOAD_CHUNK_SIZE, interval);
     this._request.start(this, null);
 
     writeStatusFile(updateDir, STATE_DOWNLOADING);
     this._patch.QueryInterface(Ci.nsIWritablePropertyBag);
@@ -4012,16 +4056,17 @@ Downloader.prototype = {
         }
         AUSTLMY.pingDownloadCode(this.isCompleteUpdate, AUSTLMY.DWNLD_SUCCESS);
 
         // Tell the updater.exe we're ready to apply.
         writeStatusFile(getUpdatesDir(), state);
         writeVersionFile(getUpdatesDir(), this._update.appVersion);
         this._update.installDate = (new Date()).getTime();
         this._update.statusText = gUpdateBundle.GetStringFromName("installPending");
+        Services.prefs.setIntPref(PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, 0);
       } else {
         LOG("Downloader:onStopRequest - download verification failed");
         state = STATE_DOWNLOAD_FAILED;
         status = Cr.NS_ERROR_CORRUPTED_CONTENT;
 
         // Yes, this code is a string.
         const vfCode = "verification_failed";
         var message = getStatusTextFromCode(vfCode, vfCode);
@@ -4131,17 +4176,36 @@ Downloader.prototype = {
         if (updateStatus == STATE_NONE) {
           cleanupActiveUpdate();
         } else {
           allFailed = false;
         }
       }
 
       if (allFailed) {
-        LOG("Downloader:onStopRequest - all update patch downloads failed");
+        if (getPref("getBoolPref", PREF_APP_UPDATE_DOORHANGER, false)) {
+          let downloadAttempts = getPref("getIntPref", PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, 0);
+          downloadAttempts++;
+          Services.prefs.setIntPref(PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, downloadAttempts);
+          let maxAttempts = Math.min(getPref("getIntPref", PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS, 2), 10);
+
+          if (downloadAttempts > maxAttempts) {
+            LOG("Downloader:onStopRequest - notifying observers of error. " +
+                "topic: update-error, status: download-attempts-exceeded, " +
+                "downloadAttempts: " + downloadAttempts + " " +
+                "maxAttempts: " + maxAttempts);
+            Services.obs.notifyObservers(this._update, "update-error", "download-attempts-exceeded");
+          } else {
+            this._update.selectedPatch.selected = false;
+            LOG("Downloader:onStopRequest - notifying observers of error. " +
+                "topic: update-error, status: download-attempt-failed");
+            Services.obs.notifyObservers(this._update, "update-error", "download-attempt-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"].
@@ -4257,33 +4321,41 @@ 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.
+    LOG("showUpdateDownloaded - Notifying observers that " +
+        "an update was downloaded. topic: update-downloaded, status: " + update.state);
+    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 {
@@ -4292,16 +4364,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/chrome.ini
+++ b/toolkit/mozapps/update/tests/chrome/chrome.ini
@@ -1,15 +1,16 @@
 # 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/.
 
 [DEFAULT]
 tags = appupdate
 support-files =
+  testConstants.js
   utils.js
   update.sjs
 
 # mochitest-chrome tests must start with "test_" and are executed in sorted
 # order and not in the order specified in the manifest.
 [test_0010_background_basic.xul]
 [test_0011_check_basic.xul]
 [test_0012_check_basic_staging.xul]
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/testConstants.js
@@ -0,0 +1,4 @@
+const REL_PATH_DATA = "chrome/toolkit/mozapps/update/tests/data/";
+const URL_HOST = "http://example.com";
+const URL_PATH_UPDATE_XML = "/chrome/toolkit/mozapps/update/tests/chrome/update.sjs";
+const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML;
--- a/toolkit/mozapps/update/tests/chrome/update.sjs
+++ b/toolkit/mozapps/update/tests/chrome/update.sjs
@@ -3,45 +3,48 @@
  */
 
 /**
  * Server side http server script for application update tests.
  */
 
 const { classes: Cc, interfaces: Ci } = Components;
 
-const REL_PATH_DATA = "chrome/toolkit/mozapps/update/tests/data/";
-
 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");
+function loadHelperScript(aScriptFile) {
   let io = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService2);
-  let scriptSpec = io.newFileURI(scriptFile).spec;
+  let scriptSpec = io.newFileURI(aScriptFile).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 = "/chrome/toolkit/mozapps/update/tests/chrome/update.sjs";
-const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML;
+var scriptFile = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsILocalFile);
+scriptFile.initWithPath(getState("__LOCATION__"));
+scriptFile = scriptFile.parent;
+scriptFile.append("testConstants.js");
+loadHelperScript(scriptFile);
+
+scriptFile = getTestDataFile("sharedUpdateXML.js");
+loadHelperScript(scriptFile);
+
 const SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + FILE_SIMPLE_MAR;
+const BAD_SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + "not_here.mar";
 
 const SLOW_MAR_DOWNLOAD_INTERVAL = 100;
 var gTimer;
 
 function handleRequest(aRequest, aResponse) {
   let params = { };
   if (aRequest.queryString) {
     params = parseQueryString(aRequest.queryString);
@@ -103,25 +106,27 @@ function handleRequest(aRequest, aRespon
                                               "unsupported=\"true\" " +
                                               "detailsURL=\"" + URL_HOST +
                                               "\"></update>\n"));
     return;
   }
 
   let size;
   let patches = "";
+  let url = params.badURL ? BAD_SERVICE_URL : SERVICE_URL;
+
   if (!params.partialPatchOnly) {
     size = SIZE_SIMPLE_MAR + (params.invalidCompleteSize ? "1" : "");
-    patches += getRemotePatchString("complete", SERVICE_URL, "SHA512",
+    patches += getRemotePatchString("complete", url, "SHA512",
                                     SHA512_HASH_SIMPLE_MAR, size);
   }
 
   if (!params.completePatchOnly) {
     size = SIZE_SIMPLE_MAR + (params.invalidPartialSize ? "1" : "");
-    patches += getRemotePatchString("partial", SERVICE_URL, "SHA512",
+    patches += getRemotePatchString("partial", 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;
--- a/toolkit/mozapps/update/tests/chrome/utils.js
+++ b/toolkit/mozapps/update/tests/chrome/utils.js
@@ -71,16 +71,19 @@
 
 /* globals TESTS, runTest, finishTest */
 
 const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr,
         utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm", this);
 
+/* import-globals-from testConstants.js */
+Services.scriptloader.loadSubScript("chrome://mochitests/content/chrome/toolkit/mozapps/update/tests/chrome/testConstants.js", this);
+
 const IS_MACOSX = ("nsILocalFileMac" in Ci);
 const IS_WIN = ("@mozilla.org/windows-registry-key;1" in Cc);
 
 // The tests have to use the pageid instead of the pageIndex due to the
 // app update wizard's access method being random.
 const PAGEID_DUMMY            = "dummy";                 // Done
 const PAGEID_CHECKING         = "checking";              // Done
 const PAGEID_NO_UPDATES_FOUND = "noupdatesfound";        // Done
@@ -91,23 +94,19 @@ const PAGEID_DOWNLOADING      = "downloa
 const PAGEID_ERRORS           = "errors";                // Done
 const PAGEID_ERROR_EXTRA      = "errorextra";            // Done
 const PAGEID_ERROR_PATCHING   = "errorpatching";         // Done
 const PAGEID_FINISHED         = "finished";              // Done
 const PAGEID_FINISHED_BKGRD   = "finishedBackground";    // Done
 
 const UPDATE_WINDOW_NAME = "Update:Wizard";
 
-const URL_HOST = "http://example.com";
-const URL_PATH_UPDATE_XML = "/chrome/toolkit/mozapps/update/tests/chrome/update.sjs";
-const REL_PATH_DATA = "chrome/toolkit/mozapps/update/tests/data";
-
 // These two URLs must not contain parameters since tests add their own
 // test specific parameters.
-const URL_HTTP_UPDATE_XML = URL_HOST + URL_PATH_UPDATE_XML;
+const URL_HTTP_UPDATE_XML = URL_HTTP_UPDATE_SJS;
 const URL_HTTPS_UPDATE_XML = "https://example.com" + URL_PATH_UPDATE_XML;
 
 const URI_UPDATE_PROMPT_DIALOG  = "chrome://mozapps/content/update/updates.xul";
 
 const PREF_APP_UPDATE_INTERVAL = "app.update.interval";
 const PREF_APP_UPDATE_LASTUPDATETIME = "app.update.lastUpdateTime.background-update-timer";
 
 const LOG_FUNCTION = info;
@@ -484,17 +483,17 @@ function delayedDefaultCallback() {
  * downloading for slow download mar file tests without creating it.
  *
  * @return nsILocalFile for the continue file.
  */
 function getContinueFile() {
   let continueFile = Cc["@mozilla.org/file/directory_service;1"].
                      getService(Ci.nsIProperties).
                      get("CurWorkD", Ci.nsILocalFile);
-  let continuePath = REL_PATH_DATA + "/continue";
+  let continuePath = REL_PATH_DATA + "continue";
   let continuePathParts = continuePath.split("/");
   for (let i = 0; i < continuePathParts.length; ++i) {
     continueFile.append(continuePathParts[i]);
   }
   return continueFile;
 }
 
 /**
@@ -808,16 +807,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();
@@ -901,16 +901,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
@@ -3,32 +3,37 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Shared code for xpcshell and mochitests-chrome */
 /* eslint-disable no-undef */
 
 Cu.import("resource://gre/modules/FileUtils.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-const PREF_APP_UPDATE_AUTO                 = "app.update.auto";
-const PREF_APP_UPDATE_BACKGROUNDERRORS     = "app.update.backgroundErrors";
-const PREF_APP_UPDATE_BACKGROUNDMAXERRORS  = "app.update.backgroundMaxErrors";
-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_UPDATE_LOG                  = "app.update.log";
-const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED  = "app.update.notifiedUnsupported";
-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_AUTO                       = "app.update.auto";
+const PREF_APP_UPDATE_BACKGROUNDERRORS           = "app.update.backgroundErrors";
+const PREF_APP_UPDATE_BACKGROUNDMAXERRORS        = "app.update.backgroundMaxErrors";
+const PREF_APP_UPDATE_CHANNEL                    = "app.update.channel";
+const PREF_APP_UPDATE_DOORHANGER                 = "app.update.doorhanger";
+const PREF_APP_UPDATE_DOWNLOADPROMPTATTEMPTS     = "app.update.download.attempts";
+const PREF_APP_UPDATE_DOWNLOADPROMPTMAXATTEMPTS  = "app.update.download.maxAttempts";
+const PREF_APP_UPDATE_ENABLED                    = "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_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_URL_MANUAL                 = "app.update.url.manual";
+
 
 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";
 
--- a/toolkit/mozapps/update/tests/data/sharedUpdateXML.js
+++ b/toolkit/mozapps/update/tests/data/sharedUpdateXML.js
@@ -226,18 +226,18 @@ function getLocalPatchString(aType, aURL
  *         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.
+ *         If not specified it will default to
+ *         URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS" 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
@@ -268,17 +268,17 @@ function getUpdateString(aType, aName, a
   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/") + "\" ";
+                                : URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS") + "\" ";
   let showPrompt = aShowPrompt ? "showPrompt=\"" + aShowPrompt + "\" " : "";
   let showNeverForVersion = aShowNeverForVersion ? "showNeverForVersion=\"" +
                                                    aShowNeverForVersion + "\" "
                                                  : "";
   let promptWaitTime = aPromptWaitTime ? "promptWaitTime=\"" + aPromptWaitTime +
                                          "\" "
                                        : "";
   let backgroundInterval = aBackgroundInterval ? "backgroundInterval=\"" +
--- a/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js
+++ b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js
@@ -29,16 +29,18 @@
  */
 
 "use strict";
 /* eslint-disable no-undef */
 
 const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr,
         utils: Cu } = Components;
 
+const URL_HTTP_UPDATE_SJS = "http://test_details/";
+
 /* global INSTALL_LOCALE, MOZ_APP_NAME, BIN_SUFFIX, MOZ_APP_VENDOR */
 /* global MOZ_APP_BASENAME, APP_BIN_SUFFIX, APP_INFO_NAME, APP_INFO_VENDOR */
 /* global IS_WIN, IS_MACOSX, IS_UNIX, IS_ANDROID, IS_TOOLKIT_GONK */
 /* global MOZ_VERIFY_MAR_SIGNATURE, MOZ_VERIFY_MAR_SIGNATURE, IS_AUTHENTICODE_CHECK_ENABLED */
 load("../data/xpcshellConstantsPP.js");
 
 function getLogSuffix() {
   if (IS_WIN) {
--- a/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in
+++ b/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in
@@ -1,39 +1,42 @@
 # vim:set ts=8 sw=8 sts=8 noet:
 # 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/.
 
 # For changes here, also consider ../Makefile.in
 
-XPCSHELLTESTROOT = $(topobjdir)/_tests/xpcshell/toolkit/mozapps/update/tests
-MOCHITESTROOT = $(topobjdir)/_tests/testing/mochitest/chrome/toolkit/mozapps/update/tests
+XPCSHELLTESTDIR = $(topobjdir)/_tests/xpcshell/toolkit/mozapps/update/tests
+MOCHITESTCHROMEDIR = $(topobjdir)/_tests/testing/mochitest/chrome/toolkit/mozapps/update/tests
+MOCHITESTBROWSERDIR = $(topobjdir)/_tests/testing/mochitest/browser/browser/base/content/test/appUpdate
 
 include $(topsrcdir)/config/rules.mk
 
 ifndef MOZ_WINCONSOLE
 ifdef MOZ_DEBUG
 MOZ_WINCONSOLE = 1
 else
 MOZ_WINCONSOLE = 0
 endif
 endif
 
 tools::
 ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
 	# Copy for xpcshell tests
-	$(NSINSTALL) -D $(XPCSHELLTESTROOT)/data/updater-xpcshell.app
-	rsync -a -C --exclude '*.in' $(srcdir)/../macbuild/Contents $(XPCSHELLTESTROOT)/data/updater-xpcshell.app
+	$(NSINSTALL) -D $(XPCSHELLTESTDIR)/data/updater-xpcshell.app
+	rsync -a -C --exclude '*.in' $(srcdir)/../macbuild/Contents $(XPCSHELLTESTDIR)/data/updater-xpcshell.app
 	sed -e 's/%APP_NAME%/$(MOZ_APP_DISPLAYNAME)/' $(srcdir)/../macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in | \
-	  iconv -f UTF-8 -t UTF-16 > $(XPCSHELLTESTROOT)/data/updater-xpcshell.app/Contents/Resources/English.lproj/InfoPlist.strings
-	$(NSINSTALL) -D $(XPCSHELLTESTROOT)/data/updater-xpcshell.app/Contents/MacOS
-	$(NSINSTALL) $(FINAL_TARGET)/updater-xpcshell $(XPCSHELLTESTROOT)/data/updater-xpcshell.app/Contents/MacOS
-	rm -Rf $(XPCSHELLTESTROOT)/data/updater.app
-	mv $(XPCSHELLTESTROOT)/data/updater-xpcshell.app $(XPCSHELLTESTROOT)/data/updater.app
-	mv $(XPCSHELLTESTROOT)/data/updater.app/Contents/MacOS/updater-xpcshell $(XPCSHELLTESTROOT)/data/updater.app/Contents/MacOS/org.mozilla.updater
+	  iconv -f UTF-8 -t UTF-16 > $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/Resources/English.lproj/InfoPlist.strings
+	$(NSINSTALL) -D $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/MacOS
+	$(NSINSTALL) $(FINAL_TARGET)/updater-xpcshell $(XPCSHELLTESTDIR)/data/updater-xpcshell.app/Contents/MacOS
+	rm -Rf $(XPCSHELLTESTDIR)/data/updater.app
+	mv $(XPCSHELLTESTDIR)/data/updater-xpcshell.app $(XPCSHELLTESTDIR)/data/updater.app
+	mv $(XPCSHELLTESTDIR)/data/updater.app/Contents/MacOS/updater-xpcshell $(XPCSHELLTESTDIR)/data/updater.app/Contents/MacOS/org.mozilla.updater
 
 	# Copy for mochitest chrome tests
-	rsync -a -C $(XPCSHELLTESTROOT)/data/updater.app $(MOCHITESTROOT)/data/
+	rsync -a -C $(XPCSHELLTESTDIR)/data/updater.app $(MOCHITESTCHROMEDIR)/data/
+	rsync -a -C $(XPCSHELLTESTDIR)/data/updater.app $(MOCHITESTBROWSERDIR)/
 else
-	cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(XPCSHELLTESTROOT)/data/updater$(BIN_SUFFIX)
-	cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(MOCHITESTROOT)/data/updater$(BIN_SUFFIX)
+	cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(XPCSHELLTESTDIR)/data/updater$(BIN_SUFFIX)
+	cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(MOCHITESTCHROMEDIR)/data/updater$(BIN_SUFFIX)
+	cp $(FINAL_TARGET)/updater-xpcshell$(BIN_SUFFIX) $(MOCHITESTBROWSERDIR)/updater$(BIN_SUFFIX)
 endif