Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Wed, 31 May 2017 20:26:16 -0400
changeset 409873 59fa62ee9d7537bf9dd5e75ef77a6561b396fd21
parent 409872 9bf2eafa8defa9b0d69bfe8c757ca69826543e9b (current diff)
parent 409791 a8f378825e81daff1279a7d6e940b610912ee6dc (diff)
child 409874 aea06724da78381b79b399b6ca72aaf0d49a4cac
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound. a=merge
browser/components/uitour/test/browser_UITour_heartbeat.js
browser/extensions/activity-stream/data/content/assets/glyph-search-history.svg
browser/extensions/activity-stream/lib/SearchFeed.jsm
browser/extensions/activity-stream/test/unit/lib/SearchFeed.test.js
browser/themes/shared/compacttheme.inc.css
browser/themes/shared/heartbeat-icon.svg
browser/themes/shared/heartbeat-star-lit.svg
browser/themes/shared/heartbeat-star-off.svg
browser/themes/shared/jar.inc.mn
services/crypto/tests/unit/test_crypto_deriveKey.js
services/sync/tests/unit/test_utils_deriveKey.js
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -347,20 +347,24 @@
 
     <key id="key_switchTextDirection" key="&bidiSwitchTextDirectionItem.commandkey;" command="cmd_switchTextDirection" modifiers="accel,shift" />
 
     <key id="key_privatebrowsing" command="Tools:PrivateBrowsing" key="&privateBrowsingCmd.commandkey;"
          modifiers="accel,shift" reserved="true"/>
     <key id="key_sanitize" command="Tools:Sanitize" keycode="VK_DELETE" modifiers="accel,shift"/>
 #ifdef XP_MACOSX
     <key id="key_sanitize_mac" command="Tools:Sanitize" keycode="VK_BACK" modifiers="accel,shift"/>
-    <key id="key_quitApplication" key="&quitApplicationCmdUnix.key;" modifiers="accel" reserved="true"/>
-#elifdef XP_UNIX
-    <key id="key_quitApplication" key="&quitApplicationCmdUnix.key;" command="cmd_quitApplication" modifiers="accel" reserved="true"/>
 #endif
+    <key id="key_quitApplication" key="&quitApplicationCmd.key;"
+#ifdef XP_WIN
+         modifiers="accel,shift"
+#else
+         modifiers="accel"
+#endif
+         reserved="true"/>
 
 #ifdef FULL_BROWSER_WINDOW
     <key id="key_undoCloseTab" command="History:UndoCloseTab" key="&tabCmd.commandkey;" modifiers="accel,shift"/>
 #endif
     <key id="key_undoCloseWindow" command="History:UndoCloseWindow" key="&newNavigatorCmd.key;" modifiers="accel,shift"/>
 
 #ifdef XP_GNOME
 #define NUM_SELECT_TAB_MODIFIER alt
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -33,16 +33,17 @@ subsuite = clipboard
 support-files =
   redirect_bug623155.sjs
 [browser_bug783614.js]
 [browser_canonizeURL.js]
 [browser_dragdropURL.js]
 [browser_locationBarCommand.js]
 [browser_locationBarExternalLoad.js]
 [browser_moz_action_link.js]
+[browser_new_tab_urlbar_reset.js]
 [browser_pasteAndGo.js]
 subsuite = clipboard
 [browser_removeUnsafeProtocolsFromURLBarPaste.js]
 subsuite = clipboard
 [browser_search_favicon.js]
 [browser_tabMatchesInAwesomebar.js]
 support-files =
   moz.png
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_new_tab_urlbar_reset.js
@@ -0,0 +1,16 @@
+"use strict";
+
+/**
+ * Verify that urlbar state is reset when openig a new tab, so searching for the
+ * same text will reopen the results popup.
+ */
+add_task(async function() {
+  let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank", false);
+  await promiseAutocompleteResultPopup("m");
+  ok(gURLBar.popupOpen, "The popup is open");
+  let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:blank", false);
+  await promiseAutocompleteResultPopup("m");
+  ok(gURLBar.popupOpen, "The popup is open");
+  await BrowserTestUtils.removeTab(tab);
+  await BrowserTestUtils.removeTab(tab2);
+});
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -117,16 +117,21 @@ file, You can obtain one at http://mozil
           pasteAndGo.setAttribute("label", label);
           pasteAndGo.setAttribute("anonid", "paste-and-go");
           pasteAndGo.setAttribute("oncommand",
               "gURLBar.select(); goDoCommand('cmd_paste'); gURLBar.handleCommand();");
           cxmenu.insertBefore(pasteAndGo, insertLocation.nextSibling);
         }
 
         this._enableOrDisableOneOffSearches();
+
+        // The autocomplete controller uses heuristic on some internal caches
+        // to handle cases like backspace, autofill or repeated searches.
+        // Ensure to clear those internal caches when switching tabs.
+        gBrowser.tabContainer.addEventListener("TabSelect", this);
       ]]></constructor>
 
       <destructor><![CDATA[
         this._prefs.removeObserver("", this);
         this._prefs = null;
         this.inputField.controllers.removeController(this._copyCutController);
         this.inputField.removeEventListener("paste", this);
         this.inputField.removeEventListener("mousedown", this);
@@ -1143,16 +1148,20 @@ file, You can obtain one at http://mozil
                 break;
               }
               this.setAttribute("textoverflow", "true");
               break;
             case "underflow":
               this.removeAttribute("textoverflow");
               this._hideURLTooltip();
               break;
+            case "TabSelect":
+              this.detachController();
+              this.attachController();
+              break;
           }
         ]]></body>
       </method>
 
       <!--
         onBeforeTextValueSet is called by the base-binding's .textValue getter.
         It should return the value that the getter should use.
       -->
--- a/browser/components/customizableui/content/panelUI.inc.xul
+++ b/browser/components/customizableui/content/panelUI.inc.xul
@@ -638,16 +638,29 @@
                        label="&webDeveloperMenu.label;"
                        closemenu="none"
                        oncommand="PanelUI.showSubView('PanelUI-developer', this, null, true)"/>
         <toolbarbutton id="appMenu-help-button"
                        class="subviewbutton subviewbutton-iconic subviewbutton-nav"
                        label="&appMenuHelp.label;"
                        closemenu="none"
                        oncommand="PanelUI.showSubView('PanelUI-helpView', this, null, true)"/>
+#ifndef XP_MACOSX
+        <toolbarseparator/>
+        <toolbarbutton id="appMenu-quit-button"
+                       class="subviewbutton subviewbutton-iconic"
+#ifdef XP_WIN
+                       label="&quitApplicationCmdWin2.label;"
+                       tooltiptext="&quitApplicationCmdWin2.tooltip;"
+#else
+                       label="&quitApplicationCmd.label;"
+#endif
+                       key="key_quitApplication"
+                       command="cmd_quitApplication"/>
+#endif
       </vbox>
     </panelview>
     <panelview id="appMenu-moreView" title="&moreMenu.label;" class="PanelUI-subView">
       <vbox class="panel-subview-body">
         <toolbarbutton id="appMenu-characterencoding-button"
                        class="subviewbutton subviewbutton-nav"
                        label="&charsetMenu2.label;"
                        closemenu="none"
--- a/browser/components/uitour/UITour-lib.js
+++ b/browser/components/uitour/UITour-lib.js
@@ -177,58 +177,16 @@ if (typeof Mozilla == "undefined") {
    */
   Mozilla.UITour.registerPageID = function(pageID) {
     _sendEvent("registerPageID", {
       pageID
     });
   };
 
   /**
-   * Show a global notification bar with a prompt and optional buttons.
-   *
-   * Only intended for use by Self Support.
-   *
-   * @deprecated Use Heartbeat from
-   * {@link https://wiki.mozilla.org/Firefox/Shield/Heartbeat|Shield} instead.
-   *
-   * @param {String} message - Text to show in the notification bar before an action is taken.
-   * @param {String} thankyouMessage - Text to show in the notification bar after a vote.
-   * @param {String} flowId - An identifier for this rating flow. Please note that this is only used
-   *                          to identify the notification box.
-   * @param {String} engagementURL - URL to open in a new tab once the user has engaged.
-   * @param {String} learnMoreLabel - The label of the learn more link. No link will be shown if
-   *                                  this is null.
-   * @param {String} learnMoreURL - URL to open when clicking on the learn more link. No link will be
-   *                                shown if this is an invalid URL.
-   * @param {Object} options - Options to control behavior.
-   */
-  Mozilla.UITour.showHeartbeat = function(message, thankyouMessage, flowId, engagementURL,
-					  learnMoreLabel, learnMoreURL, options) {
-    var args = {
-      message,
-      thankyouMessage,
-      flowId,
-      engagementURL,
-      learnMoreLabel,
-      learnMoreURL,
-    };
-
-    if (options) {
-      for (var option in options) {
-	if (!options.hasOwnProperty(option)) {
-	  continue;
-	}
-	args[option] = options[option];
-      }
-    }
-
-    _sendEvent("showHeartbeat", args);
-  };
-
-  /**
    * @typedef {String} Mozilla.UITour.HighlightEffect
    *
    * Specifies the effect/animation to use when highlighting UI elements.
    * @description Valid values:<ul>
    * <li>random
    * <li>wobble
    * <li>zoom
    * <li>color
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -8,17 +8,16 @@ this.EXPORTED_SYMBOLS = ["UITour"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
-Cu.import("resource:///modules/RecentWindow.jsm");
 Cu.import("resource://gre/modules/TelemetryController.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 
 Cu.importGlobalProperties(["URL"]);
 
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
   "resource://gre/modules/LightweightThemeManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ResetProfile",
@@ -32,17 +31,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ReaderParent",
   "resource:///modules/ReaderParent.jsm");
 
 // See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
 const PREF_LOG_LEVEL      = "browser.uitour.loglevel";
 const PREF_SEENPAGEIDS    = "browser.uitour.seenPageIDs";
-const PREF_SURVEY_DURATION = "browser.uitour.surveyDuration";
 
 const BACKGROUND_PAGE_ACTIONS_ALLOWED = new Set([
   "forceShowReaderIcon",
   "getConfiguration",
   "getTreatmentTag",
   "hideHighlight",
   "hideInfo",
   "hideMenu",
@@ -356,52 +354,16 @@ this.UITour = {
 
         this.addSeenPageID(data.pageID);
         this.pageIDSourceBrowsers.set(browser, data.pageID);
         this.setTelemetryBucket(data.pageID);
 
         break;
       }
 
-      case "showHeartbeat": {
-        // Validate the input parameters.
-        if (typeof data.message !== "string" || data.message === "") {
-          log.error("showHeartbeat: Invalid message specified.");
-          return false;
-        }
-
-        if (typeof data.thankyouMessage !== "string" || data.thankyouMessage === "") {
-          log.error("showHeartbeat: Invalid thank you message specified.");
-          return false;
-        }
-
-        if (typeof data.flowId !== "string" || data.flowId === "") {
-          log.error("showHeartbeat: Invalid flowId specified.");
-          return false;
-        }
-
-        if (data.engagementButtonLabel && typeof data.engagementButtonLabel != "string") {
-          log.error("showHeartbeat: Invalid engagementButtonLabel specified");
-          return false;
-        }
-
-        let heartbeatWindow = window;
-        if (data.privateWindowsOnly && !PrivateBrowsingUtils.isWindowPrivate(heartbeatWindow)) {
-          heartbeatWindow = RecentWindow.getMostRecentBrowserWindow({ private: true });
-          if (!heartbeatWindow) {
-            log.debug("showHeartbeat: No private window found");
-            return false;
-          }
-        }
-
-        // Finally show the Heartbeat UI.
-        this.showHeartbeat(heartbeatWindow, data);
-        break;
-      }
-
       case "showHighlight": {
         let targetPromise = this.getTarget(window, data.target);
         targetPromise.then(target => {
           if (!target.node) {
             log.error("UITour: Target could not be resolved: " + data.target);
             return;
           }
           let effect = undefined;
@@ -1037,347 +999,16 @@ this.UITour = {
       LightweightThemeManager.previewTheme(data);
   },
 
   resetTheme() {
     LightweightThemeManager.resetPreview();
   },
 
   /**
-   * Show the Heartbeat UI to request user feedback. This function reports back to the
-   * caller using |notify|. The notification event name reflects the current status the UI
-   * is in (either "Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed",
-   * "Heartbeat:LearnMore", "Heartbeat:Engaged", "Heartbeat:Voted",
-   * "Heartbeat:SurveyExpired" or "Heartbeat:WindowClosed").
-   * When a "Heartbeat:Voted" event is notified
-   * the data payload contains a |score| field which holds the rating picked by the user.
-   * Please note that input parameters are already validated by the caller.
-   *
-   * @param aChromeWindow
-   *        The chrome window that the heartbeat notification is displayed in.
-   * @param {Object} aOptions Options object.
-   * @param {String} aOptions.message
-   *        The message, or question, to display on the notification.
-   * @param {String} aOptions.thankyouMessage
-   *        The thank you message to display after user votes.
-   * @param {String} aOptions.flowId
-   *        An identifier for this rating flow. Please note that this is only used to
-   *        identify the notification box.
-   * @param {String} [aOptions.engagementButtonLabel=null]
-   *        The text of the engagement button to use instad of stars. If this is null
-   *        or invalid, rating stars are used.
-   * @param {String} [aOptions.engagementURL=null]
-   *        The engagement URL to open in a new tab once user has engaged. If this is null
-   *        or invalid, no new tab is opened.
-   * @param {String} [aOptions.learnMoreLabel=null]
-   *        The label of the learn more link. No link will be shown if this is null.
-   * @param {String} [aOptions.learnMoreURL=null]
-   *        The learn more URL to open when clicking on the learn more link. No learn more
-   *        will be shown if this is an invalid URL.
-   * @param {boolean} [aOptions.privateWindowsOnly=false]
-   *        Whether the heartbeat UI should only be targeted at a private window (if one exists).
-   *        No notifications should be fired when this is true.
-   * @param {String} [aOptions.surveyId]
-   *        An ID for the survey, reflected in the Telemetry ping.
-   * @param {Number} [aOptions.surveyVersion]
-   *        Survey's version number, reflected in the Telemetry ping.
-   * @param {boolean} [aOptions.testing]
-   *        Whether this is a test survey, reflected in the Telemetry ping.
-   */
-  showHeartbeat(aChromeWindow, aOptions) {
-    // Initialize survey state
-    let pingSent = false;
-    let surveyResults = {};
-    let surveyEndTimer = null;
-
-    /**
-     * Accumulates survey events and submits to Telemetry after the survey ends.
-     *
-     * @param {String} aEventName
-     *        Heartbeat event name
-     * @param {Object} aParams
-     *        Additional parameters and their values
-     */
-    let maybeNotifyHeartbeat = (aEventName, aParams = {}) => {
-      // Return if event occurred after the ping was sent
-      if (pingSent) {
-        log.warn("maybeNotifyHeartbeat: event occurred after ping sent:", aEventName, aParams);
-        return;
-      }
-
-      // No Telemetry from private-window-only Heartbeats
-      if (aOptions.privateWindowsOnly) {
-        return;
-      }
-
-      let ts = Date.now();
-      let sendPing = false;
-      switch (aEventName) {
-        case "Heartbeat:NotificationOffered":
-          surveyResults.flowId = aOptions.flowId;
-          surveyResults.offeredTS = ts;
-          break;
-        case "Heartbeat:LearnMore":
-          // record only the first click
-          if (!surveyResults.learnMoreTS) {
-            surveyResults.learnMoreTS = ts;
-          }
-          break;
-        case "Heartbeat:Engaged":
-          surveyResults.engagedTS = ts;
-          break;
-        case "Heartbeat:Voted":
-          surveyResults.votedTS = ts;
-          surveyResults.score = aParams.score;
-          break;
-        case "Heartbeat:SurveyExpired":
-          surveyResults.expiredTS = ts;
-          break;
-        case "Heartbeat:NotificationClosed":
-          // this is the final event in most surveys
-          surveyResults.closedTS = ts;
-          sendPing = true;
-          break;
-        case "Heartbeat:WindowClosed":
-          surveyResults.windowClosedTS = ts;
-          sendPing = true;
-          break;
-        default:
-          log.error("maybeNotifyHeartbeat: unrecognized event:", aEventName);
-          break;
-      }
-
-      aParams.timestamp = ts;
-      aParams.flowId = aOptions.flowId;
-      this.notify(aEventName, aParams);
-
-      if (!sendPing) {
-        return;
-      }
-
-      // Send the ping to Telemetry
-      let payload = Object.assign({}, surveyResults);
-      payload.version = 1;
-      for (let meta of ["surveyId", "surveyVersion", "testing"]) {
-        if (aOptions.hasOwnProperty(meta)) {
-          payload[meta] = aOptions[meta];
-        }
-      }
-
-      log.debug("Sending payload to Telemetry: aEventName:", aEventName,
-                "payload:", payload);
-
-      TelemetryController.submitExternalPing("heartbeat", payload, {
-        addClientId: true,
-        addEnvironment: true,
-      });
-
-      // only for testing
-      this.notify("Heartbeat:TelemetrySent", payload);
-
-      // Survey is complete, clear out the expiry timer & survey configuration
-      if (surveyEndTimer) {
-        clearTimeout(surveyEndTimer);
-        surveyEndTimer = null;
-      }
-
-      pingSent = true;
-      surveyResults = {};
-    };
-
-    let nb = aChromeWindow.document.getElementById("high-priority-global-notificationbox");
-    let buttons = null;
-
-    if (aOptions.engagementButtonLabel) {
-      buttons = [{
-        label: aOptions.engagementButtonLabel,
-        callback: () => {
-          // Let the consumer know user engaged.
-          maybeNotifyHeartbeat("Heartbeat:Engaged");
-
-          userEngaged(new Map([
-            ["type", "button"],
-            ["flowid", aOptions.flowId]
-          ]));
-
-          // Return true so that the notification bar doesn't close itself since
-          // we have a thank you message to show.
-          return true;
-        },
-      }];
-    }
-
-    let defaultIcon = "chrome://browser/skin/heartbeat-icon.svg";
-    let iconURL = defaultIcon;
-    try {
-      // Take the optional icon URL if specified
-      if (aOptions.iconURL) {
-        iconURL = new URL(aOptions.iconURL);
-        // For now, only allow chrome URIs.
-        if (iconURL.protocol != "chrome:") {
-          iconURL = defaultIcon;
-          throw new Error("Invalid protocol");
-        }
-      }
-    } catch (error) {
-      log.error("showHeartbeat: Invalid icon URL specified.");
-    }
-
-    // Create the notification. Prefix its ID to decrease the chances of collisions.
-    let notice = nb.appendNotification(aOptions.message, "heartbeat-" + aOptions.flowId,
-                                       iconURL,
-                                       nb.PRIORITY_INFO_HIGH, buttons,
-                                       (aEventType) => {
-                                         if (aEventType != "removed") {
-                                           return;
-                                         }
-                                         // Let the consumer know the notification bar was closed.
-                                         // This also happens after voting.
-                                         maybeNotifyHeartbeat("Heartbeat:NotificationClosed");
-                                       });
-
-    // Get the elements we need to style.
-    let messageImage =
-      aChromeWindow.document.getAnonymousElementByAttribute(notice, "anonid", "messageImage");
-    let messageText =
-      aChromeWindow.document.getAnonymousElementByAttribute(notice, "anonid", "messageText");
-
-    function userEngaged(aEngagementParams) {
-      // Make the heartbeat icon pulse twice.
-      notice.label = aOptions.thankyouMessage;
-      messageImage.classList.remove("pulse-onshow");
-      messageImage.classList.add("pulse-twice");
-
-      // Remove all the children of the notice (rating container
-      // and the flex).
-      while (notice.firstChild) {
-        notice.firstChild.remove();
-      }
-
-      // Make sure that we have a valid URL. If we haven't, do not open the engagement page.
-      let engagementURL = null;
-      try {
-        engagementURL = new URL(aOptions.engagementURL);
-      } catch (error) {
-        log.error("showHeartbeat: Invalid URL specified.");
-      }
-
-      // Just open the engagement tab if we have a valid engagement URL.
-      if (engagementURL) {
-        for (let [param, value] of aEngagementParams) {
-          engagementURL.searchParams.append(param, value);
-        }
-
-        // Open the engagement URL in a new tab.
-        aChromeWindow.gBrowser.selectedTab =
-          aChromeWindow.gBrowser.addTab(engagementURL.toString(), {
-            owner: aChromeWindow.gBrowser.selectedTab,
-            relatedToCurrent: true
-          });
-      }
-
-      // Remove the notification bar after 3 seconds.
-      aChromeWindow.setTimeout(() => {
-        nb.removeNotification(notice);
-      }, 3000);
-    }
-
-    // Create the fragment holding the rating UI.
-    let frag = aChromeWindow.document.createDocumentFragment();
-
-    // Build the Heartbeat star rating.
-    const numStars = aOptions.engagementButtonLabel ? 0 : 5;
-    let ratingContainer = aChromeWindow.document.createElement("hbox");
-    ratingContainer.id = "star-rating-container";
-
-    for (let i = 0; i < numStars; i++) {
-      // Create a star rating element.
-      let ratingElement = aChromeWindow.document.createElement("toolbarbutton");
-
-      // Style it.
-      let starIndex = numStars - i;
-      ratingElement.className = "plain star-x";
-      ratingElement.id = "star" + starIndex;
-      ratingElement.setAttribute("data-score", starIndex);
-
-      // Add the click handler.
-      ratingElement.addEventListener("click", function(evt) {
-        let rating = Number(evt.target.getAttribute("data-score"), 10);
-
-        // Let the consumer know user voted.
-        maybeNotifyHeartbeat("Heartbeat:Voted", { score: rating });
-
-        // Append the score data to the engagement URL.
-        userEngaged(new Map([
-          ["type", "stars"],
-          ["score", rating],
-          ["flowid", aOptions.flowId]
-        ]));
-      });
-
-      // Add it to the container.
-      ratingContainer.appendChild(ratingElement);
-    }
-
-    frag.appendChild(ratingContainer);
-
-    // Make sure the stars are not pushed to the right by the spacer.
-    let rightSpacer = aChromeWindow.document.createElement("spacer");
-    rightSpacer.flex = 20;
-    frag.appendChild(rightSpacer);
-
-    messageText.flex = 0; // Collapse the space before the stars.
-    let leftSpacer = messageText.nextSibling;
-    leftSpacer.flex = 0;
-
-    // Make sure that we have a valid learn more URL.
-    let learnMoreURL = null;
-    try {
-      learnMoreURL = new URL(aOptions.learnMoreURL);
-    } catch (error) {
-      log.error("showHeartbeat: Invalid learnMore URL specified.");
-    }
-
-    // Add the learn more link.
-    if (aOptions.learnMoreLabel && learnMoreURL) {
-      let learnMore = aChromeWindow.document.createElement("label");
-      learnMore.className = "text-link";
-      learnMore.href = learnMoreURL.toString();
-      learnMore.setAttribute("value", aOptions.learnMoreLabel);
-      learnMore.addEventListener("click", () => maybeNotifyHeartbeat("Heartbeat:LearnMore"));
-      frag.appendChild(learnMore);
-    }
-
-    // Append the fragment and apply the styling.
-    notice.appendChild(frag);
-    notice.classList.add("heartbeat");
-    messageImage.classList.add("heartbeat", "pulse-onshow");
-    messageText.classList.add("heartbeat");
-
-    // Let the consumer know the notification was shown.
-    maybeNotifyHeartbeat("Heartbeat:NotificationOffered");
-
-    // End the survey if the user quits, closes the window, or
-    // hasn't responded before expiration.
-    if (!aOptions.privateWindowsOnly) {
-      function handleWindowClosed(aTopic) {
-        maybeNotifyHeartbeat("Heartbeat:WindowClosed");
-        aChromeWindow.removeEventListener("SSWindowClosing", handleWindowClosed);
-      }
-      aChromeWindow.addEventListener("SSWindowClosing", handleWindowClosed);
-
-      let surveyDuration = Services.prefs.getIntPref(PREF_SURVEY_DURATION) * 1000;
-      surveyEndTimer = setTimeout(() => {
-        maybeNotifyHeartbeat("Heartbeat:SurveyExpired");
-        nb.removeNotification(notice);
-      }, surveyDuration);
-    }
-  },
-
-  /**
    * The node to which a highlight or notification(-popup) is anchored is sometimes
    * obscured because it may be inside an overflow menu. This function should figure
    * that out and offer the overflow chevron as an alternative.
    *
    * @param {Node} aAnchor The element that's supposed to be the anchor
    * @type {Node}
    */
   _correctAnchor(aAnchor) {
--- a/browser/components/uitour/test/browser.ini
+++ b/browser/components/uitour/test/browser.ini
@@ -28,18 +28,16 @@ skip-if = os == "linux" # Intermittent f
 [browser_UITour2.js]
 [browser_UITour3.js]
 skip-if = os == "linux" # Linux: Bug 986760, Bug 989101.
 [browser_UITour_availableTargets.js]
 [browser_UITour_annotation_size_attributes.js]
 [browser_UITour_defaultBrowser.js]
 [browser_UITour_detach_tab.js]
 [browser_UITour_forceReaderMode.js]
-[browser_UITour_heartbeat.js]
-skip-if = os == "win" # Bug 1277107
 [browser_UITour_modalDialog.js]
 skip-if = os != "mac" # modal dialog disabling only working on OS X.
 [browser_UITour_observe.js]
 [browser_UITour_panel_close_annotation.js]
 skip-if = true # Disabled due to frequent failures, bugs 1026310 and 1032137
 [browser_UITour_pocket.js]
 skip-if = true # Disabled pending removal of pocket UI Tour
 [browser_UITour_registerPageID.js]
deleted file mode 100644
--- a/browser/components/uitour/test/browser_UITour_heartbeat.js
+++ /dev/null
@@ -1,753 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-"use strict";
-
-var gTestTab;
-var gContentAPI;
-var gContentWindow;
-
-function getHeartbeatNotification(aId, aChromeWindow = window) {
-  let notificationBox = aChromeWindow.document.getElementById("high-priority-global-notificationbox");
-  // UITour.jsm prefixes the notification box ID with "heartbeat-" to prevent collisions.
-  return notificationBox.getNotificationWithValue("heartbeat-" + aId);
-}
-
-/**
- * Simulate a click on a rating element in the Heartbeat notification.
- *
- * @param aId
- *        The id of the notification box.
- * @param aScore
- *        The score related to the rating element we want to click on.
- */
-function simulateVote(aId, aScore) {
-  let notification = getHeartbeatNotification(aId);
-
-  let ratingContainer = notification.childNodes[0];
-  ok(ratingContainer, "The notification has a valid rating container.");
-
-  let ratingElement = ratingContainer.getElementsByAttribute("data-score", aScore);
-  ok(ratingElement[0], "The rating container contains the requested rating element.");
-
-  ratingElement[0].click();
-}
-
-/**
- * Simulate a click on the learn-more link.
- *
- * @param aId
- *        The id of the notification box.
- */
-function clickLearnMore(aId) {
-  let notification = getHeartbeatNotification(aId);
-
-  let learnMoreLabel = notification.childNodes[2];
-  ok(learnMoreLabel, "The notification has a valid learn more label.");
-
-  learnMoreLabel.click();
-}
-
-/**
- * Remove the notification box.
- *
- * @param aId
- *        The id of the notification box to remove.
- * @param [aChromeWindow=window]
- *        The chrome window the notification box is in.
- */
-function cleanUpNotification(aId, aChromeWindow = window) {
-  let notification = getHeartbeatNotification(aId, aChromeWindow);
-  notification.close();
-}
-
-/**
- * Check telemetry payload for proper format and expected content.
- *
- * @param aPayload
- *        The Telemetry payload to verify
- * @param aFlowId
- *        Expected value of the flowId field.
- * @param aExpectedFields
- *        Array of expected fields. No other fields are allowed.
- */
-function checkTelemetry(aPayload, aFlowId, aExpectedFields) {
-  // Basic payload format
-  is(aPayload.version, 1, "Telemetry ping must have heartbeat version=1");
-  is(aPayload.flowId, aFlowId, "Flow ID in the Telemetry ping must match");
-
-  // Check for superfluous fields
-  let extraKeys = new Set(Object.keys(aPayload));
-  extraKeys.delete("version");
-  extraKeys.delete("flowId");
-
-  // Check for expected fields
-  for (let field of aExpectedFields) {
-    ok(field in aPayload, "The payload should have the field '" + field + "'");
-    if (field.endsWith("TS")) {
-      let ts = aPayload[field];
-      ok(Number.isInteger(ts) && ts > 0, "Timestamp '" + field + "' must be a natural number");
-    }
-    extraKeys.delete(field);
-  }
-
-  is(extraKeys.size, 0, "No unexpected fields in the Telemetry payload");
-}
-
-/**
- * Waits for an UITour notification dispatched through |UITour.notify|. This should be
- * done with |gContentAPI.observe|. Unfortunately, in e10s, |gContentAPI.observe| doesn't
- * allow for multiple calls to the same callback, allowing to catch just the first
- * notification.
- *
- * @param aEventName
- *        The notification name to wait for.
- * @return {Promise} Resolved with the data that comes with the event.
- */
-function promiseWaitHeartbeatNotification(aEventName) {
-  return ContentTask.spawn(gTestTab.linkedBrowser, aEventName, (aContentEventName) => {
-        return new Promise(resolve => {
-          addEventListener("mozUITourNotification", function listener(event) {
-            if (event.detail.event !== aContentEventName) {
-              return;
-            }
-            removeEventListener("mozUITourNotification", listener, false);
-            resolve(event.detail.params);
-          }, false);
-        });
-      });
-}
-
-/**
- * Waits for UITour notifications dispatched through |UITour.notify|. This works like
- * |promiseWaitHeartbeatNotification|, but waits for all the passed notifications to
- * be received before resolving. If it receives an unaccounted notification, it rejects.
- *
- * @param events
- *        An array of expected notification names to wait for.
- * @return {Promise} Resolved with the data that comes with the event. Rejects with the
- *         name of an undesired notification if received.
- */
-function promiseWaitExpectedNotifications(events) {
-  return ContentTask.spawn(gTestTab.linkedBrowser, events, contentEvents => {
-        let stillToReceive = contentEvents;
-        return new Promise((res, rej) => {
-          addEventListener("mozUITourNotification", function listener(event) {
-            if (stillToReceive.includes(event.detail.event)) {
-              // Filter out the received event.
-              stillToReceive = stillToReceive.filter(x => x !== event.detail.event);
-            } else {
-              removeEventListener("mozUITourNotification", listener, false);
-              rej(event.detail.event);
-            }
-            // We still need to catch some notifications. Don't do anything.
-            if (stillToReceive.length > 0) {
-              return;
-            }
-            // We don't need to listen for other notifications. Resolve the promise.
-            removeEventListener("mozUITourNotification", listener, false);
-            res();
-          }, false);
-        });
-      });
-}
-
-function validateTimestamp(eventName, timestamp) {
-  info("'" + eventName + "' notification received (timestamp " + timestamp.toString() + ").");
-  ok(Number.isFinite(timestamp), "Timestamp must be a number.");
-}
-
-add_task(async function test_setup() {
-  await setup_UITourTest();
-  requestLongerTimeout(2);
-  registerCleanupFunction(() => {
-    Services.prefs.clearUserPref("browser.uitour.surveyDuration");
-  });
-});
-
-/**
- * Check that the "stars" heartbeat UI correctly shows and closes.
- */
-add_UITour_task(async function test_heartbeat_stars_show() {
-  let flowId = "ui-ratefirefox-" + Math.random();
-  let engagementURL = "http://example.com";
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(
-    ["Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
-
-  // Validate the returned timestamp.
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Close the heartbeat notification.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-  cleanUpNotification(flowId);
-
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received");
-  checkTelemetry(data, flowId, ["offeredTS", "closedTS"]);
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Check that the heartbeat UI correctly takes optional icon URL.
- */
-add_UITour_task(async function test_heartbeat_take_optional_icon_URL() {
-  let flowId = "ui-ratefirefox-" + Math.random();
-  let engagementURL = "http://example.com";
-  let iconURL = "chrome://branding/content/icon48.png";
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(
-    ["Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL, null, null, {
-    iconURL
-  });
-
-  // Validate the returned timestamp.
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Check the icon URL
-  let notification = getHeartbeatNotification(flowId);
-  is(notification.image, iconURL, "The optional icon URL is not taken correctly");
-
-  // Close the heartbeat notification.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-  cleanUpNotification(flowId);
-
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received");
-  checkTelemetry(data, flowId, ["offeredTS", "closedTS"]);
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Test that the heartbeat UI correctly works with null engagement URL.
- */
-add_UITour_task(async function test_heartbeat_null_engagementURL() {
-  let flowId = "ui-ratefirefox-" + Math.random();
-  let originalTabCount = gBrowser.tabs.length;
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:Voted", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, null);
-
-  // Validate the returned timestamp.
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Wait an the Voted, Closed and Telemetry Sent events. They are fired together, so
-  // wait for them here.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let votedPromise = promiseWaitHeartbeatNotification("Heartbeat:Voted");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
-  simulateVote(flowId, 2);
-  data = await votedPromise;
-  validateTimestamp("Heartbeat:Voted", data.timestamp);
-
-  // Validate the closing timestamp.
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-  is(gBrowser.tabs.length, originalTabCount, "No engagement tab should be opened.");
-
-  // Validate the data we send out.
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
-  is(data.score, 2, "Checking Telemetry payload.score");
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Test that the heartbeat UI correctly works with an invalid, but non null, engagement URL.
- */
-add_UITour_task(async function test_heartbeat_invalid_engagement_URL() {
-  let flowId = "ui-ratefirefox-" + Math.random();
-  let originalTabCount = gBrowser.tabs.length;
-  let invalidEngagementURL = "invalidEngagement";
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:Voted", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, invalidEngagementURL);
-
-  // Validate the returned timestamp.
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Wait an the Voted, Closed and Telemetry Sent events. They are fired together, so
-  // wait for them here.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let votedPromise = promiseWaitHeartbeatNotification("Heartbeat:Voted");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
-  simulateVote(flowId, 2);
-  data = await votedPromise;
-  validateTimestamp("Heartbeat:Voted", data.timestamp);
-
-  // Validate the closing timestamp.
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-  is(gBrowser.tabs.length, originalTabCount, "No engagement tab should be opened.");
-
-  // Validate the data we send out.
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
-  is(data.score, 2, "Checking Telemetry payload.score");
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Test that the score is correctly reported.
- */
-add_UITour_task(async function test_heartbeat_stars_vote() {
-  const expectedScore = 4;
-  let originalTabCount = gBrowser.tabs.length;
-  let flowId = "ui-ratefirefox-" + Math.random();
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:Voted", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, null);
-
-  // Validate the returned timestamp.
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Wait an the Voted, Closed and Telemetry Sent events. They are fired together, so
-  // wait for them here.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let votedPromise = promiseWaitHeartbeatNotification("Heartbeat:Voted");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
-  simulateVote(flowId, expectedScore);
-  data = await votedPromise;
-  validateTimestamp("Heartbeat:Voted", data.timestamp);
-  is(data.score, expectedScore, "Should report a score of " + expectedScore);
-
-  // Validate the closing timestamp and vote.
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-  is(gBrowser.tabs.length, originalTabCount, "No engagement tab should be opened.");
-
-  // Validate the data we send out.
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
-  is(data.score, expectedScore, "Checking Telemetry payload.score");
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Test that the engagement page is correctly opened when voting.
- */
-add_UITour_task(async function test_heartbeat_engagement_tab() {
-  let engagementURL = "http://example.com";
-  let flowId = "ui-ratefirefox-" + Math.random();
-  let originalTabCount = gBrowser.tabs.length;
-  const expectedTabCount = originalTabCount + 1;
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:Voted", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
-
-  // Validate the returned timestamp.
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Wait an the Voted, Closed and Telemetry Sent events. They are fired together, so
-  // wait for them here.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let votedPromise = promiseWaitHeartbeatNotification("Heartbeat:Voted");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  // The UI was just shown. We can simulate a click on a rating element (i.e., "star").
-  simulateVote(flowId, 1);
-  data = await votedPromise;
-  validateTimestamp("Heartbeat:Voted", data.timestamp);
-
-  // Validate the closing timestamp, vote and make sure the engagement page was opened.
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-  is(gBrowser.tabs.length, expectedTabCount, "Engagement URL should open in a new tab.");
-  gBrowser.removeCurrentTab();
-
-  // Validate the data we send out.
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "votedTS", "closedTS", "score"]);
-  is(data.score, 1, "Checking Telemetry payload.score");
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Test that the engagement button opens the engagement URL.
- */
-add_UITour_task(async function test_heartbeat_engagement_button() {
-  let engagementURL = "http://example.com";
-  let flowId = "ui-engagewithfirefox-" + Math.random();
-  let originalTabCount = gBrowser.tabs.length;
-  const expectedTabCount = originalTabCount + 1;
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:Engaged", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL, null, null, {
-    engagementButtonLabel: "Engage Me",
-  });
-
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Wait an the Engaged, Closed and Telemetry Sent events. They are fired together, so
-  // wait for them here.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let engagedPromise = promiseWaitHeartbeatNotification("Heartbeat:Engaged");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  // Simulate user engagement.
-  let notification = getHeartbeatNotification(flowId);
-  is(notification.querySelectorAll(".star-x").length, 0, "No stars should be present");
-  // The UI was just shown. We can simulate a click on the engagement button.
-  let engagementButton = notification.querySelector(".notification-button");
-  is(engagementButton.label, "Engage Me", "Check engagement button text");
-  engagementButton.doCommand();
-
-  data = await engagedPromise;
-  validateTimestamp("Heartbeat:Engaged", data.timestamp);
-
-  // Validate the closing timestamp, vote and make sure the engagement page was opened.
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-  is(gBrowser.tabs.length, expectedTabCount, "Engagement URL should open in a new tab.");
-  gBrowser.removeCurrentTab();
-
-  // Validate the data we send out.
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "engagedTS", "closedTS"]);
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Test that the learn more link is displayed and that the page is correctly opened when
- * clicking on it.
- */
-add_UITour_task(async function test_heartbeat_learnmore() {
-  let dummyURL = "http://example.com";
-  let flowId = "ui-ratefirefox-" + Math.random();
-  let originalTabCount = gBrowser.tabs.length;
-  const expectedTabCount = originalTabCount + 1;
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:LearnMore", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, dummyURL,
-                            "What is this?", dummyURL);
-
-  let data = await shownPromise;
-  validateTimestamp("Heartbeat:Offered", data.timestamp);
-
-  // Wait an the LearnMore, Closed and Telemetry Sent events. They are fired together, so
-  // wait for them here.
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let learnMorePromise = promiseWaitHeartbeatNotification("Heartbeat:LearnMore");
-  let pingSentPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  // The UI was just shown. Simulate a click on the learn more link.
-  clickLearnMore(flowId);
-
-  data = await learnMorePromise;
-  validateTimestamp("Heartbeat:LearnMore", data.timestamp);
-  cleanUpNotification(flowId);
-
-  // The notification was closed.
-  data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-  is(gBrowser.tabs.length, expectedTabCount, "Learn more URL should open in a new tab.");
-  gBrowser.removeCurrentTab();
-
-  // Validate the data we send out.
-  data = await pingSentPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "learnMoreTS", "closedTS"]);
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-add_UITour_task(async function test_invalidEngagementButtonLabel() {
-  let engagementURL = "http://example.com";
-  let flowId = "invalidEngagementButtonLabel-" + Math.random();
-
-  let eventPromise = promisePageEvent();
-
-  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
-                            null, null, {
-                              engagementButtonLabel: 42,
-                            });
-
-  await eventPromise;
-  ok(!isTourBrowser(gBrowser.selectedBrowser),
-     "Invalid engagementButtonLabel should prevent init");
-
-})
-
-add_UITour_task(async function test_privateWindowsOnly_noneOpen() {
-  let engagementURL = "http://example.com";
-  let flowId = "privateWindowsOnly_noneOpen-" + Math.random();
-
-  let eventPromise = promisePageEvent();
-
-  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
-                            null, null, {
-                              engagementButtonLabel: "Yes!",
-                              privateWindowsOnly: true,
-                            });
-
-  await eventPromise;
-  ok(!isTourBrowser(gBrowser.selectedBrowser),
-     "If there are no private windows opened, tour init should be prevented");
-})
-
-add_UITour_task(async function test_privateWindowsOnly_notMostRecent() {
-  let engagementURL = "http://example.com";
-  let flowId = "notMostRecent-" + Math.random();
-
-  let privateWin = await BrowserTestUtils.openNewBrowserWindow({ private: true });
-  let mostRecentWin = await BrowserTestUtils.openNewBrowserWindow();
-
-  let eventPromise = promisePageEvent();
-
-  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
-                            null, null, {
-                              engagementButtonLabel: "Yes!",
-                              privateWindowsOnly: true,
-                            });
-
-  await eventPromise;
-  is(getHeartbeatNotification(flowId, window), null,
-     "Heartbeat shouldn't appear in the default window");
-  is(!!getHeartbeatNotification(flowId, privateWin), true,
-     "Heartbeat should appear in the most recent private window");
-  is(getHeartbeatNotification(flowId, mostRecentWin), null,
-     "Heartbeat shouldn't appear in the most recent non-private window");
-
-  await BrowserTestUtils.closeWindow(mostRecentWin);
-  await BrowserTestUtils.closeWindow(privateWin);
-})
-
-add_UITour_task(async function test_privateWindowsOnly() {
-  let engagementURL = "http://example.com";
-  let learnMoreURL = "http://example.org/learnmore/";
-  let flowId = "ui-privateWindowsOnly-" + Math.random();
-
-  let privateWin = await BrowserTestUtils.openNewBrowserWindow({ private: true });
-
-  await new Promise((resolve) => {
-    gContentAPI.observe(function(aEventName, aData) {
-      info(aEventName + " notification received: " + JSON.stringify(aData, null, 2));
-      ok(false, "No heartbeat notifications should arrive for privateWindowsOnly");
-    }, resolve);
-  });
-
-  gContentAPI.showHeartbeat("Do you want to engage with us?", "Thank you!", flowId, engagementURL,
-                            "Learn More", learnMoreURL, {
-                              engagementButtonLabel: "Yes!",
-                              privateWindowsOnly: true,
-                            });
-
-  await promisePageEvent();
-
-  ok(isTourBrowser(gBrowser.selectedBrowser), "UITour should have been init for the browser");
-
-  let notification = getHeartbeatNotification(flowId, privateWin);
-
-  is(notification.querySelectorAll(".star-x").length, 0, "No stars should be present");
-
-  info("Test the learn more link.");
-  let learnMoreLink = notification.querySelector(".text-link");
-  is(learnMoreLink.value, "Learn More", "Check learn more label");
-  let learnMoreTabPromise = BrowserTestUtils.waitForNewTab(privateWin.gBrowser, null);
-  learnMoreLink.click();
-  let learnMoreTab = await learnMoreTabPromise;
-  is(learnMoreTab.linkedBrowser.currentURI.host, "example.org", "Check learn more site opened");
-  ok(PrivateBrowsingUtils.isBrowserPrivate(learnMoreTab.linkedBrowser), "Ensure the learn more tab is private");
-  await BrowserTestUtils.removeTab(learnMoreTab);
-
-  info("Test the engagement button's new tab.");
-  let engagementButton = notification.querySelector(".notification-button");
-  is(engagementButton.label, "Yes!", "Check engagement button text");
-  let engagementTabPromise = BrowserTestUtils.waitForNewTab(privateWin.gBrowser, null);
-  engagementButton.doCommand();
-  let engagementTab = await engagementTabPromise;
-  is(engagementTab.linkedBrowser.currentURI.host, "example.com", "Check enagement site opened");
-  ok(PrivateBrowsingUtils.isBrowserPrivate(engagementTab.linkedBrowser), "Ensure the engagement tab is private");
-  await BrowserTestUtils.removeTab(engagementTab);
-
-  await BrowserTestUtils.closeWindow(privateWin);
-})
-
-/**
- * Test that the survey closes itself after a while and submits Telemetry
- */
-add_UITour_task(async function test_telemetry_surveyExpired() {
-  let flowId = "survey-expired-" + Math.random();
-  let engagementURL = "http://example.com";
-  let surveyDuration = 1; // 1 second (pref is in seconds)
-  Services.prefs.setIntPref("browser.uitour.surveyDuration", surveyDuration);
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(["Heartbeat:NotificationOffered",
-    "Heartbeat:NotificationClosed", "Heartbeat:SurveyExpired", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!", flowId, engagementURL);
-
-  let expiredPromise = promiseWaitHeartbeatNotification("Heartbeat:SurveyExpired");
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let pingPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-
-  await Promise.all([shownPromise, expiredPromise, closedPromise]);
-  // Validate the ping data.
-  let data = await pingPromise;
-  checkTelemetry(data, flowId, ["offeredTS", "expiredTS", "closedTS"]);
-
-  Services.prefs.clearUserPref("browser.uitour.surveyDuration");
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
-
-/**
- * Check that certain whitelisted experiment parameters get reflected in the
- * Telemetry ping
- */
-add_UITour_task(async function test_telemetry_params() {
-  let flowId = "telemetry-params-" + Math.random();
-  let engagementURL = "http://example.com";
-  let extraParams = {
-    "surveyId": "foo",
-    "surveyVersion": 1.5,
-    "testing": true,
-    "notWhitelisted": 123,
-  };
-  let expectedFields = ["surveyId", "surveyVersion", "testing"];
-
-  // We need to call |gContentAPI.observe| at least once to set a valid |notificationListener|
-  // in UITour-lib.js, otherwise no message will get propagated.
-  gContentAPI.observe(() => {});
-
-  let receivedExpectedPromise = promiseWaitExpectedNotifications(
-    ["Heartbeat:NotificationOffered", "Heartbeat:NotificationClosed", "Heartbeat:TelemetrySent"]);
-
-  // Show the Heartbeat notification and wait for it to be displayed.
-  let shownPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationOffered");
-  gContentAPI.showHeartbeat("How would you rate Firefox?", "Thank you!",
-                            flowId, engagementURL, null, null, extraParams);
-  await shownPromise;
-
-  let closedPromise = promiseWaitHeartbeatNotification("Heartbeat:NotificationClosed");
-  let pingPromise = promiseWaitHeartbeatNotification("Heartbeat:TelemetrySent");
-  cleanUpNotification(flowId);
-
-  // The notification was closed.
-  let data = await closedPromise;
-  validateTimestamp("Heartbeat:NotificationClosed", data.timestamp);
-
-  // Validate the data we send out.
-  data = await pingPromise;
-  info("'Heartbeat:TelemetrySent' notification received.");
-  checkTelemetry(data, flowId, ["offeredTS", "closedTS"].concat(expectedFields));
-  for (let param of expectedFields) {
-    is(data[param], extraParams[param],
-       "Whitelisted experiment configs should be copied into Telemetry pings");
-  }
-
-  // This rejects whenever an unexpected notification is received.
-  await receivedExpectedPromise;
-})
--- a/browser/extensions/activity-stream/README.md
+++ b/browser/extensions/activity-stream/README.md
@@ -1,8 +1,10 @@
 # activity-stream
 
 This system add-on replaces the new tab page in Firefox with a new design and
 functionality as part of the Activity Stream project. It can be enabled (or disabled)
 via the browser.newtabpage.activity-stream.enabled pref.
 
 The files in this directory, including vendor dependencies, are imported from the
 system-addon directory in https://github.com/mozilla/activity-stream.
+
+Read [docs/v2-system-addon](https://github.com/mozilla/activity-stream/tree/master/docs/v2-system-addon) for more detail.
--- a/browser/extensions/activity-stream/bootstrap.js
+++ b/browser/extensions/activity-stream/bootstrap.js
@@ -1,32 +1,57 @@
 /* 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/. */
-/* globals Components, XPCOMUtils, Preferences, Services, ActivityStream */
 "use strict";
 
-const {utils: Cu} = Components;
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+Cu.importGlobalProperties(["fetch"]);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/Preferences.jsm");
-Cu.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
+  "resource://gre/modules/Preferences.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "ActivityStream",
-  "resource://activity-stream/lib/ActivityStream.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+  "resource://gre/modules/Services.jsm");
 
+const ACTIVITY_STREAM_ENABLED_PREF = "browser.newtabpage.activity-stream.enabled";
 const BROWSER_READY_NOTIFICATION = "browser-ui-startup-complete";
-const ACTIVITY_STREAM_ENABLED_PREF = "browser.newtabpage.activity-stream.enabled";
+const REASON_SHUTDOWN_ON_PREF_CHANGE = "PREF_OFF";
 const REASON_STARTUP_ON_PREF_CHANGE = "PREF_ON";
-const REASON_SHUTDOWN_ON_PREF_CHANGE = "PREF_OFF";
+const RESOURCE_BASE = "resource://activity-stream";
 
 const ACTIVITY_STREAM_OPTIONS = {newTabURL: "about:newtab"};
 
 let activityStream;
+let modulesToUnload = new Set();
 let startupData;
 let startupReason;
+let waitingForBrowserReady = true;
+
+// Lazily load ActivityStream then find related modules to unload
+XPCOMUtils.defineLazyModuleGetter(this, "ActivityStream",
+  "resource://activity-stream/lib/ActivityStream.jsm", null, null, () => {
+    // Helper to fetch a resource directory listing and call back with each item
+    const processListing = async(uri, cb) => (await (await fetch(uri)).text())
+      .split("\n").slice(2).forEach(line => cb(line.split(" ").slice(1)));
+
+    // Look for modules one level deeper than the top resource URI
+    processListing(RESOURCE_BASE, ([directory, , , type]) => {
+      if (type === "DIRECTORY") {
+        // Look into this directory for .jsm files
+        const subDir = `${RESOURCE_BASE}/${directory}`;
+        processListing(subDir, ([name]) => {
+          if (name && name.search(/\.jsm$/) !== -1) {
+            modulesToUnload.add(`${subDir}/${name}`);
+          }
+        });
+      }
+    });
+  });
 
 /**
  * init - Initializes an instance of ActivityStream. This could be called by
  *        the startup() function exposed by bootstrap.js, or it could be called
  *        when ACTIVITY_STREAM_ENABLED_PREF is changed from false to true.
  *
  * @param  {string} reason - Reason for initialization. Could be install, upgrade, or PREF_ON
  */
@@ -45,64 +70,94 @@ function init(reason) {
  *          called by the shutdown() function exposed by bootstrap.js, or it could
  *          be called when ACTIVITY_STREAM_ENABLED_PREF is changed from true to false.
  *
  * @param  {type} reason Reason for uninitialization. Could be uninstall, upgrade, or PREF_OFF
  */
 function uninit(reason) {
   if (activityStream) {
     activityStream.uninit(reason);
-    activityStream = null;
   }
 }
 
 /**
  * onPrefChanged - handler for changes to ACTIVITY_STREAM_ENABLED_PREF
  *
  * @param  {bool} isEnabled Determines whether Activity Stream is enabled
  */
 function onPrefChanged(isEnabled) {
   if (isEnabled) {
     init(REASON_STARTUP_ON_PREF_CHANGE);
   } else {
     uninit(REASON_SHUTDOWN_ON_PREF_CHANGE);
   }
 }
 
+/**
+ * onBrowserReady - Continues startup of the add-on after browser is ready.
+ */
+function onBrowserReady() {
+  waitingForBrowserReady = false;
+
+  // Listen for changes to the pref that enables Activity Stream
+  Preferences.observe(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
+
+  // Only initialize if the pref is true
+  if (Preferences.get(ACTIVITY_STREAM_ENABLED_PREF)) {
+    init(startupReason);
+  }
+}
+
+/**
+ * observe - nsIObserver callback to handle various browser notifications.
+ */
 function observe(subject, topic, data) {
   switch (topic) {
     case BROWSER_READY_NOTIFICATION:
-      // Listen for changes to the pref that enables Activity Stream
-      Preferences.observe(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
-      // Only initialize if the pref is true
-      if (Preferences.get(ACTIVITY_STREAM_ENABLED_PREF)) {
-        init(startupReason);
-        Services.obs.removeObserver(this, BROWSER_READY_NOTIFICATION);
-      }
+      Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION);
+      onBrowserReady();
       break;
   }
 }
 
 // The functions below are required by bootstrap.js
 
 this.install = function install(data, reason) {};
 
 this.startup = function startup(data, reason) {
-  // Only start Activity Stream up when the browser UI is ready
-  Services.obs.addObserver(observe, BROWSER_READY_NOTIFICATION);
-
   // Cache startup data which contains stuff like the version number, etc.
   // so we can use it when we init
   startupData = data;
   startupReason = reason;
+
+  // Only start Activity Stream up when the browser UI is ready
+  if (Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup).startingUp) {
+    Services.obs.addObserver(observe, BROWSER_READY_NOTIFICATION);
+  } else {
+    // Handle manual install or automatic install after manual uninstall
+    onBrowserReady();
+  }
 };
 
 this.shutdown = function shutdown(data, reason) {
   // Uninitialize Activity Stream
   startupData = null;
   startupReason = null;
   uninit(reason);
 
-  // Stop listening to the pref that enables Activity Stream
-  Preferences.ignore(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
+  // Stop waiting for browser to be ready
+  if (waitingForBrowserReady) {
+    Services.obs.removeObserver(observe, BROWSER_READY_NOTIFICATION);
+  } else {
+    // Stop listening to the pref that enables Activity Stream
+    Preferences.ignore(ACTIVITY_STREAM_ENABLED_PREF, onPrefChanged);
+  }
+
+  // Unload any add-on modules that might might have been imported
+  modulesToUnload.forEach(Cu.unload);
 };
 
-this.uninstall = function uninstall(data, reason) {};
+this.uninstall = function uninstall(data, reason) {
+  if (activityStream) {
+    activityStream.uninstall(reason);
+    activityStream = null;
+  }
+};
--- a/browser/extensions/activity-stream/common/Actions.jsm
+++ b/browser/extensions/activity-stream/common/Actions.jsm
@@ -25,25 +25,23 @@ const actionTypes = [
   "INIT",
   "LOCALE_UPDATED",
   "NEW_TAB_INITIAL_STATE",
   "NEW_TAB_LOAD",
   "NEW_TAB_UNLOAD",
   "NEW_TAB_VISIBLE",
   "OPEN_NEW_WINDOW",
   "OPEN_PRIVATE_WINDOW",
-  "PERFORM_SEARCH",
   "PLACES_BOOKMARK_ADDED",
   "PLACES_BOOKMARK_CHANGED",
   "PLACES_BOOKMARK_REMOVED",
   "PLACES_HISTORY_CLEARED",
   "PLACES_LINK_BLOCKED",
   "PLACES_LINK_DELETED",
   "SCREENSHOT_UPDATED",
-  "SEARCH_STATE_UPDATED",
   "TELEMETRY_PERFORMANCE_EVENT",
   "TELEMETRY_UNDESIRED_EVENT",
   "TELEMETRY_USER_EVENT",
   "TOP_SITES_UPDATED",
   "UNINIT"
 // The line below creates an object like this:
 // {
 //   INIT: "INIT",
--- a/browser/extensions/activity-stream/common/Reducers.jsm
+++ b/browser/extensions/activity-stream/common/Reducers.jsm
@@ -16,25 +16,16 @@ const INITIAL_STATE = {
     // The version of the system-addon
     version: null
   },
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
     rows: []
-  },
-  Search: {
-    // The search engine currently set by the browser
-    currentEngine: {
-      name: "",
-      icon: ""
-    },
-    // All possible search engines
-    engines: []
   }
 };
 
 function App(prevState = INITIAL_STATE.App, action) {
   switch (action.type) {
     case at.INIT:
       return Object.assign({}, action.data || {}, {initialized: true});
     case at.LOCALE_UPDATED: {
@@ -95,28 +86,12 @@ function TopSites(prevState = INITIAL_ST
     case at.PLACES_LINK_BLOCKED:
       newRows = prevState.rows.filter(val => val.url !== action.data.url);
       return Object.assign({}, prevState, {rows: newRows});
     default:
       return prevState;
   }
 }
 
-function Search(prevState = INITIAL_STATE.Search, action) {
-  switch (action.type) {
-    case at.SEARCH_STATE_UPDATED: {
-      if (!action.data) {
-        return prevState;
-      }
-      let {currentEngine, engines} = action.data;
-      return Object.assign({}, prevState, {
-        currentEngine,
-        engines
-      });
-    }
-    default:
-      return prevState;
-  }
-}
 this.INITIAL_STATE = INITIAL_STATE;
-this.reducers = {TopSites, App, Search};
+this.reducers = {TopSites, App};
 
 this.EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE"];
--- a/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
+++ b/browser/extensions/activity-stream/data/content/activity-stream.bundle.js
@@ -92,17 +92,17 @@ var BACKGROUND_PROCESS = 2;
  *                       Use this in action creators if you need different logic
  *                       for ui/background processes.
  */
 
 const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS : UI_CODE;
 // Export for tests
 
 
-const actionTypes = ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "INIT", "LOCALE_UPDATED", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "NEW_TAB_VISIBLE", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PERFORM_SEARCH", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "SCREENSHOT_UPDATED", "SEARCH_STATE_UPDATED", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_UPDATED", "UNINIT"
+const actionTypes = ["BLOCK_URL", "BOOKMARK_URL", "DELETE_BOOKMARK_BY_ID", "DELETE_HISTORY_URL", "INIT", "LOCALE_UPDATED", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_UNLOAD", "NEW_TAB_VISIBLE", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_CHANGED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "SCREENSHOT_UPDATED", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_UPDATED", "UNINIT"
 // The line below creates an object like this:
 // {
 //   INIT: "INIT",
 //   UNINIT: "UNINIT"
 // }
 // It prevents accidentally adding a different key/value name.
 ].reduce((obj, type) => {
   obj[type] = type;return obj;
@@ -435,17 +435,17 @@ module.exports = class DetectUserSession
 
 /***/ }),
 /* 6 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
 
-/* globals sendAsyncMessage, addMessageListener */
+/* eslint-env mozilla/frame-script */
 
 var _require = __webpack_require__(14);
 
 const createStore = _require.createStore,
       combineReducers = _require.combineReducers,
       applyMiddleware = _require.applyMiddleware;
 
 var _require2 = __webpack_require__(1);
@@ -539,25 +539,16 @@ const INITIAL_STATE = {
     // The version of the system-addon
     version: null
   },
   TopSites: {
     // Have we received real data from history yet?
     initialized: false,
     // The history (and possibly default) links
     rows: []
-  },
-  Search: {
-    // The search engine currently set by the browser
-    currentEngine: {
-      name: "",
-      icon: ""
-    },
-    // All possible search engines
-    engines: []
   }
 };
 
 function App() {
   let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.App;
   let action = arguments[1];
 
   switch (action.type) {
@@ -632,40 +623,17 @@ function TopSites() {
     case at.PLACES_LINK_BLOCKED:
       newRows = prevState.rows.filter(val => val.url !== action.data.url);
       return Object.assign({}, prevState, { rows: newRows });
     default:
       return prevState;
   }
 }
 
-function Search() {
-  let prevState = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : INITIAL_STATE.Search;
-  let action = arguments[1];
-
-  switch (action.type) {
-    case at.SEARCH_STATE_UPDATED:
-      {
-        if (!action.data) {
-          return prevState;
-        }
-        var _action$data3 = action.data;
-        let currentEngine = _action$data3.currentEngine,
-            engines = _action$data3.engines;
-
-        return Object.assign({}, prevState, {
-          currentEngine,
-          engines
-        });
-      }
-    default:
-      return prevState;
-  }
-}
-var reducers = { TopSites, App, Search };
+var reducers = { TopSites, App };
 module.exports = {
   reducers,
   INITIAL_STATE
 };
 
 /***/ }),
 /* 8 */
 /***/ (function(module, exports) {
@@ -768,79 +736,95 @@ var _require = __webpack_require__(3);
 
 const injectIntl = _require.injectIntl;
 
 const ContextMenu = __webpack_require__(9);
 
 var _require2 = __webpack_require__(1);
 
 const actionTypes = _require2.actionTypes,
-      actionCreators = _require2.actionCreators;
+      ac = _require2.actionCreators;
 
 
 class LinkMenu extends React.Component {
   getBookmarkStatus(site) {
     return site.bookmarkGuid ? {
       id: "menu_action_remove_bookmark",
       icon: "bookmark-remove",
       action: "DELETE_BOOKMARK_BY_ID",
-      data: site.bookmarkGuid
+      data: site.bookmarkGuid,
+      userEvent: "BOOKMARK_DELETE"
     } : {
       id: "menu_action_bookmark",
       icon: "bookmark",
       action: "BOOKMARK_URL",
-      data: site.url
+      data: site.url,
+      userEvent: "BOOKMARK_ADD"
     };
   }
   getDefaultContextMenu(site) {
     return [{
       id: "menu_action_open_new_window",
       icon: "new-window",
       action: "OPEN_NEW_WINDOW",
-      data: { url: site.url }
+      data: { url: site.url },
+      userEvent: "OPEN_NEW_WINDOW"
     }, {
       id: "menu_action_open_private_window",
       icon: "new-window-private",
       action: "OPEN_PRIVATE_WINDOW",
-      data: { url: site.url }
+      data: { url: site.url },
+      userEvent: "OPEN_PRIVATE_WINDOW"
     }];
   }
   getOptions() {
     var _props = this.props;
     const dispatch = _props.dispatch,
-          site = _props.site;
+          site = _props.site,
+          index = _props.index,
+          source = _props.source;
 
     // default top sites have a limited set of context menu options
 
     let options = this.getDefaultContextMenu(site);
 
     // all other top sites have all the following context menu options
     if (!site.isDefault) {
       options = [this.getBookmarkStatus(site), { type: "separator" }, ...options, { type: "separator" }, {
         id: "menu_action_dismiss",
         icon: "dismiss",
         action: "BLOCK_URL",
-        data: site.url
+        data: site.url,
+        userEvent: "BLOCK"
       }, {
         id: "menu_action_delete",
         icon: "delete",
         action: "DELETE_HISTORY_URL",
-        data: site.url
+        data: site.url,
+        userEvent: "DELETE"
       }];
     }
     options.forEach(option => {
-      let action = option.action,
-          data = option.data,
-          id = option.id,
-          type = option.type;
+      const action = option.action,
+            data = option.data,
+            id = option.id,
+            type = option.type,
+            userEvent = option.userEvent;
       // Convert message ids to localized labels and add onClick function
 
       if (!type && id) {
         option.label = this.props.intl.formatMessage(option);
-        option.onClick = () => dispatch(actionCreators.SendToMain({ type: actionTypes[action], data }));
+        option.onClick = () => {
+          dispatch(ac.SendToMain({ type: actionTypes[action], data }));
+          dispatch(ac.UserEvent({
+            event: userEvent,
+            source,
+            action_position: index
+          }));
+        };
       }
     });
 
     // this is for a11y - we want to know which item is the first and which item
     // is the last, so we can close the context menu accordingly
     options[0].first = true;
     options[options.length - 1].last = true;
     return options;
@@ -856,104 +840,98 @@ class LinkMenu extends React.Component {
 module.exports = injectIntl(LinkMenu);
 module.exports._unconnected = LinkMenu;
 
 /***/ }),
 /* 11 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
+/* globals ContentSearchUIController */
 
 
 const React = __webpack_require__(0);
 
 var _require = __webpack_require__(2);
 
 const connect = _require.connect;
 
 var _require2 = __webpack_require__(3);
 
 const FormattedMessage = _require2.FormattedMessage,
       injectIntl = _require2.injectIntl;
 
 var _require3 = __webpack_require__(1);
 
-const actionTypes = _require3.actionTypes,
-      actionCreators = _require3.actionCreators;
+const ac = _require3.actionCreators;
 
 
 class Search extends React.Component {
   constructor(props) {
     super(props);
-    this.state = { searchString: "" };
     this.onClick = this.onClick.bind(this);
-    this.onChange = this.onChange.bind(this);
-  }
-
-  componentWillMount() {
-    // Trigger initialization of ContentSearch in case it hasn't happened yet
-    dispatchEvent(new CustomEvent("ContentSearchClient", { detail: {} }));
+    this.onInputMount = this.onInputMount.bind(this);
   }
 
-  performSearch(options) {
-    let searchData = {
-      engineName: options.engineName,
-      searchString: options.searchString,
-      searchPurpose: "newtab",
-      healthReportKey: "newtab"
-    };
-    this.props.dispatch(actionCreators.SendToMain({ type: actionTypes.PERFORM_SEARCH, data: searchData }));
+  handleEvent(event) {
+    // Also track search events with our own telemetry
+    if (event.detail.type === "Search") {
+      this.props.dispatch(ac.UserEvent({ event: "SEARCH" }));
+    }
   }
   onClick(event) {
-    const currentEngine = this.props.Search.currentEngine;
+    this.controller.search(event);
+  }
+  onInputMount(input) {
+    if (input) {
+      this.controller = new ContentSearchUIController(input, input.parentNode, "newtab", "activity");
+      addEventListener("ContentSearchClient", this);
+    } else {
+      this.controller = null;
+      removeEventListener("ContentSearchClient", this);
+    }
+  }
 
-    event.preventDefault();
-    this.performSearch({ engineName: currentEngine.name, searchString: this.state.searchString });
-  }
-  onChange(event) {
-    this.setState({ searchString: event.target.value });
-  }
   render() {
     return React.createElement(
       "form",
       { className: "search-wrapper" },
       React.createElement(
         "label",
         { htmlFor: "search-input", className: "search-label" },
         React.createElement(
           "span",
           { className: "sr-only" },
           React.createElement(FormattedMessage, { id: "search_web_placeholder" })
         )
       ),
       React.createElement("input", {
         id: "search-input",
         maxLength: "256",
-        onChange: this.onChange,
         placeholder: this.props.intl.formatMessage({ id: "search_web_placeholder" }),
+        ref: this.onInputMount,
         title: this.props.intl.formatMessage({ id: "search_web_placeholder" }),
-        type: "search",
-        value: this.state.searchString }),
+        type: "search" }),
       React.createElement(
         "button",
         {
           className: "search-button",
           onClick: this.onClick,
           title: this.props.intl.formatMessage({ id: "search_button" }) },
         React.createElement(
           "span",
           { className: "sr-only" },
           React.createElement(FormattedMessage, { id: "search_button" })
         )
       )
     );
   }
 }
 
-module.exports = connect(state => ({ Search: state.Search }))(injectIntl(Search));
+module.exports = connect()(injectIntl(Search));
 module.exports._unconnected = Search;
 
 /***/ }),
 /* 12 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
 
@@ -966,41 +944,54 @@ const connect = _require.connect;
 
 var _require2 = __webpack_require__(3);
 
 const FormattedMessage = _require2.FormattedMessage;
 
 const shortURL = __webpack_require__(13);
 const LinkMenu = __webpack_require__(10);
 
+var _require3 = __webpack_require__(1);
+
+const ac = _require3.actionCreators;
+
+const TOP_SITES_SOURCE = "TOP_SITES";
+
 class TopSite extends React.Component {
   constructor(props) {
     super(props);
     this.state = { showContextMenu: false, activeTile: null };
   }
   toggleContextMenu(event, index) {
     this.setState({ showContextMenu: true, activeTile: index });
   }
+  trackClick() {
+    this.props.dispatch(ac.UserEvent({
+      event: "CLICK",
+      source: TOP_SITES_SOURCE,
+      action_position: this.props.index
+    }));
+  }
   render() {
     var _props = this.props;
     const link = _props.link,
           index = _props.index,
           dispatch = _props.dispatch;
 
     const isContextMenuOpen = this.state.showContextMenu && this.state.activeTile === index;
     const title = shortURL(link);
     const screenshotClassName = `screenshot${link.screenshot ? " active" : ""}`;
     const topSiteOuterClassName = `top-site-outer${isContextMenuOpen ? " active" : ""}`;
     const style = { backgroundImage: link.screenshot ? `url(${link.screenshot})` : "none" };
     return React.createElement(
       "li",
       { className: topSiteOuterClassName, key: link.url },
       React.createElement(
         "a",
-        { href: link.url },
+        { onClick: () => this.trackClick(), href: link.url },
         React.createElement(
           "div",
           { className: "tile", "aria-hidden": true },
           React.createElement(
             "span",
             { className: "letter-fallback" },
             title[0]
           ),
@@ -1025,33 +1016,38 @@ class TopSite extends React.Component {
           `Open context menu for ${title}`
         )
       ),
       React.createElement(LinkMenu, {
         dispatch: dispatch,
         visible: isContextMenuOpen,
         onUpdate: val => this.setState({ showContextMenu: val }),
         site: link,
-        index: index })
+        index: index,
+        source: TOP_SITES_SOURCE })
     );
   }
 }
 
 const TopSites = props => React.createElement(
   "section",
   null,
   React.createElement(
     "h3",
     { className: "section-title" },
     React.createElement(FormattedMessage, { id: "header_top_sites" })
   ),
   React.createElement(
     "ul",
     { className: "top-sites-list" },
-    props.TopSites.rows.map((link, index) => React.createElement(TopSite, { dispatch: props.dispatch, key: link.url, link: link, index: index }))
+    props.TopSites.rows.map((link, index) => React.createElement(TopSite, {
+      key: link.url,
+      dispatch: props.dispatch,
+      link: link,
+      index: index }))
   )
 );
 
 module.exports = connect(state => ({ TopSites: state.TopSites }))(TopSites);
 module.exports._unconnected = TopSites;
 module.exports.TopSite = TopSite;
 
 /***/ }),
--- a/browser/extensions/activity-stream/data/content/activity-stream.css
+++ b/browser/extensions/activity-stream/data/content/activity-stream.css
@@ -238,159 +238,75 @@ main {
 
 .search-wrapper {
   cursor: default;
   display: flex;
   position: relative;
   margin: 0 0 48px;
   width: 100%;
   height: 36px; }
-  .search-wrapper .search-container {
-    z-index: 1001;
-    background: #FFF;
-    position: absolute;
-    left: 0;
-    right: 0;
-    top: 100%;
-    margin-top: -2px;
-    border: 1px solid #BFBFBF;
-    font-size: 12px;
-    box-shadow: 0 5px 10px rgba(0, 0, 0, 0.1);
-    overflow: hidden; }
-    .search-wrapper .search-container .search-title {
-      color: #666;
-      padding: 5px 10px;
-      background-color: #F7F7F7;
-      display: flex;
-      align-items: center;
-      word-break: break-all; }
-      .search-wrapper .search-container .search-title p {
-        margin: 0; }
-      .search-wrapper .search-container .search-title #current-engine-icon {
-        margin-inline-end: 8px; }
-    .search-wrapper .search-container section {
-      border-bottom: 1px solid #EAEAEA;
-      margin-bottom: 0; }
-    .search-wrapper .search-container .search-suggestions ul {
-      padding: 0;
-      margin: 0;
-      list-style: none; }
-      .search-wrapper .search-container .search-suggestions ul li a {
-        cursor: default;
-        color: #000;
-        display: block;
-        padding: 4px 36px; }
-        .search-wrapper .search-container .search-suggestions ul li a:hover, .search-wrapper .search-container .search-suggestions ul li a.active {
-          background: #0996F8;
-          color: #FFF; }
-    .search-wrapper .search-container .history-search-suggestions {
-      border-bottom: 0; }
-      .search-wrapper .search-container .history-search-suggestions ul {
-        padding: 0;
-        margin: 0;
-        list-style: none; }
-        .search-wrapper .search-container .history-search-suggestions ul li a {
-          cursor: default;
-          color: #000;
-          display: block;
-          padding: 4px 10px; }
-          .search-wrapper .search-container .history-search-suggestions ul li a:hover, .search-wrapper .search-container .history-search-suggestions ul li a.active {
-            background: #0996F8;
-            color: #FFF; }
-          .search-wrapper .search-container .history-search-suggestions ul li a:hover > #historyIcon,
-          .search-wrapper .search-container .history-search-suggestions ul li a.active > #historyIcon {
-            background-image: url("assets/glyph-search-history.svg#search-history-active"); }
-    .search-wrapper .search-container .history-search-suggestions #historyIcon {
-      width: 16px;
-      height: 16px;
-      display: inline-block;
-      margin-inline-end: 10px;
-      margin-bottom: -3px;
-      background-image: url("assets/glyph-search-history.svg#search-history"); }
-    .search-wrapper .search-container .search-partners ul {
-      padding: 0;
-      margin: 0;
-      list-style: none; }
-      .search-wrapper .search-container .search-partners ul li {
-        display: inline-block;
-        padding: 5px 0; }
-        .search-wrapper .search-container .search-partners ul li a {
-          display: block;
-          padding: 3px 16px;
-          border-right: 1px solid #BFBFBF; }
-        .search-wrapper .search-container .search-partners ul li:hover, .search-wrapper .search-container .search-partners ul li.active {
-          background: #0996F8;
-          color: #FFF; }
-          .search-wrapper .search-container .search-partners ul li:hover a, .search-wrapper .search-container .search-partners ul li.active a {
-            border-color: transparent; }
-    .search-wrapper .search-container .search-settings button {
-      color: #666;
-      margin: 0;
-      padding: 0;
-      height: 32px;
-      text-align: center;
-      width: 100%;
-      border-style: solid none none;
-      border-radius: 0;
-      background: #F7F7F7;
-      border-top: 0; }
-      .search-wrapper .search-container .search-settings button:hover, .search-wrapper .search-container .search-settings button.active {
-        background: #EBEBEB;
-        box-shadow: none; }
   .search-wrapper input {
     border: 0;
     box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
     flex-grow: 1;
     margin: 0;
     outline: none;
     padding: 0 12px 0 35px;
     height: 100%;
-    border-top-left-radius: 4px;
-    border-bottom-left-radius: 4px;
+    border-radius: 4px 0 0 4px;
     padding-inline-start: 35px; }
     .search-wrapper input:focus {
       border-color: #0996F8;
       box-shadow: 0 0 0 2px #0996F8;
       z-index: 1; }
-    .search-wrapper input:focus + button {
+    .search-wrapper input:focus + .search-button {
       z-index: 1;
       box-shadow: 0 0 0 2px #0996F8;
       background-color: #0996F8;
       background-image: url("assets/glyph-forward-16-white.svg");
       color: #FFF; }
-  .search-wrapper input:dir(rtl) {
-    border-radius: 0 4px 4px 0; }
+    .search-wrapper input[aria-expanded="true"] {
+      border-radius: 4px 0 0 0; }
+    .search-wrapper input:dir(rtl) {
+      border-radius: 0 4px 4px 0; }
+      .search-wrapper input:dir(rtl)[aria-expanded="true"] {
+        border-radius: 0 4px 0 0; }
   .search-wrapper .search-label {
     background: url("assets/glyph-search-16.svg") no-repeat center center/20px;
     position: absolute;
     top: 0;
     offset-inline-start: 0;
     height: 100%;
     width: 35px;
     display: flex;
     align-items: center;
     justify-content: center;
     z-index: 2; }
-  .search-wrapper button {
+  .search-wrapper .search-button {
     border-radius: 0 3px 3px 0;
     margin-inline-start: -1px;
     border: 0;
     width: 36px;
     padding: 0;
     box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1);
     background: #FFF url("assets/glyph-forward-16.svg") no-repeat center center;
     background-size: 16px 16px; }
-    .search-wrapper button:hover {
+    .search-wrapper .search-button:hover {
       z-index: 1;
       box-shadow: 0 1px 0 0 rgba(0, 0, 1, 0.5);
       background-color: #0996F8;
       background-image: url("assets/glyph-forward-16-white.svg");
-      color: #FFF; }
-  .search-wrapper button:dir(rtl) {
-    transform: scaleX(-1); }
+      color: #FFF;
+      cursor: pointer; }
+    .search-wrapper .search-button:dir(rtl) {
+      transform: scaleX(-1); }
+  .search-wrapper .contentSearchSuggestionTable {
+    transform: translate(-2px, 2px); }
+    .search-wrapper .contentSearchSuggestionTable:dir(rtl) {
+      transform: translate(2px, 2px); }
 
 .context-menu {
   display: block;
   position: absolute;
   font-size: 14px;
   box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(0, 0, 0, 0.2);
   top: 6.75px;
   offset-inline-start: 100%;
--- a/browser/extensions/activity-stream/data/content/activity-stream.html
+++ b/browser/extensions/activity-stream/data/content/activity-stream.html
@@ -1,17 +1,19 @@
 <!doctype html>
 <html lang="en-us" dir="ltr">
   <head>
     <meta charset="utf-8">
     <title></title>
+    <link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
     <link rel="stylesheet" href="resource://activity-stream/data/content/activity-stream.css" />
   </head>
   <body class="activity-stream">
     <div id="root"></div>
+    <script src="chrome://browser/content/contentSearchUI.js"></script>
     <script src="resource://activity-stream/vendor/react.js"></script>
     <script src="resource://activity-stream/vendor/react-dom.js"></script>
     <script src="resource://activity-stream/vendor/react-intl.js"></script>
     <script src="resource://activity-stream/vendor/redux.js"></script>
     <script src="resource://activity-stream/vendor/react-redux.js"></script>
     <script src="resource://activity-stream/data/content/activity-stream.bundle.js"></script>
   </body>
 </html>
deleted file mode 100644
--- a/browser/extensions/activity-stream/data/content/assets/glyph-search-history.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0"?>
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
-  <style>
-    use:not(:target) {
-      display: none;
-    }
-    use {
-      fill: graytext;
-    }
-    use[id$="-active"] {
-      fill: HighlightText;
-    }
-  </style>
-  <defs>
-    <path id="search-history-glyph" d="M8,1C4.1,1,1,4.1,1,8c0,3.9,3.1,7,7,7c3.9,0,7-3.1,7-7 C15,4.1,11.9,1,8,1z M8,13.3c-2.9,0-5.3-2.4-5.3-5.3S5.1,2.7,8,2.7c2.9,0,5.3,2.4,5.3,5.3S10.9,13.3,8,13.3z M10.5,7H9V5 c0-0.6-0.4-1-1-1S7,4.4,7,5v3c0,0.6,0.4,1,1,1h2.5c0.6,0,1-0.4,1-1C11.5,7.4,11.1,7,10.5,7z"/>
-  </defs>
-  <use id="search-history" xlink:href="#search-history-glyph"/>
-  <use id="search-history-active" xlink:href="#search-history-glyph"/>
-</svg>
--- a/browser/extensions/activity-stream/data/locales.json
+++ b/browser/extensions/activity-stream/data/locales.json
@@ -125,26 +125,46 @@
     "menu_action_remove_bookmark": "Əlfəcini sil",
     "menu_action_copy_address": "Ünvanı köçür",
     "menu_action_email_link": "Keçidi e-poçt ilə göndər…",
     "menu_action_open_new_window": "Yeni Pəncərədə Aç",
     "menu_action_open_private_window": "Yeni Məxfi Pəncərədə Aç",
     "menu_action_dismiss": "Rədd et",
     "menu_action_delete": "Tarixçədən Sil",
     "search_for_something_with": "{search_term} üçün bununla axtar:",
+    "search_button": "Axtar",
     "search_header": "{search_engine_name} Axtarış",
     "search_web_placeholder": "İnternetdə Axtar",
     "search_settings": "Axtarış Tənzimləmələrini Dəyiş",
     "welcome_title": "Yeni vərəqə xoş gəldiniz",
     "welcome_body": "Firefox bu səhifədə ən uyğun əlfəcin, məqalə, video və son ziyarət etdiyiniz səhifələri göstərərək onları rahat tapmağınıza kömək edəcək.",
     "welcome_label": "Seçilmişləriniz təyin edilir",
     "time_label_less_than_minute": "<1d",
     "time_label_minute": "{number}d",
     "time_label_hour": "{number}s",
-    "time_label_day": "{number}g"
+    "time_label_day": "{number}g",
+    "settings_pane_button_label": "Yeni Vərəq səhifənizi özəlləşdirin",
+    "settings_pane_header": "Yeni Vərəq Nizamlamaları",
+    "settings_pane_body": "Yeni vərəq açdığınızda nə görəcəyinizi seçin.",
+    "settings_pane_search_header": "Axtar",
+    "settings_pane_search_body": "Yeni vərəqinizdən Web-də axtarış edin.",
+    "settings_pane_topsites_header": "Qabaqcıl Saytlar",
+    "settings_pane_topsites_body": "Ən çox ziyarət etdiyiniz saytları görün.",
+    "settings_pane_topsites_options_showmore": "İki sətir göstər",
+    "settings_pane_highlights_header": "Seçilmişlər",
+    "settings_pane_highlights_body": "Son gəzmə tarixçəniz və yeni yaradılar əlfəcinlərinizə göz gəzdirin.",
+    "settings_pane_done_button": "Oldu",
+    "edit_topsites_button_text": "Redaktə et",
+    "edit_topsites_button_label": "Qabaqcıl Saytlar bölümünüzü fərdiləşdirin",
+    "edit_topsites_showmore_button": "Daha çox göstər",
+    "edit_topsites_showless_button": "Daha az göstər",
+    "edit_topsites_done_button": "Oldu",
+    "edit_topsites_pin_button": "Bu saytı sabitlə",
+    "edit_topsites_edit_button": "Bu saytı düzəlt",
+    "edit_topsites_dismiss_button": "Bu saytı çıxart"
   },
   "be": {
     "newtab_page_title": "Новая картка",
     "default_label_loading": "Загрузка…",
     "header_top_sites": "Папулярныя сайты",
     "header_highlights": "Выбранае",
     "type_label_visited": "Наведанае",
     "type_label_bookmarked": "У закладках",
@@ -370,17 +390,66 @@
     "edit_topsites_button_label": "Upravit oddíl Top stránek",
     "edit_topsites_showmore_button": "Zobrazit více",
     "edit_topsites_showless_button": "Zobrazit méně",
     "edit_topsites_done_button": "Hotovo",
     "edit_topsites_pin_button": "Připnout tuto stránku",
     "edit_topsites_edit_button": "Upravit tuto stránku",
     "edit_topsites_dismiss_button": "Skrýt tuto stránku"
   },
-  "cy": {},
+  "cy": {
+    "newtab_page_title": "Tab Newydd",
+    "default_label_loading": "Llwytho…",
+    "header_top_sites": "Hoff Wefannau",
+    "header_highlights": "Goreuon",
+    "type_label_visited": "Ymwelwyd",
+    "type_label_bookmarked": "Nod Tudalen",
+    "type_label_synced": "Cydweddwyd o ddyfais arall",
+    "type_label_open": "Ar Agor",
+    "type_label_topic": "Pwnc",
+    "menu_action_bookmark": "Nod Tudalen",
+    "menu_action_remove_bookmark": "Tynnu Nod Tudalen",
+    "menu_action_copy_address": "Copïo'r Cyfeiriad",
+    "menu_action_email_link": "Dolen E-bost…",
+    "menu_action_open_new_window": "Agor Ffenestr Newydd",
+    "menu_action_open_private_window": "Agor mewn Ffenestr Preifat Newydd",
+    "menu_action_dismiss": "Cau",
+    "menu_action_delete": "Dileu o'r Hanes",
+    "search_for_something_with": "Chwilio am {search_term} gyda:",
+    "search_button": "Chwilio",
+    "search_header": "{search_engine_name} Chwilio",
+    "search_web_placeholder": "Chwilio'r We",
+    "search_settings": "Newid y Gosodiadau Chwilio",
+    "welcome_title": "Croeso i dab newydd",
+    "welcome_body": "Bydd Firefox yn defnyddio'r gofod hwn i ddangos y nodau tudalen, erthyglau, fideos a thudalennau mwyaf perthnasol i chi, a thudalennau fuoch yn ymweld â nhw'n ddiweddar, fel bod modd i chi ddychwelydd atyn nhw'n hawdd.",
+    "welcome_label": "Adnabod eich Goreuon",
+    "time_label_less_than_minute": "<1m",
+    "time_label_minute": "{number}m",
+    "time_label_hour": "{number}a",
+    "time_label_day": "{number}d",
+    "settings_pane_button_label": "Cyfaddasu eich tudalen Tab Newydd",
+    "settings_pane_header": "Dewisiadau Tab Newydd",
+    "settings_pane_body": "Dewis beth rydych yn ei weld pan fyddwch yn agor tab newydd.",
+    "settings_pane_search_header": "Chwilio",
+    "settings_pane_search_body": "Chwilio'r We o'ch tab newydd.",
+    "settings_pane_topsites_header": "Hoff Wefannau",
+    "settings_pane_topsites_body": "Cael mynediad at y gwefannau rydych yn ymweld â nhw amlaf.",
+    "settings_pane_topsites_options_showmore": "Dangos dwy res",
+    "settings_pane_highlights_header": "Goreuon",
+    "settings_pane_highlights_body": "Edrych nôl ar eich hanes pori a nodau tudalen diweddar.",
+    "settings_pane_done_button": "Gorffen",
+    "edit_topsites_button_text": "Golygu",
+    "edit_topsites_button_label": "Cyfaddasu eich adran Hoff Wefannau",
+    "edit_topsites_showmore_button": "Dangos rhagor",
+    "edit_topsites_showless_button": "Dangos llai",
+    "edit_topsites_done_button": "Gorffen",
+    "edit_topsites_pin_button": "Pinio'r wefan",
+    "edit_topsites_edit_button": "Golygu'r wefan",
+    "edit_topsites_dismiss_button": "Dileu'r wefan"
+  },
   "da": {
     "newtab_page_title": "Nyt faneblad",
     "default_label_loading": "Indlæser…",
     "header_top_sites": "Mest besøgte websider",
     "header_highlights": "Højdepunkter",
     "type_label_visited": "Besøgt",
     "type_label_bookmarked": "Bogmærket",
     "type_label_synced": "Synkroniseret fra en anden enhed",
@@ -390,26 +459,46 @@
     "menu_action_remove_bookmark": "Fjern bogmærke",
     "menu_action_copy_address": "Kopier adresse",
     "menu_action_email_link": "Send link…",
     "menu_action_open_new_window": "Åbn i et nyt vindue",
     "menu_action_open_private_window": "Åbn i et nyt privat vindue",
     "menu_action_dismiss": "Afvis",
     "menu_action_delete": "Slet fra historik",
     "search_for_something_with": "Søg efter {search_term} med:",
+    "search_button": "Søg",
     "search_header": "{search_engine_name}-søgning",
     "search_web_placeholder": "Søg på internettet",
     "search_settings": "Skift søgeindstillinger",
     "welcome_title": "Velkommen til nyt faneblad",
     "welcome_body": "Firefox vil bruge denne plads til at vise dine mest relevante bogmærker, artikler, videoer og sider, du har besøgt for nylig - så kan du nemmere finde dem.",
     "welcome_label": "Finder dine højdepunkter",
     "time_label_less_than_minute": "<1 m.",
     "time_label_minute": "{number} m.",
     "time_label_hour": "{number} t.",
-    "time_label_day": "{number} d."
+    "time_label_day": "{number} d.",
+    "settings_pane_button_label": "Tilpas siden Nyt faneblad",
+    "settings_pane_header": "Indstillinger for Nyt faneblad",
+    "settings_pane_body": "Vælg, hvad der vises, når du åbner et nyt faneblad.",
+    "settings_pane_search_header": "Søgning",
+    "settings_pane_search_body": "Søg på nettet fra Nyt faneblad.",
+    "settings_pane_topsites_header": "Mest besøgte websider",
+    "settings_pane_topsites_body": "Adgang til de websider, du besøger oftest.",
+    "settings_pane_topsites_options_showmore": "Vis to rækker",
+    "settings_pane_highlights_header": "Højdepunkter",
+    "settings_pane_highlights_body": "Se tilbage på din seneste browserhistorik og nyligt oprettede bogmærker.",
+    "settings_pane_done_button": "Færdig",
+    "edit_topsites_button_text": "Rediger",
+    "edit_topsites_button_label": "Tilpas afsnittet Mest besøgte websider",
+    "edit_topsites_showmore_button": "Vis flere",
+    "edit_topsites_showless_button": "Vis færre",
+    "edit_topsites_done_button": "Færdig",
+    "edit_topsites_pin_button": "Fastgør denne webside",
+    "edit_topsites_edit_button": "Rediger denne webside",
+    "edit_topsites_dismiss_button": "Afvis denne webside"
   },
   "de": {
     "newtab_page_title": "Neuer Tab",
     "default_label_loading": "Wird geladen…",
     "header_top_sites": "Meistbesuchte Seiten",
     "header_highlights": "Wichtige Seiten",
     "type_label_visited": "Besucht",
     "type_label_bookmarked": "Lesezeichen",
@@ -657,16 +746,25 @@
     "edit_topsites_button_label": "Customize your Top Sites section",
     "edit_topsites_showmore_button": "Show more",
     "edit_topsites_showless_button": "Show less",
     "edit_topsites_done_button": "Done",
     "edit_topsites_pin_button": "Pin this site",
     "edit_topsites_unpin_button": "Unpin this site",
     "edit_topsites_edit_button": "Edit this site",
     "edit_topsites_dismiss_button": "Dismiss this site",
+    "edit_topsites_add_button": "Add",
+    "topsites_form_add_header": "New Top Site",
+    "topsites_form_edit_header": "Edit Top Site",
+    "topsites_form_title_placeholder": "Enter a title",
+    "topsites_form_url_placeholder": "Type or paste a URL",
+    "topsites_form_add_button": "Add",
+    "topsites_form_save_button": "Save",
+    "topsites_form_cancel_button": "Cancel",
+    "topsites_form_url_validation": "Valid URL required",
     "pocket_read_more": "Popular Topics:",
     "pocket_read_even_more": "View More Stories",
     "pocket_feedback_header": "The best of the web, curated by over 25 million people.",
     "pocket_feedback_body": "Pocket, a part of the Mozilla family, will help connect you to high-quality content that you may not have found otherwise.",
     "pocket_send_feedback": "Send Feedback"
   },
   "en-ZA": {},
   "eo": {
@@ -683,26 +781,28 @@
     "menu_action_remove_bookmark": "Forigi legosignon",
     "menu_action_copy_address": "Kopii adreson",
     "menu_action_email_link": "Sendi ligilon retpoŝte…",
     "menu_action_open_new_window": "Malfermi en nova fenestro",
     "menu_action_open_private_window": "Malfermi en nova privata fenestro",
     "menu_action_dismiss": "Ignori",
     "menu_action_delete": "Forigi el historio",
     "search_for_something_with": "Serĉi {search_term} per:",
+    "search_button": "Serĉi",
     "search_header": "Serĉo de {search_engine_name}",
     "search_web_placeholder": "Serĉi la Teksaĵon",
     "search_settings": "Modifi serĉajn agordojn",
     "welcome_title": "Bonvenon al nova langeto",
     "welcome_body": "Firefox uzos tiun ĉi spacon por montri al vi viaj plej gravajn legosignojn, artikolojn, filmetojn kaj paĝojn, kiujn vi vizitis antaŭ nelonge, tiel ke vi povos reiri al ili facile.",
     "welcome_label": "Elstaraĵoj identigataj",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
-    "time_label_day": "{number}t"
+    "time_label_day": "{number}t",
+    "settings_pane_button_label": "Personecigi la paĝon por novaj langetoj"
   },
   "es-AR": {
     "newtab_page_title": "Nueva pestaña",
     "default_label_loading": "Cargando…",
     "header_top_sites": "Más visitados",
     "header_highlights": "Destacados",
     "type_label_visited": "Visitados",
     "type_label_bookmarked": "Marcados",
@@ -913,26 +1013,46 @@
     "menu_action_remove_bookmark": "Eemalda järjehoidja",
     "menu_action_copy_address": "Kopeeri aadress",
     "menu_action_email_link": "Saada link e-postiga…",
     "menu_action_open_new_window": "Ava uues aknas",
     "menu_action_open_private_window": "Ava uues privaatses aknas",
     "menu_action_dismiss": "Peida",
     "menu_action_delete": "Kustuta ajaloost",
     "search_for_something_with": "Otsi fraasi {search_term}, kasutades otsingumootorit:",
+    "search_button": "Otsi",
     "search_header": "{search_engine_name}",
     "search_web_placeholder": "Otsi veebist",
     "search_settings": "Muuda otsingu sätteid",
     "welcome_title": "Tere tulemast uuele kaardile",
     "welcome_body": "Firefox kasutab seda lehte, et kuvada sulle kõige olulisemaid järjehoidjaid, artikleid, videoid ja lehti, mida oled hiljuti külastanud, nii et pääseksid kergelt nende juurde tagasi.",
     "welcome_label": "Esiletõstetava sisu tuvastamine",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}t",
-    "time_label_day": "{number}p"
+    "time_label_day": "{number}p",
+    "settings_pane_button_label": "Kohanda uue kaardi lehte",
+    "settings_pane_header": "Uue kaardi sätted",
+    "settings_pane_body": "Vali asjad, mida soovid uue kaardi avamisel näha.",
+    "settings_pane_search_header": "Otsi",
+    "settings_pane_search_body": "Veebis otsimine uuel kaardil.",
+    "settings_pane_topsites_header": "Top saidid",
+    "settings_pane_topsites_body": "Ligipääs enim külastatud veebilehtedele.",
+    "settings_pane_topsites_options_showmore": "Kuvatakse kahel real",
+    "settings_pane_highlights_header": "Esiletõstetud",
+    "settings_pane_highlights_body": "Tagasivaade hiljutisele lehitsemisajaloole ning lisatud järjehoidjatele.",
+    "settings_pane_done_button": "Valmis",
+    "edit_topsites_button_text": "Muuda",
+    "edit_topsites_button_label": "Kohanda top saitide osa",
+    "edit_topsites_showmore_button": "Kuva rohkem",
+    "edit_topsites_showless_button": "Näita vähem",
+    "edit_topsites_done_button": "Valmis",
+    "edit_topsites_pin_button": "Kinnita see sait",
+    "edit_topsites_edit_button": "Muuda seda saiti",
+    "edit_topsites_dismiss_button": "Peida see sait"
   },
   "eu": {},
   "fa": {
     "newtab_page_title": "زبانه جدید",
     "default_label_loading": "در حال بارگیری…",
     "header_top_sites": "سایت‌های برتر",
     "header_highlights": "برجسته‌ها",
     "type_label_visited": "مشاهده شده",
@@ -1217,27 +1337,97 @@
     "menu_action_remove_bookmark": "הסרת סימניה",
     "menu_action_copy_address": "העתקת כתובת",
     "menu_action_email_link": "שליחת קישור בדוא״ל…",
     "menu_action_open_new_window": "פתיחה בחלון חדש",
     "menu_action_open_private_window": "פתיחה בלשונית פרטית חדשה",
     "menu_action_dismiss": "ביטול",
     "menu_action_delete": "מחיקה מההיסטוריה",
     "search_for_something_with": "חיפוש אחר {search_term} עם:",
+    "search_button": "חיפוש",
     "search_header": "חיפוש ב־{search_engine_name}",
     "search_web_placeholder": "חיפוש ברשת",
     "search_settings": "שינוי הגדרות חיפוש",
     "welcome_title": "ברוכים הבאים לדף הלשונית החדשה",
     "welcome_body": "Firefox ישתמש באזור זה כדי להציג את הסימניות הרלוונטיות ביותר, מאמרים, סרטוני וידאו ודפים שביקרת בהם לאחרונה, כך שניתן יהיה לגשת אליהם שוב בקלות.",
+    "welcome_label": "תחומי העניין שלך מזוהים",
     "time_label_less_than_minute": "פחות מדקה",
     "time_label_minute": "{number} דקות",
     "time_label_hour": "{number} שעות",
-    "time_label_day": "{number} ימים"
+    "time_label_day": "{number} ימים",
+    "settings_pane_button_label": "התאמה אישית של דף הלשונית החדשה שלך",
+    "settings_pane_header": "העדפות לשונית חדשה",
+    "settings_pane_body": "ניתן לבחור מה יופיע בפניך בעת פתיחת לשונית חדשה.",
+    "settings_pane_search_header": "חיפוש",
+    "settings_pane_search_body": "חיפוש באינטרנט ישירות מהלשונית החדשה שלך.",
+    "settings_pane_topsites_header": "אתרים מובילים",
+    "settings_pane_topsites_body": "גישה לאתרים בהם ביקרת הכי הרבה.",
+    "settings_pane_topsites_options_showmore": "הצגת שתי שורות",
+    "settings_pane_highlights_header": "המלצות",
+    "settings_pane_highlights_body": "ניתן להסתכל על היסטוריית הגלישה העדכנית שלך ועל הסימניות האחרונות שנוצרו.",
+    "settings_pane_done_button": "סיום",
+    "edit_topsites_button_text": "עריכה",
+    "edit_topsites_button_label": "התאמת אגף האתרים המובילים שלך",
+    "edit_topsites_showmore_button": "להציג יותר",
+    "edit_topsites_showless_button": "להציג פחות",
+    "edit_topsites_done_button": "בוצע",
+    "edit_topsites_pin_button": "נעיצת אתר זה",
+    "edit_topsites_edit_button": "עריכת אתר זה",
+    "edit_topsites_dismiss_button": "התעלמות מאתר זה"
   },
-  "hi-IN": {},
+  "hi-IN": {
+    "newtab_page_title": "नया टैब",
+    "default_label_loading": "लोड हो रहा है…",
+    "header_top_sites": "सर्वोच्च साइटें",
+    "header_highlights": "प्रमुखताएँ",
+    "type_label_visited": "देखी गई",
+    "type_label_bookmarked": "पुस्तचिह्न लगाया हुआ",
+    "type_label_synced": "किसी अन्य उपकरण से समकालीन किया गया",
+    "type_label_open": "खोलें",
+    "type_label_topic": "विषय",
+    "menu_action_bookmark": "पुस्तचिह्न",
+    "menu_action_remove_bookmark": "पुस्तचिह्न हटाएँ",
+    "menu_action_copy_address": "पता कॉपी करें",
+    "menu_action_email_link": "ईमेल लिंक…",
+    "menu_action_open_new_window": "एक नई विंडो में खोलें",
+    "menu_action_open_private_window": "एक नई निजी विंडो में खोलें",
+    "menu_action_dismiss": "निरस्त करें",
+    "menu_action_delete": "इतिहास से मिटाएँ",
+    "search_for_something_with": "इस के साथ {search_term} के लिए खोजें:",
+    "search_button": "खोज",
+    "search_header": "{search_engine_name} खोज",
+    "search_web_placeholder": "वेब पर खोजें",
+    "search_settings": "खोज सेटिंग बदलें",
+    "welcome_title": "नए टैब में आपका स्वागत है",
+    "welcome_body": "Firefox यह जगह आपके सर्वाधिक प्रासंगिक पुस्तचिन्ह, लेख, वीडियो और पृष्ठों जिनका आपने हाल ही में दौरा किया है उनको दर्शाने के लिए करेगा, ताकि आप बाद में उन तक आसानी से वापस जा सकें.",
+    "welcome_label": "आपके प्रमुखताओं की पहचान की जा रही है",
+    "time_label_less_than_minute": "<1मि0",
+    "time_label_minute": "{number}मि0",
+    "time_label_hour": "{number}मि0",
+    "time_label_day": "{number}दिन",
+    "settings_pane_button_label": "अपने नए टैब पृष्ठ को अनुकूलित करें",
+    "settings_pane_header": "नयी टैब वरीयताएँ",
+    "settings_pane_body": "चयन करें कि नया टैब खोलने पर आप क्या देखें.",
+    "settings_pane_search_header": "खोज",
+    "settings_pane_search_body": "अपने नए टैब से वेब पर खोजें.",
+    "settings_pane_topsites_header": "सर्वोच्च साइटें",
+    "settings_pane_topsites_body": "आपके द्वारा सबसे ज्यादा खोजी जाने वाली वेबसाइट्स देखें.",
+    "settings_pane_topsites_options_showmore": "दो पंक्तियाँ दिखाएँ",
+    "settings_pane_highlights_header": "प्रमुखताएँ",
+    "settings_pane_highlights_body": "अपने हाल के ब्राउज़िंग इतिहास और नए बनाए गए पुस्तचिन्हों को वापस देखें.",
+    "settings_pane_done_button": "संपन्न",
+    "edit_topsites_button_text": "संपादित करें",
+    "edit_topsites_button_label": "अपने शीर्ष साइट्स अनुभाग को अनुकूलित करें",
+    "edit_topsites_showmore_button": "अधिक दिखाएँ",
+    "edit_topsites_showless_button": "कम दिखाएँ",
+    "edit_topsites_done_button": "पूर्ण",
+    "edit_topsites_pin_button": "इस साइट को पिन करें",
+    "edit_topsites_edit_button": "इस साइट को संपादित करें",
+    "edit_topsites_dismiss_button": "इस साइट को ख़ारिज करें"
+  },
   "hr": {
     "newtab_page_title": "Nova kartica",
     "default_label_loading": "Učitavanje…",
     "header_top_sites": "Najbolje stranice",
     "header_highlights": "Istaknuto",
     "type_label_visited": "Posjećeno",
     "type_label_bookmarked": "Zabilježeno",
     "type_label_synced": "Sinkronizirano s drugog uređaja",
@@ -1247,26 +1437,46 @@
     "menu_action_remove_bookmark": "Ukloni zabilješku",
     "menu_action_copy_address": "Kopiraj adresu",
     "menu_action_email_link": "Pošalji poveznicu e-poštom…",
     "menu_action_open_new_window": "Otvori u novom prozoru",
     "menu_action_open_private_window": "Otvori u novom privatnom prozoru",
     "menu_action_dismiss": "Odbaci",
     "menu_action_delete": "Obriši iz povijesti",
     "search_for_something_with": "Traži {search_term} s:",
+    "search_button": "Traži",
     "search_header": "{search_engine_name} pretraživanje",
     "search_web_placeholder": "Pretraži web",
     "search_settings": "Promijeni postavke pretraživanja",
     "welcome_title": "Dobro došli u novu karticu",
     "welcome_body": "Firefox će koristiti ovaj prostor kako bi vam pokazao najbitnije zabilješke, članke, video uratke i stranice koje ste nedavno posjetili, tako da se možete lako vratiti na njih.",
     "welcome_label": "Identificiranje istaknutog",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
-    "time_label_day": "{number}d"
+    "time_label_day": "{number}d",
+    "settings_pane_button_label": "Prilagodite svoju početnu stranicu nove kartice",
+    "settings_pane_header": "Postavke nove kartice",
+    "settings_pane_body": "Odaberite što ćete vidjeti kada otvorite novu karticu.",
+    "settings_pane_search_header": "Traži",
+    "settings_pane_search_body": "Pretražite Web iz nove kartice.",
+    "settings_pane_topsites_header": "Najbolje stranice",
+    "settings_pane_topsites_body": "Pristupite stranicama koje najčešće posjećujete.",
+    "settings_pane_topsites_options_showmore": "Prikaži dva reda",
+    "settings_pane_highlights_header": "Istaknuto",
+    "settings_pane_highlights_body": "Osvrnite se na nedavno posjećene stranice i nove zabilješke.",
+    "settings_pane_done_button": "Gotovo",
+    "edit_topsites_button_text": "Uredi",
+    "edit_topsites_button_label": "Prilagodite odjel s najboljim stranicama",
+    "edit_topsites_showmore_button": "Prikaži više",
+    "edit_topsites_showless_button": "Prikaži manje",
+    "edit_topsites_done_button": "Gotovo",
+    "edit_topsites_pin_button": "Zakači stranicu",
+    "edit_topsites_edit_button": "Uredi ovu stranicu",
+    "edit_topsites_dismiss_button": "Odbaci stranicu"
   },
   "hsb": {
     "newtab_page_title": "Nowy rajtark",
     "default_label_loading": "Začituje so…",
     "header_top_sites": "Najhusćišo wopytane sydła",
     "header_highlights": "Wjerški",
     "type_label_visited": "Wopytany",
     "type_label_bookmarked": "Jako zapołožka składowany",
@@ -1690,26 +1900,46 @@
     "menu_action_remove_bookmark": "즐겨찾기 삭제",
     "menu_action_copy_address": "주소 복사",
     "menu_action_email_link": "메일로 링크 보내기…",
     "menu_action_open_new_window": "새 창에서 열기",
     "menu_action_open_private_window": "새 사생활 보호 창에서 열기",
     "menu_action_dismiss": "닫기",
     "menu_action_delete": "방문 기록에서 삭제",
     "search_for_something_with": "다음에서 {search_term} 검색:",
+    "search_button": "검색",
     "search_header": "{search_engine_name} 검색",
     "search_web_placeholder": "웹 검색",
     "search_settings": "검색 설정 바꾸기",
     "welcome_title": "새 탭을 소개합니다",
     "welcome_body": "최근에 방문한 관련있는 즐겨찾기나 글, 동영상, 페이지를 Firefox가 여기에 표시해서 쉽게 다시 찾아볼 수 있게 할 것입니다.",
     "welcome_label": "하이라이트 확인",
     "time_label_less_than_minute": "<1분",
     "time_label_minute": "{number}분",
     "time_label_hour": "{number}시",
-    "time_label_day": "{number}일"
+    "time_label_day": "{number}일",
+    "settings_pane_button_label": "새 탭 페이지 꾸미기",
+    "settings_pane_header": "새 탭 설정",
+    "settings_pane_body": "새 탭을 열 때 어떤 화면을 볼지 선택하세요.",
+    "settings_pane_search_header": "검색",
+    "settings_pane_search_body": "새 탭에서 웹을 검색하세요.",
+    "settings_pane_topsites_header": "상위 사이트",
+    "settings_pane_topsites_body": "가장 많이 방문한 웹 사이트에 접근하세요.",
+    "settings_pane_topsites_options_showmore": "두 줄로 보기",
+    "settings_pane_highlights_header": "하이라이트",
+    "settings_pane_highlights_body": "최근 방문 기록과 북마크를 살펴보세요.",
+    "settings_pane_done_button": "완료",
+    "edit_topsites_button_text": "수정",
+    "edit_topsites_button_label": "상위 사이트 영역 꾸미기",
+    "edit_topsites_showmore_button": "더보기",
+    "edit_topsites_showless_button": "줄이기",
+    "edit_topsites_done_button": "완료",
+    "edit_topsites_pin_button": "이 사이트 고정",
+    "edit_topsites_edit_button": "이 사이트 수정",
+    "edit_topsites_dismiss_button": "이 사이트 제거"
   },
   "ku": {},
   "lij": {
     "newtab_page_title": "Neuvo Feuggio",
     "default_label_loading": "Carego…",
     "header_top_sites": "I megio sciti",
     "header_highlights": "In evidensa",
     "type_label_visited": "Vixitou",
@@ -1771,25 +2001,46 @@
     "menu_action_remove_bookmark": "ລຶບບຸກມາກອອກ",
     "menu_action_copy_address": "ສຳເນົາທີ່ຢູ່",
     "menu_action_email_link": "ລີ້ງອີເມວ…",
     "menu_action_open_new_window": "ເປີດລີ້ງໃນຫນ້າຕ່າງໃຫມ່",
     "menu_action_open_private_window": "ເປີດໃນຫນ້າຕ່າງສ່ວນຕົວໃຫມ່",
     "menu_action_dismiss": "ຍົກເລີກ",
     "menu_action_delete": "ລຶບອອກຈາກປະຫວັດການນຳໃຊ້",
     "search_for_something_with": "ຄົ້ນຫາສໍາລັບ {search_term} ດ້ວຍ:",
+    "search_button": "ຊອກ​ຫາ",
     "search_header": "ຄົ້ນຫາ {search_engine_name}",
     "search_web_placeholder": "ຄົ້ນຫາເວັບ",
     "search_settings": "ປ່ຽນການຕັ້ງຄ່າການຄົ້ນຫາ",
     "welcome_title": "ຍິນດີຕອນຮັບເຂົ້າສູ່ແຖບໃຫມ່",
+    "welcome_body": "Firefox ຈະໃຊ້ພື້ນທີ່ນີ້ເພື່ອສະແດງໃຫ້ເຫັນບຸກມາກທີ່ກ່ຽວຂ້ອງທີ່ສຸດຂອງທ່ານ, ບົດຄວາມ, ວິດີໂອ, ແລະ ຫນ້າທີ່ທ່ານຫາກາໄດ້ເຂົ້າໄປເບິງ, ສະນັ້ນທ່ານຈຶ່ງສາມາດກັບໄປເບິງຄືນອີກໄດ້ຢ່າງງ່າຍດາຍ.",
     "welcome_label": "ກໍາລັງລະບຸລາຍການເດັ່ນຂອງທ່ານ",
     "time_label_less_than_minute": "<1 ນາທີ",
     "time_label_minute": "{number} ນາທີ",
     "time_label_hour": "{number} ຊົ່ວໂມງ",
-    "time_label_day": "{number} ມື້"
+    "time_label_day": "{number} ມື້",
+    "settings_pane_button_label": "ປັບແຕ່ງຫນ້າແທັບໃຫມ່ຂອງທ່ານ",
+    "settings_pane_header": "ການຕັ້ງຄ່າແທັບໃຫມ່",
+    "settings_pane_body": "ເລືອກສິ່ງທີ່ທ່ານເຫັນເມື່ອທ່ານເປີດແທັບໃຫມ່.",
+    "settings_pane_search_header": "ຊອກຫາ",
+    "settings_pane_search_body": "ຊອກຫາເວັບຈາກແທັບໃຫມ່ຂອງທ່ານ.",
+    "settings_pane_topsites_header": "ເວັບໄຊທ໌ຍອດນິຍົມ",
+    "settings_pane_topsites_body": "ເຂົ້າເວັບໄຊທ໌ທີ່ທ່ານໄດ້ເຂົ້າໄປຫລາຍທີ່ສຸດ.",
+    "settings_pane_topsites_options_showmore": "ສະແດງເປັນສອງແຖວ",
+    "settings_pane_highlights_header": "ຈຸດເດັ່ນ",
+    "settings_pane_highlights_body": "ຍ້ອນຄືນກັບໄປເບິງປະຫວັດການທ່ອງເວັບທີ່ຫາກາເຂົ້າໄປ ແລະ ບຸກມາກທີ່ໄດ້ຮັບການສ້າງຂື້ນມາໃຫມ່ຂອງທ່ານ.",
+    "settings_pane_done_button": "ສຳເລັດ",
+    "edit_topsites_button_text": "ແກ້ໄຂ",
+    "edit_topsites_button_label": "ປັບແຕ່ງພາກສ່ວນເວັບໄຊທ໌ຍອດນິຍົມຂອງທ່ານ",
+    "edit_topsites_showmore_button": "ສະແດງເພີ່ມເຕີມ",
+    "edit_topsites_showless_button": "ສະແດງນ້ອຍລົງ",
+    "edit_topsites_done_button": "ສຳເລັດ",
+    "edit_topsites_pin_button": "Pin ເວັບໄຊທ໌ນີ້",
+    "edit_topsites_edit_button": "ແກ້ໄຂເວັບໄຊທ໌ນີ້",
+    "edit_topsites_dismiss_button": "ຍົກເລີກເວັບໄຊທ໌ນີ້"
   },
   "lt": {
     "newtab_page_title": "Nauja kortelė",
     "default_label_loading": "Įkeliama…",
     "header_top_sites": "Lankomiausios svetainės",
     "header_highlights": "Akcentai",
     "type_label_visited": "Aplankyti",
     "type_label_bookmarked": "Adresyne",
@@ -1832,17 +2083,19 @@
     "edit_topsites_showmore_button": "Rodyti daugiau",
     "edit_topsites_showless_button": "Rodyti mažiau",
     "edit_topsites_done_button": "Atlikta",
     "edit_topsites_pin_button": "Įsegti šią svetainę",
     "edit_topsites_edit_button": "Redaguoti šią svetainę",
     "edit_topsites_dismiss_button": "Paslėpti šią svetainę"
   },
   "ltg": {},
-  "lv": {},
+  "lv": {
+    "newtab_page_title": "Jauna cilne"
+  },
   "mai": {},
   "mk": {},
   "ml": {},
   "mn": {},
   "mr": {},
   "ms": {
     "newtab_page_title": "Tab Baru",
     "default_label_loading": "Memuatkan…",
@@ -1888,17 +2141,66 @@
     "edit_topsites_button_label": "Sesuaikan bahagian Laman Teratas anda",
     "edit_topsites_showmore_button": "Papar selanjutnya",
     "edit_topsites_showless_button": "Papar minima",
     "edit_topsites_done_button": "Siap",
     "edit_topsites_pin_button": "Pin laman ini",
     "edit_topsites_edit_button": "Edit laman ini",
     "edit_topsites_dismiss_button": "Buang laman ini"
   },
-  "my": {},
+  "my": {
+    "newtab_page_title": "တပ်ဗ်အသစ်ဖွင့်",
+    "default_label_loading": "ရယူနေသှ်…",
+    "header_top_sites": "အများဆုံးသုံးဆိုက်များ",
+    "header_highlights": "အသားပေးဖော်ပြချက်များ",
+    "type_label_visited": "သွားလည်ခဲ့သော",
+    "type_label_bookmarked": "စာအမှတ်မှတ်ထားသော",
+    "type_label_synced": "အခြားပစ္စည်းတစ်ခုမှရယူထားသှ်",
+    "type_label_open": "ဖွင့်ပါ",
+    "type_label_topic": "အကြောင်းအရာ",
+    "menu_action_bookmark": "စာအမှတ်",
+    "menu_action_remove_bookmark": "စာအမှတ်အားဖယ်ပါ",
+    "menu_action_copy_address": "လိပ်စာအားကူးယူပါ",
+    "menu_action_email_link": "လင်ခ့်အားအီးမေလ်းဖြင့်ပို့ပါ…",
+    "menu_action_open_new_window": "အခြားဝင်းဒိုးတစ်ခုမှဖွင့်ပါ",
+    "menu_action_open_private_window": "အခြားတစ်ကိုယ်ရေသုံးဝင်းဒိုးတစ်ခုဖွင့်ပါ",
+    "menu_action_dismiss": "ပိတ်လိုက်ပါ",
+    "menu_action_delete": "မှတ်တမ်းမှ ဖျက်ပါ",
+    "search_for_something_with": "{search_term} အားရှာပါ -",
+    "search_button": "ရှာ",
+    "search_header": "{search_engine_name} ရှာဖွေမှု",
+    "search_web_placeholder": "ဝတ်ဘ်ပေါ်တွင် ရှာဖွေခြင်း",
+    "search_settings": "ရှာဖွေမှုအပြင်အဆင်အားပြောင်းလဲပါ",
+    "welcome_title": "တပ်ဗ်အသစ်တစ်ခုမှကြိုဆိုပါတယ်",
+    "welcome_body": "ယခုနေရာအား Firefox မှ အသင့်လျော်ဆုံး သင်သွားလည်ခဲ့ဖူးသော စာအမှတ်များ၊ ဆောင်းပါးများ၊ ရုပ်ရှင်များ နှင့် စာမျက်နှာများအား ပြသဖို့အသုံးပြုမည်ဖြစ်ပါတယ်။",
+    "welcome_label": "သင့် အသားပေးဖော်ပြချက်များကိုသတိထားမည်",
+    "time_label_less_than_minute": "<1မီတာ",
+    "time_label_minute": "{number}မီတာ",
+    "time_label_hour": "{number}အမြင့်",
+    "time_label_day": "{number}နေ့",
+    "settings_pane_button_label": "သင့်တပ်ဗ်အသစ်စာမျက်နှာအား ပြင်ဆင်မည်",
+    "settings_pane_header": "စာတပ်ဗ်အသစ်အပြင်အဆင်များ",
+    "settings_pane_body": "သင် တပ်ဗ်အသစ်ဖွင့်လိုက်ပါကမြင်ရမည့်အရာများကိုရွေးချယ်ပါ",
+    "settings_pane_search_header": "ရှာဖွေပါ",
+    "settings_pane_search_body": "ဝက်ဘ်ပေါ်တွင် သင့်တပ်ဗ်အသစ်မှရှာဖွေပါ",
+    "settings_pane_topsites_header": "ထိပ်တန်းဝတ်ဘ်ဆိုက်များ",
+    "settings_pane_topsites_body": "သင်အများဆုံးသွားလည်သော ဝတ်ဘ်ဆိုက်များကို ရယူပါ",
+    "settings_pane_topsites_options_showmore": "အတန်းနှစ်တန်းနှင့်ပြပါ",
+    "settings_pane_highlights_header": "အသားပေးဖော်ပြချက်များ",
+    "settings_pane_highlights_body": "သင်လတ်တလောသွားလည်ထားသော မှတ်တမ်းနှင့် အသစ်ဖန်တီးထားသော စာအမှတ်များအား ပြန်ကြည့်ပါ",
+    "settings_pane_done_button": "ပြီးပြီ",
+    "edit_topsites_button_text": "ပြင်ဆင်မည်",
+    "edit_topsites_button_label": "သင့်ထိပ်တန်းဆိုက် အမြင်အားပြင်ဆင်ပါ",
+    "edit_topsites_showmore_button": "ထပ်ပြပါ",
+    "edit_topsites_showless_button": "ချုံ့ပြရန်",
+    "edit_topsites_done_button": "ပြီးပြီ",
+    "edit_topsites_pin_button": "ဝတ်ဆိုဒ်အားpinလုပ်ထားမည်",
+    "edit_topsites_edit_button": "ဆိုက်အားပြင်မည်",
+    "edit_topsites_dismiss_button": "ဆိုက်အားဖျက်လိုက်မည်"
+  },
   "nb-NO": {
     "newtab_page_title": "Ny fane",
     "default_label_loading": "Laster …",
     "header_top_sites": "Mest besøkte nettsider",
     "header_highlights": "Høydepunkter",
     "type_label_visited": "Besøkt",
     "type_label_bookmarked": "Bokmerket",
     "type_label_synced": "Synkronisert fra annen enhet",
@@ -1908,26 +2210,46 @@
     "menu_action_remove_bookmark": "Fjern bokmerke",
     "menu_action_copy_address": "Kopier adresse",
     "menu_action_email_link": "Send lenke på e-post …",
     "menu_action_open_new_window": "Åpne i nytt vindu",
     "menu_action_open_private_window": "Åpne i nytt privat vindu",
     "menu_action_dismiss": "Avslå",
     "menu_action_delete": "Slett fra historikk",
     "search_for_something_with": "Søk etter {search_term} med:",
+    "search_button": "Søk",
     "search_header": "{search_engine_name}-søk",
     "search_web_placeholder": "Søk på nettet",
     "search_settings": "Endre søkeinnstillinger",
     "welcome_title": "Velkommen til ny fane",
     "welcome_body": "Firefox vil bruke denne plassen til å vise deg de mest relevante bokmerkene, artiklene, videoene og sidene du nettopp har besøkt, slik at du enkelt kan finne tilbake til de.",
     "welcome_label": "Identifiserer dine høydepunkter",
     "time_label_less_than_minute": "<1 m",
     "time_label_minute": "{number} m",
     "time_label_hour": "{number} t",
-    "time_label_day": "{number} d"
+    "time_label_day": "{number} d",
+    "settings_pane_button_label": "Tilpass siden for Ny fane",
+    "settings_pane_header": "Innstillinger for Ny fane",
+    "settings_pane_body": "Velg hva som vises når du åpner en ny fane.",
+    "settings_pane_search_header": "Søk",
+    "settings_pane_search_body": "Søk på nettet fra din nye fane.",
+    "settings_pane_topsites_header": "Mest besøkte",
+    "settings_pane_topsites_body": "Tilgang til nettsidene du besøker mest.",
+    "settings_pane_topsites_options_showmore": "Vis to rader",
+    "settings_pane_highlights_header": "Høydepunkter",
+    "settings_pane_highlights_body": "Se tilbake på din siste nettleserhistorikk og nyopprettede bokmerker.",
+    "settings_pane_done_button": "Ferdig",
+    "edit_topsites_button_text": "Rediger",
+    "edit_topsites_button_label": "Tilpass seksjonen Mest besøkte",
+    "edit_topsites_showmore_button": "Vis mer",
+    "edit_topsites_showless_button": "Vis mindre",
+    "edit_topsites_done_button": "Ferdig",
+    "edit_topsites_pin_button": "Fest nettsiden",
+    "edit_topsites_edit_button": "Rediger denne nettsiden",
+    "edit_topsites_dismiss_button": "Avvis denne nettsiden"
   },
   "ne-NP": {
     "newtab_page_title": "नयाँ ट्याब",
     "default_label_loading": "लोड हुदैँछ...",
     "header_top_sites": "शीर्ष साइटहरु",
     "header_highlights": "विशेषताहरू",
     "type_label_visited": "भ्रमण गरिएको",
     "type_label_bookmarked": "पुस्तकचिनो लागाइएको",
@@ -2155,21 +2477,21 @@
     "edit_topsites_dismiss_button": "Odrzuć tę stronę"
   },
   "pt-BR": {
     "newtab_page_title": "Nova aba",
     "default_label_loading": "Carregando…",
     "header_top_sites": "Sites preferidos",
     "header_highlights": "Destaques",
     "type_label_visited": "Visitado",
-    "type_label_bookmarked": "Favorito",
+    "type_label_bookmarked": "Adicionado aos favoritos",
     "type_label_synced": "Sincronizado a partir de outro dispositivo",
     "type_label_open": "Abrir",
     "type_label_topic": "Tópico",
-    "menu_action_bookmark": "Favoritos",
+    "menu_action_bookmark": "Adicionar aos favoritos",
     "menu_action_remove_bookmark": "Remover favorito",
     "menu_action_copy_address": "Copiar endereço",
     "menu_action_email_link": "Enviar link por e-mail…",
     "menu_action_open_new_window": "Abrir em uma nova janela",
     "menu_action_open_private_window": "Abrir em uma nova janela privativa",
     "menu_action_dismiss": "Dispensar",
     "menu_action_delete": "Excluir do histórico",
     "search_for_something_with": "Pesquisar por {search_term} com:",
@@ -2500,26 +2822,46 @@
     "menu_action_remove_bookmark": "Hiqe Faqerojtësin",
     "menu_action_copy_address": "Kopjoje Adresën",
     "menu_action_email_link": "Dërgoni Lidhje me Email…",
     "menu_action_open_new_window": "Hape në Dritare të Re",
     "menu_action_open_private_window": "Hape në Dritare të Re Private",
     "menu_action_dismiss": "Hidhe tej",
     "menu_action_delete": "Fshije prej Historiku",
     "search_for_something_with": "Kërko për {search_term} me:",
+    "search_button": "Kërko",
     "search_header": "Kërkim me {search_engine_name}",
     "search_web_placeholder": "Kërkoni në Web",
     "search_settings": "Ndryshoji Rregullimet e Kërkimit",
     "welcome_title": "Mirë se vini te skedë e re",
     "welcome_body": "Firefox-i do ta përdorë këtë hapësirë për t’ju shfaqur faqerojtësit, artikujt, videot dhe faqet më me peshë që keni vizituar së fundi, që kështu të mund të ktheheni lehtë në to.",
     "welcome_label": "Po identifikohen Highlights tuaj",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
-    "time_label_day": "{number}d"
+    "time_label_day": "{number}d",
+    "settings_pane_button_label": "Personalizoni faqen tuaj Skedë e Re",
+    "settings_pane_header": "Parapëlqime për Skedë të Re",
+    "settings_pane_body": "Zgjidhni ç’doni të shihni kur hapni një skedë të re.",
+    "settings_pane_search_header": "Kërko",
+    "settings_pane_search_body": "Kërkoni në Web prej skedës tuaj të re.",
+    "settings_pane_topsites_header": "Sajte Kryesues",
+    "settings_pane_topsites_body": "Hyni te sajtet që vizitoni më shpesh.",
+    "settings_pane_topsites_options_showmore": "Shfaq dy rreshta",
+    "settings_pane_highlights_header": "Në Pah",
+    "settings_pane_highlights_body": "Rikthejuni historikut të shfletimeve të fundit dhe faqerojtësve të krijuar rishtas.",
+    "settings_pane_done_button": "U bë",
+    "edit_topsites_button_text": "Përpunoni",
+    "edit_topsites_button_label": "Personalizoni ndarjen tuaj Sajte Kryesues",
+    "edit_topsites_showmore_button": "Shfaq më tepër",
+    "edit_topsites_showless_button": "Shfaq më pak",
+    "edit_topsites_done_button": "U bë",
+    "edit_topsites_pin_button": "Fiksoje këtë sajt",
+    "edit_topsites_edit_button": "Përpunoni këtë sajt",
+    "edit_topsites_dismiss_button": "Hidhe tej këtë sajt"
   },
   "sr": {
     "newtab_page_title": "Нови језичак",
     "default_label_loading": "Учитавање…",
     "header_top_sites": "Популарни сајтови",
     "header_highlights": "Истакнути",
     "type_label_visited": "Посећене",
     "type_label_bookmarked": "Забележено",
@@ -2632,38 +2974,58 @@
     "menu_action_remove_bookmark": "ఇష్టాంశాన్ని తొలగించు",
     "menu_action_copy_address": "చిరునామా కాపీ చెయ్యండి",
     "menu_action_email_link": "ఈమెయిలు లింకు…",
     "menu_action_open_new_window": "కొత్త విండోలో తెరువు",
     "menu_action_open_private_window": "కొత్త వ్యక్తిగత విండోలో తెరువు",
     "menu_action_dismiss": "విస్మరించు",
     "menu_action_delete": "చరిత్ర నుంచి తీసివేయి",
     "search_for_something_with": "{search_term} కోసం దీని సాయంతో వెతుకు:",
+    "search_button": "వెతకండి",
     "search_header": "{search_engine_name} శోధన",
-    "search_web_placeholder": "వెబ్ లో వెతకండి",
+    "search_web_placeholder": "జాలంలో వెతకండి",
     "search_settings": "శోధన అమరికలు మార్చు",
     "welcome_title": "కొత్త ట్యాబుకు స్వాగతం",
     "welcome_body": "సముచితమైన మీ ఇష్టాంశాలను, వ్యాసాలను, వీడియోలను, ఇంకా మీరు ఇటీవలే చూసిన పేజీలను మీకు తేలిగ్గా అందుబాటులో ఉంచేందుకు Firefox ఈ జాగాని వాడుకుంటుంది.",
     "welcome_label": "మీ ముఖ్యాంశాలను గుర్తిస్తున్నది",
     "time_label_less_than_minute": "<1ని",
     "time_label_minute": "{number}ని",
     "time_label_hour": "{number}గం",
-    "time_label_day": "{number}రో"
+    "time_label_day": "{number}రో",
+    "settings_pane_button_label": "మీ కొత్త ట్యాబు పేజీని మలచుకోండి",
+    "settings_pane_header": "కొత్త ట్యాబు అభిరుచులు",
+    "settings_pane_body": "మీరు కొత్త ట్యాబు తెరిచినప్పుడు ఏం చూడాలో ఎంచుకోండి.",
+    "settings_pane_search_header": "వెతకడం",
+    "settings_pane_search_body": "కొత్త ట్యాబు నుండే జాలంలో వెతకండి.",
+    "settings_pane_topsites_header": "మేటి సైట్లు",
+    "settings_pane_topsites_body": "మీరు ఎక్కువగా చూసే వెబ్‌సైట్లను చూడండి.",
+    "settings_pane_topsites_options_showmore": "రెండు వరుసలు చూపించు",
+    "settings_pane_highlights_header": "విశేషాలు",
+    "settings_pane_highlights_body": "మీ ఇటీవలి విహరణ చరిత్రనూ కొత్త ఇష్టాంశాలను చూడండి.",
+    "settings_pane_done_button": "పూర్తయింది",
+    "edit_topsites_button_text": "మార్చు",
+    "edit_topsites_button_label": "మీ మేటి సైట్ల విభాగాన్ని మలచుకోండి",
+    "edit_topsites_showmore_button": "ఇంకా చూపించు",
+    "edit_topsites_showless_button": "కొన్నే చూపించు",
+    "edit_topsites_done_button": "పూర్తయింది",
+    "edit_topsites_pin_button": "ఈ సైటును ఇక్కడ గుచ్చు",
+    "edit_topsites_edit_button": "ఈ సైటును మార్చు",
+    "edit_topsites_dismiss_button": "ఈ సైటుని తీసివేయి"
   },
   "th": {
     "newtab_page_title": "แท็บใหม่",
     "default_label_loading": "กำลังโหลด…",
     "header_top_sites": "ไซต์เด่น",
     "header_highlights": "รายการเด่น",
     "type_label_visited": "เยี่ยมชมแล้ว",
-    "type_label_bookmarked": "คั่นหน้าแล้ว",
+    "type_label_bookmarked": "มีที่คั่นหน้าแล้ว",
     "type_label_synced": "ซิงค์จากอุปกรณ์อื่น",
     "type_label_open": "เปิด",
     "type_label_topic": "หัวข้อ",
-    "menu_action_bookmark": "ที่คั่นหน้า",
+    "menu_action_bookmark": "เพิ่มที่คั่นหน้า",
     "menu_action_remove_bookmark": "เอาที่คั่นหน้าออก",
     "menu_action_copy_address": "คัดลอกที่อยู่",
     "menu_action_email_link": "ส่งอีเมลลิงก์…",
     "menu_action_open_new_window": "เปิดในหน้าต่างใหม่",
     "menu_action_open_private_window": "เปิดในหน้าต่างส่วนตัวใหม่",
     "menu_action_dismiss": "ยกเลิก",
     "menu_action_delete": "ลบออกจากประวัติ",
     "search_for_something_with": "ค้นหาสำหรับ {search_term} ด้วย:",
@@ -2682,16 +3044,17 @@
     "settings_pane_header": "ตั้งค่าแท็บใหม่",
     "settings_pane_body": "เลือกสิ่งที่คุณเห็นเมื่อคุณเปิดแท็บใหม่",
     "settings_pane_search_header": "ค้นหา",
     "settings_pane_search_body": "ค้นหาเว็บจากแท็บใหม่ของคุณ",
     "settings_pane_topsites_header": "ไซต์เด่น",
     "settings_pane_topsites_body": "เข้าถึงเว็บไซต์ที่คุณเยี่ยมชมมากที่สุด",
     "settings_pane_topsites_options_showmore": "แสดงสองแถว",
     "settings_pane_highlights_header": "รายการเด่น",
+    "settings_pane_highlights_body": "มองย้อนกลับมาดูประวัติการท่องเว็บเมื่อเร็ว ๆ นี้และที่คั่นหน้าที่สร้างใหม่ของคุณ",
     "settings_pane_done_button": "เสร็จสิ้น",
     "edit_topsites_button_text": "แก้ไข",
     "edit_topsites_button_label": "ปรับแต่งส่วนไซต์เด่นของคุณ",
     "edit_topsites_showmore_button": "แสดงเพิ่มเติม",
     "edit_topsites_showless_button": "แสดงน้อยลง",
     "edit_topsites_done_button": "เสร็จสิ้น",
     "edit_topsites_pin_button": "ปักหมุดไซต์นี้",
     "edit_topsites_edit_button": "แก้ไขไซต์นี้",
@@ -2873,16 +3236,17 @@
     "welcome_title": "نئے ٹیب میں خوش آمدید",
     "welcome_body": "اس جگہ کا استعمال کرنے ہوئے Firefox آپکی متعلقہ نشانیاں، عبارات، وڈیوز اور صفحات جن کا حال ہی میں ص آُپ نے دورہ کیا ہے دکھائے گا۔ تاکہ آپ ان تک واپس آسانی سے پہنچ سکیں۔",
     "welcome_label": "آپکی جھلکیوں کی نشاندہی کر رہا ہے",
     "time_label_less_than_minute": "<1m",
     "time_label_minute": "{number}m",
     "time_label_hour": "{number}h",
     "time_label_day": "{number}d",
     "settings_pane_button_label": "اپنے نئے ٹیب کہ صفحہ کی تخصیص کریں",
+    "settings_pane_header": "نئے َٹیب کی ترجیحات",
     "settings_pane_search_header": "تلاش",
     "settings_pane_search_body": "اپنے نئے ٹیب سے وہب پر تلاش کریں۔",
     "settings_pane_topsites_header": "بہترین سائٹیں",
     "settings_pane_topsites_options_showmore": "دو قطاریں دکھائیں",
     "settings_pane_highlights_header": "شہ سرخياں",
     "settings_pane_done_button": "ہوگیا",
     "edit_topsites_button_text": "تدوین",
     "edit_topsites_done_button": "ہوگیا",
--- a/browser/extensions/activity-stream/lib/ActivityStream.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStream.jsm
@@ -1,78 +1,136 @@
 /* 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/. */
-/* globals LocalizationFeed, NewTabInit, SearchFeed, TelemetryFeed, TopSitesFeed, XPCOMUtils */
 "use strict";
 
 const {utils: Cu} = Components;
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
 const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
+const REASON_ADDON_UNINSTALL = 6;
+
+XPCOMUtils.defineLazyModuleGetter(this, "DefaultPrefs",
+  "resource://activity-stream/lib/ActivityStreamPrefs.jsm");
+
 // Feeds
 XPCOMUtils.defineLazyModuleGetter(this, "LocalizationFeed",
   "resource://activity-stream/lib/LocalizationFeed.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabInit",
   "resource://activity-stream/lib/NewTabInit.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesFeed",
   "resource://activity-stream/lib/PlacesFeed.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "SearchFeed",
-  "resource://activity-stream/lib/SearchFeed.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryFeed",
   "resource://activity-stream/lib/TelemetryFeed.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "TopSitesFeed",
   "resource://activity-stream/lib/TopSitesFeed.jsm");
 
-const feeds = {
-  // When you add a feed here:
-  // 1. The key in this object should directly refer to a pref, not including the
-  //    prefix (so "feeds.newtabinit" refers to the
-  //    "browser.newtabpage.activity-stream.feeds.newtabinit" pref)
-  // 2. The value should be a function that returns a feed.
+const PREFS_CONFIG = [
+  // When you add a feed pref here:
+  // 1. The pref should be prefixed with "feeds."
+  // 2. The init property should be a function that instantiates your Feed
   // 3. You should use XPCOMUtils.defineLazyModuleGetter to import the Feed,
   //    so it isn't loaded until the feed is enabled.
-  "feeds.localization": () => new LocalizationFeed(),
-  "feeds.newtabinit": () => new NewTabInit(),
-  "feeds.places": () => new PlacesFeed(),
-  "feeds.search": () => new SearchFeed(),
-  "feeds.telemetry": () => new TelemetryFeed(),
-  "feeds.topsites": () => new TopSitesFeed()
-};
+  {
+    name: "feeds.localization",
+    title: "Initialize strings and detect locale for Activity Stream",
+    value: true,
+    init: () => new LocalizationFeed()
+  },
+  {
+    name: "feeds.newtabinit",
+    title: "Sends a copy of the state to each new tab that is opened",
+    value: true,
+    init: () => new NewTabInit()
+  },
+  {
+    name: "feeds.places",
+    title: "Listens for and relays various Places-related events",
+    value: true,
+    init: () => new PlacesFeed()
+  },
+  {
+    name: "feeds.telemetry",
+    title: "Relays telemetry-related actions to TelemetrySender",
+    value: true,
+    init: () => new TelemetryFeed()
+  },
+  {
+    name: "feeds.topsites",
+    title: "Queries places and gets metadata for Top Sites section",
+    value: true,
+    init: () => new TopSitesFeed()
+  },
+  // End feeds
+
+  {
+    name: "telemetry",
+    title: "Enable system error and usage data collection",
+    value: false
+  },
+  {
+    name: "telemetry.log",
+    title: "Log telemetry events in the console",
+    value: false
+  },
+  {
+    name: "telemetry.ping.endpoint",
+    title: "Telemetry server endpoint",
+    value: "https://tiles.services.mozilla.com/v3/links/activity-stream"
+  }
+];
+
+const feeds = {};
+for (const pref of PREFS_CONFIG) {
+  if (pref.name.match(/^feeds\./)) {
+    feeds[pref.name] = pref.init;
+  }
+}
 
 this.ActivityStream = class ActivityStream {
 
   /**
    * constructor - Initializes an instance of ActivityStream
    *
    * @param  {object} options Options for the ActivityStream instance
    * @param  {string} options.id Add-on ID. e.g. "activity-stream@mozilla.org".
    * @param  {string} options.version Version of the add-on. e.g. "0.1.0"
    * @param  {string} options.newTabURL URL of New Tab page on which A.S. is displayed. e.g. "about:newtab"
    */
   constructor(options = {}) {
     this.initialized = false;
     this.options = options;
     this.store = new Store();
     this.feeds = feeds;
+    this._defaultPrefs = new DefaultPrefs(PREFS_CONFIG);
   }
   init() {
     this.initialized = true;
+    this._defaultPrefs.init();
     this.store.init(this.feeds);
     this.store.dispatch({
       type: at.INIT,
       data: {version: this.options.version}
     });
   }
   uninit() {
     this.store.dispatch({type: at.UNINIT});
     this.store.uninit();
+
     this.initialized = false;
   }
+  uninstall(reason) {
+    if (reason === REASON_ADDON_UNINSTALL) {
+      // This resets all prefs in the config to their default values,
+      // so we DON'T want to do this on an upgrade/downgrade, only on a
+      // real uninstall
+      this._defaultPrefs.reset();
+    }
+  }
 };
 
 this.EXPORTED_SYMBOLS = ["ActivityStream"];
--- a/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
+++ b/browser/extensions/activity-stream/lib/ActivityStreamMessageChannel.jsm
@@ -1,12 +1,11 @@
 /* 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/. */
-/* globals AboutNewTab, RemotePages, XPCOMUtils */
 
 "use strict";
 
 const {utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 const {
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/lib/ActivityStreamPrefs.jsm
@@ -0,0 +1,82 @@
+/* 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/. */
+"use strict";
+
+const {utils: Cu} = Components;
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const ACTIVITY_STREAM_PREF_BRANCH = "browser.newtabpage.activity-stream.";
+
+this.Prefs = class Prefs extends Preferences {
+
+  /**
+   * Prefs - A wrapper around Preferences that always sets the branch to
+   *         ACTIVITY_STREAM_PREF_BRANCH
+   */
+  constructor(branch = ACTIVITY_STREAM_PREF_BRANCH) {
+    super({branch});
+    this._branchName = branch;
+  }
+  get branchName() {
+    return this._branchName;
+  }
+};
+
+this.DefaultPrefs = class DefaultPrefs {
+
+  /**
+   * DefaultPrefs - A helper for setting and resetting default prefs for the add-on
+   *
+   * @param  {Array} config An array of configuration objects with the following properties:
+   *         {string} .name The name of the pref
+   *         {string} .title (optional) A description of the pref
+   *         {bool|string|number} .value The default value for the pref
+   * @param  {string} branch (optional) The pref branch (defaults to ACTIVITY_STREAM_PREF_BRANCH)
+   */
+  constructor(config, branch = ACTIVITY_STREAM_PREF_BRANCH) {
+    this._config = config;
+    this.branch = Services.prefs.getDefaultBranch(branch);
+  }
+
+  /**
+   * _setDefaultPref - Sets the default value (not user-defined) for a given pref
+   *
+   * @param  {string} key The name of the pref
+   * @param  {type} val The default value of the pref
+   */
+  _setDefaultPref(key, val) {
+    switch (typeof val) {
+      case "boolean":
+        this.branch.setBoolPref(key, val);
+        break;
+      case "number":
+        this.branch.setIntPref(key, val);
+        break;
+      case "string":
+        this.branch.setStringPref(key, val);
+        break;
+    }
+  }
+
+  /**
+   * init - Set default prefs for all prefs in the config
+   */
+  init() {
+    for (const pref of this._config) {
+      this._setDefaultPref(pref.name, pref.value);
+    }
+  }
+
+  /**
+   * reset - Resets all user-defined prefs for prefs in ._config to their defaults
+   */
+  reset() {
+    for (const pref of this._config) {
+      this.branch.clearUserPref(pref.name);
+    }
+  }
+};
+
+this.EXPORTED_SYMBOLS = ["DefaultPrefs", "Prefs"];
--- a/browser/extensions/activity-stream/lib/LocalizationFeed.jsm
+++ b/browser/extensions/activity-stream/lib/LocalizationFeed.jsm
@@ -1,12 +1,11 @@
 /* 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/. */
- /* globals Services, XPCOMUtils */
 "use strict";
 
 const {utils: Cu} = Components;
 const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 Cu.importGlobalProperties(["fetch"]);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
--- a/browser/extensions/activity-stream/lib/PlacesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/PlacesFeed.jsm
@@ -1,12 +1,11 @@
 /* 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/. */
- /* globals ContentSearch, XPCOMUtils, PlacesUtils, NewTabUtils, Services */
 "use strict";
 
 const {utils: Cu, interfaces: Ci} = Components;
 const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
deleted file mode 100644
--- a/browser/extensions/activity-stream/lib/SearchFeed.jsm
+++ /dev/null
@@ -1,77 +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/. */
- /* globals ContentSearch, XPCOMUtils, Services */
-"use strict";
-
-const {utils: Cu} = Components;
-const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
-const SEARCH_ENGINE_TOPIC = "browser-search-engine-modified";
-
-Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-
-XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
-  "resource:///modules/ContentSearch.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Services",
-  "resource://gre/modules/Services.jsm");
-
-this.SearchFeed = class SearchFeed {
-  addObservers() {
-    Services.obs.addObserver(this, SEARCH_ENGINE_TOPIC);
-
-    // Notice when ContentSearch.init would be lazily loaded from nsBrowserGlue
-    this.contentSearch = new Promise(resolve => Services.mm.addMessageListener(
-      "ContentSearch", (this._onMessage = () => {
-        Services.mm.removeMessageListener("ContentSearch", this._onMessage);
-        resolve(ContentSearch);
-      })));
-  }
-  removeObservers() {
-    Services.obs.removeObserver(this, SEARCH_ENGINE_TOPIC);
-    Services.mm.removeMessageListener("ContentSearch", this._onMessage);
-  }
-
-  observe(subject, topic, data) {
-    switch (topic) {
-      case SEARCH_ENGINE_TOPIC:
-        if (data !== "engine-default") {
-          this.getState();
-        }
-        break;
-    }
-  }
-
-  async getState() {
-    // Wait for ContentSearch to be lazily loaded before getting state
-    const state = await (await this.contentSearch).currentStateObj(true);
-    const engines = state.engines.map(engine => ({
-      name: engine.name,
-      icon: engine.iconBuffer
-    }));
-    const currentEngine = {
-      name: state.currentEngine.name,
-      icon: state.currentEngine.iconBuffer
-    };
-    const action = {type: at.SEARCH_STATE_UPDATED, data: {engines, currentEngine}};
-    this.store.dispatch(ac.BroadcastToContent(action));
-  }
-  performSearch(browser, data) {
-    ContentSearch.performSearch({target: browser}, data);
-  }
-
-  async onAction(action) {
-    switch (action.type) {
-      case at.INIT:
-        this.addObservers();
-        await this.getState();
-        break;
-      case at.PERFORM_SEARCH:
-        this.performSearch(action._target.browser, action.data);
-        break;
-      case at.UNINIT:
-        this.removeObservers();
-        break;
-    }
-  }
-};
-this.EXPORTED_SYMBOLS = ["SearchFeed"];
--- a/browser/extensions/activity-stream/lib/Store.jsm
+++ b/browser/extensions/activity-stream/lib/Store.jsm
@@ -1,22 +1,22 @@
 /* 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/. */
-/* global Preferences */
 "use strict";
 
 const {utils: Cu} = Components;
 
 const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
 const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
 const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-const PREF_PREFIX = "browser.newtabpage.activity-stream.";
-Cu.import("resource://gre/modules/Preferences.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Prefs",
+  "resource://activity-stream/lib/ActivityStreamPrefs.jsm");
 
 /**
  * Store - This has a similar structure to a redux store, but includes some extra
  *         functionality to allow for routing of actions between the Main processes
  *         and child processes via a ActivityStreamMessageChannel.
  *         It also accepts an array of "Feeds" on inititalization, which
  *         can listen for any action that is dispatched through the store.
  */
@@ -32,16 +32,17 @@ this.Store = class Store {
     // store.dispatch() will call store._store.dispatch();
     ["dispatch", "getState", "subscribe"].forEach(method => {
       this[method] = function(...args) {
         return this._store[method](...args);
       }.bind(this);
     });
     this.feeds = new Map();
     this._feedFactories = null;
+    this._prefs = new Prefs();
     this._prefHandlers = new Map();
     this._messageChannel = new ActivityStreamMessageChannel({dispatch: this.dispatch});
     this._store = redux.createStore(
       redux.combineReducers(reducers),
       redux.applyMiddleware(this._middleware, this._messageChannel.middleware)
     );
   }
 
@@ -89,62 +90,56 @@ this.Store = class Store {
   /**
    * maybeStartFeedAndListenForPrefChanges - Listen for pref changes that turn a
    *     feed off/on, and as long as that pref was not explicitly set to
    *     false, initialize the feed immediately.
    *
    * @param  {string} name The name of a feed, as defined in the object passed
    *                       to Store.init
    */
-  maybeStartFeedAndListenForPrefChanges(name) {
-    const prefName = PREF_PREFIX + name;
-
-    // If the pref was never set, set it to true by default.
-    if (!Preferences.has(prefName)) {
-      Preferences.set(prefName, true);
-    }
-
+  maybeStartFeedAndListenForPrefChanges(prefName) {
     // Create a listener that turns the feed off/on based on changes
     // to the pref, and cache it so we can unlisten on shut-down.
-    const onPrefChanged = isEnabled => (isEnabled ? this.initFeed(name) : this.uninitFeed(name));
+    const onPrefChanged = isEnabled => (isEnabled ? this.initFeed(prefName) : this.uninitFeed(prefName));
     this._prefHandlers.set(prefName, onPrefChanged);
-    Preferences.observe(prefName, onPrefChanged);
+    this._prefs.observe(prefName, onPrefChanged);
 
     // TODO: This should propbably be done in a generic pref manager for Activity Stream.
     // If the pref is true, start the feed immediately.
-    if (Preferences.get(prefName)) {
-      this.initFeed(name);
+    if (this._prefs.get(prefName)) {
+      this.initFeed(prefName);
     }
   }
 
   /**
    * init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
    *
-   * @param  {array} feeds An array of objects with an optional .onAction method
+   * @param  {array} feedConstructors An array of configuration objects for feeds
+   *                 each with .name (the name of the pref for the feed) and .init,
+   *                 a function that returns an instance of the feed
    */
   init(feedConstructors) {
     if (feedConstructors) {
       this._feedFactories = feedConstructors;
-      for (const name of Object.keys(feedConstructors)) {
-        this.maybeStartFeedAndListenForPrefChanges(name);
+      for (const pref of Object.keys(feedConstructors)) {
+        this.maybeStartFeedAndListenForPrefChanges(pref);
       }
     }
     this._messageChannel.createChannel();
   }
 
   /**
    * uninit -  Uninitalizes each feed, clears them, and destroys the message
    *           manager channel.
    *
    * @return {type}  description
    */
   uninit() {
     this.feeds.forEach(feed => this.uninitFeed(feed));
-    this._prefHandlers.forEach((handler, pref) => Preferences.ignore(pref, handler));
+    this._prefHandlers.forEach((handler, pref) => this._prefs.ignore(pref, handler));
     this._prefHandlers.clear();
     this._feedFactories = null;
     this.feeds.clear();
     this._messageChannel.destroyChannel();
   }
 };
 
-this.PREF_PREFIX = PREF_PREFIX;
-this.EXPORTED_SYMBOLS = ["Store", "PREF_PREFIX"];
+this.EXPORTED_SYMBOLS = ["Store"];
--- a/browser/extensions/activity-stream/lib/TelemetryFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TelemetryFeed.jsm
@@ -1,12 +1,11 @@
 /* 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/. */
-/* globals XPCOMUtils, gUUIDGenerator, ClientID */
 
 "use strict";
 
 const {utils: Cu} = Components;
 const {actionTypes: at, actionUtils: au} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 Cu.import("resource://gre/modules/ClientID.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
--- a/browser/extensions/activity-stream/lib/TelemetrySender.jsm
+++ b/browser/extensions/activity-stream/lib/TelemetrySender.jsm
@@ -1,12 +1,11 @@
 /* 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/. */
-/* globals Preferences, Services, XPCOMUtils */
 
 const {interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.importGlobalProperties(["fetch"]);
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/Console.jsm"); // eslint-disable-line no-console
--- a/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
+++ b/browser/extensions/activity-stream/lib/TopSitesFeed.jsm
@@ -1,12 +1,11 @@
 /* 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/. */
- /* globals NewTabUtils, PreviewProvider */
 "use strict";
 
 const {utils: Cu} = Components;
 const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
 
 Cu.import("resource://gre/modules/NewTabUtils.jsm");
 Cu.import("resource:///modules/PreviewProvider.jsm");
 
--- a/browser/extensions/activity-stream/test/.eslintrc.js
+++ b/browser/extensions/activity-stream/test/.eslintrc.js
@@ -1,11 +1,12 @@
 module.exports = {
   "env": {
     "node": true,
     "es6": true,
     "mocha": true
   },
   "globals": {
     "assert": true,
-    "sinon": true
+    "sinon": true,
+    "chai": true
   }
 };
--- a/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
+++ b/browser/extensions/activity-stream/test/functional/mochitest/.eslintrc.js
@@ -26,16 +26,17 @@ module.exports = {
     "OpenBrowserWindow": false,
     "Preferences": false,
     "registerCleanupFunction": false,
     "requestLongerTimeout": false,
     "Services": false,
     "SimpleTest": false,
     "SpecialPowers": false,
     "TestUtils": false,
+    "thisTestLeaksUncaughtRejectionsAndShouldBeFixed": false,
     "todo": false,
     "todo_is": false,
     "todo_isnot": false,
     "waitForClipboard": false,
     "waitForExplicitFinish": false,
     "waitForFocus": false
   }
 };
--- a/browser/extensions/activity-stream/test/schemas/pings.js
+++ b/browser/extensions/activity-stream/test/schemas/pings.js
@@ -1,9 +1,10 @@
 const Joi = require("joi-browser");
+const {MAIN_MESSAGE_TYPE, CONTENT_MESSAGE_TYPE} = require("common/Actions.jsm");
 
 const baseKeys = {
   client_id: Joi.string().required(),
   addon_version: Joi.string().required(),
   locale: Joi.string().required(),
   session_id: Joi.string(),
   page: Joi.valid(["about:home", "about:newtab"])
 };
@@ -16,16 +17,39 @@ const UserEventPing = Joi.object().keys(
   source: Joi.string().required(),
   event: Joi.string().required(),
   action: Joi.valid("activity_stream_user_event").required(),
   metadata_source: Joi.string(),
   highlight_type: Joi.valid(["bookmarks", "recommendation", "history"]),
   recommender_type: Joi.string()
 }));
 
+// Use this to validate actions generated from Redux
+const UserEventAction = Joi.object().keys({
+  type: Joi.string().required(),
+  data: Joi.object().keys({
+    event: Joi.valid([
+      "CLICK",
+      "SEARCH",
+      "BLOCK",
+      "DELETE",
+      "OPEN_NEW_WINDOW",
+      "OPEN_PRIVATE_WINDOW",
+      "BOOKMARK_DELETE",
+      "BOOKMARK_ADD"
+    ]).required(),
+    source: Joi.valid(["TOP_SITES"]),
+    action_position: Joi.number().integer()
+  }).required(),
+  meta: Joi.object().keys({
+    to: Joi.valid(MAIN_MESSAGE_TYPE).required(),
+    from: Joi.valid(CONTENT_MESSAGE_TYPE).required()
+  }).required()
+});
+
 const UndesiredPing = Joi.object().keys(Object.assign({}, baseKeys, {
   source: Joi.string().required(),
   event: Joi.string().required(),
   action: Joi.valid("activity_stream_undesired_event").required(),
   value: Joi.number().required()
 }));
 
 const PerfPing = Joi.object().keys(Object.assign({}, baseKeys, {
@@ -37,21 +61,53 @@ const PerfPing = Joi.object().keys(Objec
 
 const SessionPing = Joi.object().keys(Object.assign({}, baseKeys, {
   session_id: baseKeys.session_id.required(),
   page: baseKeys.page.required(),
   session_duration: Joi.number().integer().required(),
   action: Joi.valid("activity_stream_session").required()
 }));
 
-function assertMatchesSchema(ping, schema) {
-  assert.isNull(Joi.validate(ping, schema).error);
+function chaiAssertions(_chai, utils) {
+  const {Assertion} = _chai;
+
+  Assertion.addMethod("validate", function(schema, schemaName) {
+    const {error} = Joi.validate(this._obj, schema);
+    this.assert(
+      !error,
+      `Expected to be ${schemaName ? `a valid ${schemaName}` : "valid"} but there were errors: ${error}`
+    );
+  });
+
+  const assertions = {
+    /**
+     * assert.validate - Validates an item given a Joi schema
+     *
+     * @param  {any} actual The item to validate
+     * @param  {obj} schema A Joi schema
+     */
+    validate(actual, schema, schemaName) {
+      new Assertion(actual).validate(schema, schemaName);
+    },
+
+    /**
+     * isUserEventAction - Passes if the item is a valid UserEvent action
+     *
+     * @param  {any} actual The item to validate
+     */
+    isUserEventAction(actual) {
+      new Assertion(actual).validate(UserEventAction, "UserEventAction");
+    }
+  };
+
+  Object.assign(_chai.assert, assertions);
 }
 
 module.exports = {
   baseKeys,
   BasePing,
   UndesiredPing,
   UserEventPing,
+  UserEventAction,
   PerfPing,
   SessionPing,
-  assertMatchesSchema
+  chaiAssertions
 };
--- a/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
+++ b/browser/extensions/activity-stream/test/unit/common/Reducers.test.js
@@ -1,10 +1,10 @@
 const {reducers, INITIAL_STATE} = require("common/Reducers.jsm");
-const {TopSites, Search, App} = reducers;
+const {TopSites, App} = reducers;
 const {actionTypes: at} = require("common/Actions.jsm");
 
 describe("Reducers", () => {
   describe("App", () => {
     it("should return the initial state", () => {
       const nextState = App(undefined, {type: "FOO"});
       assert.equal(nextState, INITIAL_STATE.App);
     });
@@ -103,27 +103,9 @@ describe("Reducers", () => {
       events.forEach(event => {
         const oldState = {rows: [{url: "foo.com"}, {url: "bar.com"}]};
         const action = {type: event, data: {url: "bar.com"}};
         const nextState = TopSites(oldState, action);
         assert.deepEqual(nextState.rows, [{url: "foo.com"}]);
       });
     });
   });
-  describe("Search", () => {
-    it("should return the initial state", () => {
-      const nextState = Search(undefined, {type: "FOO"});
-      assert.equal(nextState, INITIAL_STATE.Search);
-    });
-    it("should not update state for empty action.data on Search", () => {
-      const nextState = Search(undefined, {type: at.SEARCH_STATE_UPDATED});
-      assert.equal(nextState, INITIAL_STATE.Search);
-    });
-    it("should update the current engine and the engines on SEARCH_STATE_UPDATED", () => {
-      const newEngine = {name: "Google", iconBuffer: "icon.ico"};
-      const nextState = Search(undefined, {type: at.SEARCH_STATE_UPDATED, data: {currentEngine: newEngine, engines: [newEngine]}});
-      assert.equal(nextState.currentEngine.name, newEngine.name);
-      assert.equal(nextState.currentEngine.icon, newEngine.icon);
-      assert.equal(nextState.engines[0].name, newEngine.name);
-      assert.equal(nextState.engines[0].icon, newEngine.icon);
-    });
-  });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStream.test.js
@@ -1,55 +1,60 @@
 const injector = require("inject!lib/ActivityStream.jsm");
 
+const REASON_ADDON_UNINSTALL = 6;
+
 describe("ActivityStream", () => {
   let sandbox;
   let as;
   let ActivityStream;
   function Fake() {}
-  before(() => {
+
+  beforeEach(() => {
     sandbox = sinon.sandbox.create();
     ({ActivityStream} = injector({
       "lib/LocalizationFeed.jsm": {LocalizationFeed: Fake},
       "lib/NewTabInit.jsm": {NewTabInit: Fake},
       "lib/PlacesFeed.jsm": {PlacesFeed: Fake},
-      "lib/SearchFeed.jsm": {SearchFeed: Fake},
       "lib/TelemetryFeed.jsm": {TelemetryFeed: Fake},
       "lib/TopSitesFeed.jsm": {TopSitesFeed: Fake}
     }));
+    as = new ActivityStream();
+    sandbox.stub(as.store, "init");
+    sandbox.stub(as.store, "uninit");
+    sandbox.stub(as._defaultPrefs, "init");
+    sandbox.stub(as._defaultPrefs, "reset");
   });
 
   afterEach(() => sandbox.restore());
 
-  beforeEach(() => {
-    as = new ActivityStream();
-    sandbox.stub(as.store, "init");
-    sandbox.stub(as.store, "uninit");
-  });
-
   it("should exist", () => {
     assert.ok(ActivityStream);
   });
   it("should initialize with .initialized=false", () => {
     assert.isFalse(as.initialized, ".initialized");
   });
   describe("#init", () => {
     beforeEach(() => {
       as.init();
     });
+    it("should initialize default prefs", () => {
+      assert.calledOnce(as._defaultPrefs.init);
+    });
     it("should set .initialized to true", () => {
       assert.isTrue(as.initialized, ".initialized");
     });
     it("should call .store.init", () => {
       assert.calledOnce(as.store.init);
     });
     it("should emit an INIT event with the right version", () => {
       as = new ActivityStream({version: "1.2.3"});
       sandbox.stub(as.store, "init");
       sandbox.stub(as.store, "dispatch");
+      sandbox.stub(as._defaultPrefs, "init");
 
       as.init();
 
       assert.calledOnce(as.store.dispatch);
       const action = as.store.dispatch.firstCall.args[0];
       assert.propertyVal(action.data, "version", "1.2.3");
     });
   });
@@ -60,16 +65,26 @@ describe("ActivityStream", () => {
     });
     it("should set .initialized to false", () => {
       assert.isFalse(as.initialized, ".initialized");
     });
     it("should call .store.uninit", () => {
       assert.calledOnce(as.store.uninit);
     });
   });
+  describe("#uninstall", () => {
+    it("should reset default prefs if the reason is REASON_ADDON_UNINSTALL", () => {
+      as.uninstall(REASON_ADDON_UNINSTALL);
+      assert.calledOnce(as._defaultPrefs.reset);
+    });
+    it("should not reset default prefs if the reason is something else", () => {
+      as.uninstall("foo");
+      assert.notCalled(as._defaultPrefs.reset);
+    });
+  });
   describe("feeds", () => {
     it("should create a Localization feed", () => {
       const feed = as.feeds["feeds.localization"]();
       assert.instanceOf(feed, Fake);
     });
     it("should create a NewTabInit feed", () => {
       const feed = as.feeds["feeds.newtabinit"]();
       assert.instanceOf(feed, Fake);
@@ -81,14 +96,10 @@ describe("ActivityStream", () => {
     it("should create a TopSites feed", () => {
       const feed = as.feeds["feeds.topsites"]();
       assert.instanceOf(feed, Fake);
     });
     it("should create a Telemetry feed", () => {
       const feed = as.feeds["feeds.telemetry"]();
       assert.instanceOf(feed, Fake);
     });
-    it("should create a Search feed", () => {
-      const feed = as.feeds["feeds.search"]();
-      assert.instanceOf(feed, Fake);
-    });
   });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStreamMessageChannel.test.js
@@ -4,38 +4,35 @@ const {createStore, applyMiddleware} = r
 const {actionTypes: at, actionCreators: ac} = require("common/Actions.jsm");
 
 const OPTIONS = ["pageURL, outgoingMessageName", "incomingMessageName", "dispatch"];
 
 describe("ActivityStreamMessageChannel", () => {
   let globals;
   let dispatch;
   let mm;
-  before(() => {
+  beforeEach(() => {
     function RP(url) {
       this.url = url;
       this.messagePorts = [];
       this.addMessageListener = globals.sandbox.spy();
       this.sendAsyncMessage = globals.sandbox.spy();
       this.destroy = globals.sandbox.spy();
     }
     globals = new GlobalOverrider();
     globals.set("AboutNewTab", {
       override: globals.sandbox.spy(),
       reset: globals.sandbox.spy()
     });
     globals.set("RemotePages", RP);
     dispatch = globals.sandbox.spy();
-  });
-  beforeEach(() => {
     mm = new ActivityStreamMessageChannel({dispatch});
   });
 
-  afterEach(() => globals.reset());
-  after(() => globals.restore());
+  afterEach(() => globals.restore());
 
   it("should exist", () => {
     assert.ok(ActivityStreamMessageChannel);
   });
   it("should apply default options", () => {
     mm = new ActivityStreamMessageChannel();
     OPTIONS.forEach(o => assert.equal(mm[o], DEFAULT_OPTIONS[o], o));
   });
@@ -126,16 +123,22 @@ describe("ActivityStreamMessageChannel",
       it("should dispatch a NEW_TAB_UNLOAD action", () => {
         const t = {portID: "foo"};
         sinon.stub(mm, "onActionFromContent");
         mm.onNewTabUnload({target: t});
         assert.calledWith(mm.onActionFromContent, {type: at.NEW_TAB_UNLOAD}, "foo");
       });
     });
     describe("#onMessage", () => {
+      let sandbox;
+      beforeEach(() => {
+        sandbox = sinon.sandbox.create();
+        sandbox.spy(global.Components.utils, "reportError");
+      });
+      afterEach(() => sandbox.restore());
       it("should report an error if the msg.data is missing", () => {
         mm.onMessage({target: {portID: "foo"}});
         assert.calledOnce(global.Components.utils.reportError);
       });
       it("should report an error if the msg.data.type is missing", () => {
         mm.onMessage({target: {portID: "foo"}, data: "foo"});
         assert.calledOnce(global.Components.utils.reportError);
       });
new file mode 100644
--- /dev/null
+++ b/browser/extensions/activity-stream/test/unit/lib/ActivityStreamPrefs.test.js
@@ -0,0 +1,63 @@
+const ACTIVITY_STREAM_PREF_BRANCH = "browser.newtabpage.activity-stream.";
+const {Prefs, DefaultPrefs} = require("lib/ActivityStreamPrefs.jsm");
+
+const TEST_PREF_CONFIG = [
+  {name: "foo", value: true},
+  {name: "bar", value: "BAR"},
+  {name: "baz", value: 1}
+];
+
+describe("ActivityStreamPrefs", () => {
+  describe("Prefs", () => {
+    it("should have get, set, and observe methods", () => {
+      const p = new Prefs();
+      assert.property(p, "get");
+      assert.property(p, "set");
+      assert.property(p, "observe");
+    });
+    describe(".branchName", () => {
+      it("should return the activity stream branch by default", () => {
+        const p = new Prefs();
+        assert.equal(p.branchName, ACTIVITY_STREAM_PREF_BRANCH);
+      });
+      it("should return the custom branch name if it was passed to the constructor", () => {
+        const p = new Prefs("foo");
+        assert.equal(p.branchName, "foo");
+      });
+    });
+  });
+
+  describe("DefaultPrefs", () => {
+    describe("#init", () => {
+      let defaultPrefs;
+      beforeEach(() => {
+        defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG);
+        sinon.spy(defaultPrefs.branch, "setBoolPref");
+        sinon.spy(defaultPrefs.branch, "setStringPref");
+        sinon.spy(defaultPrefs.branch, "setIntPref");
+      });
+      it("should initialize a boolean pref", () => {
+        defaultPrefs.init();
+        assert.calledWith(defaultPrefs.branch.setBoolPref, "foo", true);
+      });
+      it("should initialize a string pref", () => {
+        defaultPrefs.init();
+        assert.calledWith(defaultPrefs.branch.setStringPref, "bar", "BAR");
+      });
+      it("should initialize a integer pref", () => {
+        defaultPrefs.init();
+        assert.calledWith(defaultPrefs.branch.setIntPref, "baz", 1);
+      });
+    });
+    describe("#reset", () => {
+      it("should clear user preferences for each pref in the config", () => {
+        const defaultPrefs = new DefaultPrefs(TEST_PREF_CONFIG);
+        sinon.spy(defaultPrefs.branch, "clearUserPref");
+        defaultPrefs.reset();
+        for (const pref of TEST_PREF_CONFIG) {
+          assert.calledWith(defaultPrefs.branch.clearUserPref, pref.name);
+        }
+      });
+    });
+  });
+});
--- a/browser/extensions/activity-stream/test/unit/lib/LocalizationFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/LocalizationFeed.test.js
@@ -14,29 +14,32 @@ const TEST_STRINGS = {
     too: "Boo"
   },
   "ru": {foo: "Baz"}
 };
 
 describe("Localization Feed", () => {
   let feed;
   let globals;
-  before(() => {
+  let sandbox;
+  beforeEach(() => {
     globals = new GlobalOverrider();
-  });
-  beforeEach(() => {
+    sandbox = globals.sandbox;
     feed = new LocalizationFeed();
     feed.store = {dispatch: sinon.spy()};
+
+    sandbox.stub(global.Services.locale, "getRequestedLocale");
   });
   afterEach(() => {
     globals.restore();
   });
 
   it("should fetch strings on init", async () => {
-    sinon.stub(feed, "updateLocale");
+    sandbox.stub(feed, "updateLocale");
+    sandbox.stub(global, "fetch");
     fetch.returns(Promise.resolve({json() { return Promise.resolve(TEST_STRINGS); }}));
 
     await feed.init();
 
     assert.deepEqual(feed.allStrings, TEST_STRINGS);
     assert.calledOnce(feed.updateLocale);
   });
 
@@ -110,19 +113,23 @@ describe("Localization Feed", () => {
       feed.observe(null, "some-other-notification");
 
       assert.notCalled(feed.updateLocale);
     });
   });
 
   describe("#onAction", () => {
     it("should addObserver on INIT", () => {
+      const stub = sandbox.stub(global.Services.obs, "addObserver");
+
       feed.onAction({type: at.INIT});
 
-      assert.calledOnce(global.Services.obs.addObserver);
+      assert.calledOnce(stub);
     });
     it("should removeObserver on UNINIT", () => {
+      const stub = sandbox.stub(global.Services.obs, "removeObserver");
+
       feed.onAction({type: at.UNINIT});
 
-      assert.calledOnce(global.Services.obs.removeObserver);
+      assert.calledOnce(stub);
     });
   });
 });
--- a/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/PlacesFeed.test.js
@@ -23,16 +23,19 @@ describe("PlacesFeed", () => {
         deleteHistoryEntry: sandbox.spy(),
         blockURL: sandbox.spy()
       }
     });
     globals.set("PlacesUtils", {
       history: {addObserver: sandbox.spy(), removeObserver: sandbox.spy()},
       bookmarks: {TYPE_BOOKMARK, addObserver: sandbox.spy(), removeObserver: sandbox.spy()}
     });
+    sandbox.spy(global.Services.obs, "addObserver");
+    sandbox.spy(global.Services.obs, "removeObserver");
+    sandbox.spy(global.Components.utils, "reportError");
 
     feed = new PlacesFeed();
     feed.store = {dispatch: sinon.spy()};
   });
   afterEach(() => globals.restore());
 
   it("should have a HistoryObserver that dispatches to the store", () => {
     assert.instanceOf(feed.historyObserver, HistoryObserver);
deleted file mode 100644
--- a/browser/extensions/activity-stream/test/unit/lib/SearchFeed.test.js
+++ /dev/null
@@ -1,84 +0,0 @@
-"use strict";
-const {SearchFeed} = require("lib/SearchFeed.jsm");
-const {GlobalOverrider} = require("test/unit/utils");
-const {actionTypes: at} = require("common/Actions.jsm");
-const fakeEngines = [{name: "Google", iconBuffer: "icon.ico"}];
-describe("Search Feed", () => {
-  let feed;
-  let globals;
-  before(() => {
-    globals = new GlobalOverrider();
-    globals.set("ContentSearch", {
-      currentStateObj: globals.sandbox.spy(() => Promise.resolve({engines: fakeEngines, currentEngine: {}})),
-      performSearch: globals.sandbox.spy((browser, searchData) => Promise.resolve({browser, searchData}))
-    });
-  });
-  beforeEach(() => {
-    feed = new SearchFeed();
-    feed.store = {dispatch: sinon.spy()};
-  });
-  afterEach(() => globals.reset());
-  after(() => globals.restore());
-
-  it("should call get state (with true) from the content search provider on INIT", async() => {
-    await feed.onAction({type: at.INIT});
-
-    // calling currentStateObj with 'true' allows us to return a data uri for the
-    // icon, instead of an array buffer
-    assert.calledWith(global.ContentSearch.currentStateObj, true);
-  });
-  it("should get the the state on INIT", () => {
-    sinon.stub(feed, "getState");
-    feed.onAction({type: at.INIT});
-    assert.calledOnce(feed.getState);
-  });
-  it("should add observers on INIT", () => {
-    sinon.stub(feed, "addObservers");
-    feed.onAction({type: at.INIT});
-    assert.calledOnce(feed.addObservers);
-  });
-  it("should remove observers on UNINIT", () => {
-    sinon.stub(feed, "removeObservers");
-    feed.onAction({type: at.UNINIT});
-    assert.calledOnce(feed.removeObservers);
-  });
-  it("should add event handlers on INIT", () => {
-    feed.onAction({type: at.INIT});
-
-    assert.calledOnce(global.Services.obs.addObserver);
-    assert.calledOnce(global.Services.mm.addMessageListener);
-  });
-  it("should remove event handlers on UNINIT", () => {
-    feed.onAction({type: at.UNINIT});
-
-    assert.calledOnce(global.Services.obs.removeObserver);
-    assert.calledOnce(global.Services.mm.removeMessageListener);
-  });
-  it("should dispatch one event with the state", async() => {
-    feed.contentSearch = Promise.resolve(global.ContentSearch);
-
-    await feed.getState();
-
-    assert.calledOnce(feed.store.dispatch);
-  });
-  it("should perform a search on PERFORM_SEARCH", () => {
-    sinon.stub(feed, "performSearch");
-    feed.onAction({_target: {browser: {}}, type: at.PERFORM_SEARCH});
-    assert.calledOnce(feed.performSearch);
-  });
-  it("should call performSearch with an action", () => {
-    const action = {_target: {browser: "browser"}, data: {searchString: "hello"}};
-    feed.performSearch(action._target.browser, action.data);
-    assert.calledWith(global.ContentSearch.performSearch, {target: action._target.browser}, action.data);
-  });
-  it("should get the state if we change the search engines", () => {
-    sinon.stub(feed, "getState");
-    feed.observe(null, "browser-search-engine-modified", "engine-current");
-    assert.calledOnce(feed.getState);
-  });
-  it("shouldn't get the state if it's not the right notification", () => {
-    sinon.stub(feed, "getState");
-    feed.observe(null, "some-other-notification", "engine-current");
-    assert.notCalled(feed.getState);
-  });
-});
--- a/browser/extensions/activity-stream/test/unit/lib/Store.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/Store.test.js
@@ -1,40 +1,34 @@
 const injector = require("inject!lib/Store.jsm");
 const {createStore} = require("redux");
 const {addNumberReducer} = require("test/unit/utils");
-const {GlobalOverrider} = require("test/unit/utils");
+const {FakePrefs} = require("test/unit/utils");
 describe("Store", () => {
   let Store;
-  let Preferences;
   let sandbox;
   let store;
-  let globals;
-  let PREF_PREFIX;
   beforeEach(() => {
-    globals = new GlobalOverrider();
-    sandbox = globals.sandbox;
-    Preferences = new Map();
-    Preferences.observe = sandbox.spy();
-    Preferences.ignore = sandbox.spy();
-    globals.set("Preferences", Preferences);
+    sandbox = sinon.sandbox.create();
     function ActivityStreamMessageChannel(options) {
       this.dispatch = options.dispatch;
       this.createChannel = sandbox.spy();
       this.destroyChannel = sandbox.spy();
       this.middleware = sandbox.spy(s => next => action => next(action));
     }
-    ({Store, PREF_PREFIX} = injector({"lib/ActivityStreamMessageChannel.jsm": {ActivityStreamMessageChannel}}));
+    ({Store} = injector({
+      "lib/ActivityStreamMessageChannel.jsm": {ActivityStreamMessageChannel},
+      "lib/ActivityStreamPrefs.jsm": {Prefs: FakePrefs}
+    }));
     store = new Store();
   });
   afterEach(() => {
-    Preferences.clear();
-    globals.restore();
+    sandbox.restore();
   });
-  it("should have an .feeds property that is a Map", () => {
+  it("should have a .feeds property that is a Map", () => {
     assert.instanceOf(store.feeds, Map);
     assert.equal(store.feeds.size, 0, ".feeds.size");
   });
   it("should have a redux store at ._store", () => {
     assert.ok(store._store);
     assert.property(store, "dispatch");
     assert.property(store, "getState");
   });
@@ -44,17 +38,17 @@ describe("Store", () => {
   });
   it("should connect the ActivityStreamMessageChannel's middleware", () => {
     store.dispatch({type: "FOO"});
     assert.calledOnce(store._messageChannel.middleware);
   });
   describe("#initFeed", () => {
     it("should add an instance of the feed to .feeds", () => {
       class Foo {}
-      Preferences.set(`${PREF_PREFIX}foo`, false);
+      store._prefs.set("foo", false);
       store.init({foo: () => new Foo()});
       store.initFeed("foo");
 
       assert.isTrue(store.feeds.has("foo"), "foo is set");
       assert.instanceOf(store.feeds.get("foo"), Foo);
     });
     it("should add a .store property to the feed", () => {
       class Foo {}
@@ -93,44 +87,36 @@ describe("Store", () => {
       assert.isFalse(store.feeds.has("foo"), "foo is not in .feeds");
     });
   });
   describe("maybeStartFeedAndListenForPrefChanges", () => {
     beforeEach(() => {
       sinon.stub(store, "initFeed");
       sinon.stub(store, "uninitFeed");
     });
-    it("should set the new pref in Preferences to true, if it was never defined", () => {
-      store.maybeStartFeedAndListenForPrefChanges("foo");
-      assert.isTrue(Preferences.get(`${PREF_PREFIX}foo`));
-    });
-    it("should not override the pref if it was already set", () => {
-      Preferences.set(`${PREF_PREFIX}foo`, false);
-      store.maybeStartFeedAndListenForPrefChanges("foo");
-      assert.isFalse(Preferences.get(`${PREF_PREFIX}foo`));
-    });
     it("should initialize the feed if the Pref is set to true", () => {
-      Preferences.set(`${PREF_PREFIX}foo`, true);
+      store._prefs.set("foo", true);
       store.maybeStartFeedAndListenForPrefChanges("foo");
       assert.calledWith(store.initFeed, "foo");
     });
     it("should not initialize the feed if the Pref is set to false", () => {
-      Preferences.set(`${PREF_PREFIX}foo`, false);
+      store._prefs.set("foo", false);
       store.maybeStartFeedAndListenForPrefChanges("foo");
       assert.notCalled(store.initFeed);
     });
     it("should observe the pref", () => {
+      sinon.stub(store._prefs, "observe");
       store.maybeStartFeedAndListenForPrefChanges("foo");
-      assert.calledWith(Preferences.observe, `${PREF_PREFIX}foo`, store._prefHandlers.get(`${PREF_PREFIX}foo`));
+      assert.calledWith(store._prefs.observe, "foo", store._prefHandlers.get("foo"));
     });
     describe("handler", () => {
       let handler;
       beforeEach(() => {
         store.maybeStartFeedAndListenForPrefChanges("foo");
-        handler = store._prefHandlers.get(`${PREF_PREFIX}foo`);
+        handler = store._prefHandlers.get("foo");
       });
       it("should initialize the feed if called with true", () => {
         handler(true);
         assert.calledWith(store.initFeed, "foo");
       });
       it("should uninitialize the feed if called with false", () => {
         handler(false);
         assert.calledWith(store.uninitFeed, "foo");
@@ -146,16 +132,19 @@ describe("Store", () => {
     });
     it("should initialize the ActivityStreamMessageChannel channel", () => {
       store.init();
       assert.calledOnce(store._messageChannel.createChannel);
     });
   });
   describe("#uninit", () => {
     it("should clear .feeds, ._prefHandlers, and ._feedFactories", () => {
+      store._prefs.set("a", true);
+      store._prefs.set("b", true);
+      store._prefs.set("c", true);
       store.init({
         a: () => ({}),
         b: () => ({}),
         c: () => ({})
       });
 
       store.uninit();
 
@@ -176,16 +165,17 @@ describe("Store", () => {
     });
   });
   describe("#dispatch", () => {
     it("should call .onAction of each feed", () => {
       const {dispatch} = store;
       const sub = {onAction: sinon.spy()};
       const action = {type: "FOO"};
 
+      store._prefs.set("sub", true);
       store.init({sub: () => sub});
 
       dispatch(action);
 
       assert.calledWith(sub.onAction, action);
     });
     it("should call the reducers", () => {
       const {dispatch} = store;
--- a/browser/extensions/activity-stream/test/unit/lib/TelemetryFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetryFeed.test.js
@@ -1,18 +1,17 @@
 const injector = require("inject!lib/TelemetryFeed.jsm");
 const {GlobalOverrider} = require("test/unit/utils");
 const {actionCreators: ac, actionTypes: at} = require("common/Actions.jsm");
 const {
   BasePing,
   UndesiredPing,
   UserEventPing,
   PerfPing,
-  SessionPing,
-  assertMatchesSchema
+  SessionPing
 } = require("test/schemas/pings");
 
 const FAKE_TELEMETRY_ID = "foo123";
 const FAKE_UUID = "{foo-123-foo}";
 
 describe("TelemetryFeed", () => {
   let globals;
   let sandbox;
@@ -98,107 +97,107 @@ describe("TelemetryFeed", () => {
       assert.calledWith(instance.sendEvent, instance.createSessionEndEvent.firstCall.returnValue);
     });
   });
   describe("ping creators", () => {
     beforeEach(async () => await instance.init());
     describe("#createPing", () => {
       it("should create a valid base ping without a session if no portID is supplied", () => {
         const ping = instance.createPing();
-        assertMatchesSchema(ping, BasePing);
+        assert.validate(ping, BasePing);
         assert.notProperty(ping, "session_id");
       });
       it("should create a valid base ping with session info if a portID is supplied", () => {
         // Add a session
         const portID = "foo";
         instance.addSession(portID);
         const sessionID = instance.sessions.get(portID).session_id;
 
         // Create a ping referencing the session
         const ping = instance.createPing(portID);
-        assertMatchesSchema(ping, BasePing);
+        assert.validate(ping, BasePing);
 
         // Make sure we added the right session-related stuff to the ping
         assert.propertyVal(ping, "session_id", sessionID);
         assert.propertyVal(ping, "page", "about:newtab");
       });
     });
     describe("#createUserEvent", () => {
       it("should create a valid event", () => {
         const portID = "foo";
         const data = {source: "TOP_SITES", event: "CLICK"};
         const action = ac.SendToMain(ac.UserEvent(data), portID);
         const session = addSession(portID);
         const ping = instance.createUserEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, UserEventPing);
+        assert.validate(ping, UserEventPing);
         // Does it have the right session_id?
         assert.propertyVal(ping, "session_id", session.session_id);
       });
     });
     describe("#createUndesiredEvent", () => {
       it("should create a valid event without a session", () => {
         const action = ac.UndesiredEvent({source: "TOP_SITES", event: "MISSING_IMAGE", value: 10});
         const ping = instance.createUndesiredEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, UndesiredPing);
+        assert.validate(ping, UndesiredPing);
         // Does it have the right value?
         assert.propertyVal(ping, "value", 10);
       });
       it("should create a valid event with a session", () => {
         const portID = "foo";
         const data = {source: "TOP_SITES", event: "MISSING_IMAGE", value: 10};
         const action = ac.SendToMain(ac.UndesiredEvent(data), portID);
         const session = addSession(portID);
         const ping = instance.createUndesiredEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, UndesiredPing);
+        assert.validate(ping, UndesiredPing);
         // Does it have the right session_id?
         assert.propertyVal(ping, "session_id", session.session_id);
         // Does it have the right value?
         assert.propertyVal(ping, "value", 10);
       });
     });
     describe("#createPerformanceEvent", () => {
       it("should create a valid event without a session", () => {
         const action = ac.PerfEvent({event: "SCREENSHOT_FINISHED", value: 100});
         const ping = instance.createPerformanceEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, PerfPing);
+        assert.validate(ping, PerfPing);
         // Does it have the right value?
         assert.propertyVal(ping, "value", 100);
       });
       it("should create a valid event with a session", () => {
         const portID = "foo";
         const data = {event: "PAGE_LOADED", value: 100};
         const action = ac.SendToMain(ac.PerfEvent(data), portID);
         const session = addSession(portID);
         const ping = instance.createPerformanceEvent(action);
 
         // Is it valid?
-        assertMatchesSchema(ping, PerfPing);
+        assert.validate(ping, PerfPing);
         // Does it have the right session_id?
         assert.propertyVal(ping, "session_id", session.session_id);
         // Does it have the right value?
         assert.propertyVal(ping, "value", 100);
       });
     });
     describe("#createSessionEndEvent", () => {
       it("should create a valid event", () => {
         const ping = instance.createSessionEndEvent({
           session_id: FAKE_UUID,
           page: "about:newtab",
           session_duration: 12345
         });
         // Is it valid?
-        assertMatchesSchema(ping, SessionPing);
+        assert.validate(ping, SessionPing);
         assert.propertyVal(ping, "session_id", FAKE_UUID);
         assert.propertyVal(ping, "page", "about:newtab");
         assert.propertyVal(ping, "session_duration", 12345);
       });
     });
   });
   describe("#sendEvent", () => {
     it("should call telemetrySender", async () => {
--- a/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TelemetrySender.test.js
@@ -13,41 +13,38 @@ let fakePrefs;
 const prefInitHook = function() {
   fakePrefs = this; // eslint-disable-line consistent-this
 };
 const tsArgs = {prefInitHook};
 
 describe("TelemetrySender", () => {
   let globals;
   let tSender;
+  let sandbox;
   let fetchStub;
   const fakeEndpointUrl = "http://127.0.0.1/stuff";
   const fakePingJSON = JSON.stringify({action: "fake_action", monkey: 1});
   const fakeFetchHttpErrorResponse = {ok: false, status: 400};
   const fakeFetchSuccessResponse = {ok: true, status: 200};
 
-  before(() => {
+  beforeEach(() => {
     globals = new GlobalOverrider();
-
-    fetchStub = globals.sandbox.stub();
+    sandbox = globals.sandbox;
+    fetchStub = sandbox.stub();
 
     globals.set("Preferences", FakePrefs);
     globals.set("fetch", fetchStub);
-  });
-
-  beforeEach(() => {
+    sandbox.spy(global.Components.utils, "reportError");
   });
 
   afterEach(() => {
-    globals.reset();
+    globals.restore();
     FakePrefs.prototype.prefs = {};
   });
 
-  after(() => globals.restore());
-
   it("should construct the Prefs object", () => {
     globals.sandbox.spy(global, "Preferences");
 
     tSender = new TelemetrySender(tsArgs);
 
     assert.calledOnce(global.Preferences);
   });
 
--- a/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/TopSitesFeed.test.js
@@ -7,21 +7,20 @@ const FAKE_LINKS = new Array(TOP_SITES_S
 const FAKE_SCREENSHOT = "data123";
 
 describe("Top Sites Feed", () => {
   let feed;
   let globals;
   let sandbox;
   let links;
   let clock;
-  before(() => {
+
+  beforeEach(() => {
     globals = new GlobalOverrider();
     sandbox = globals.sandbox;
-  });
-  beforeEach(() => {
     globals.set("NewTabUtils", {activityStreamLinks: {getTopSites: sandbox.spy(() => Promise.resolve(links))}});
     globals.set("PreviewProvider", {getThumbnail: sandbox.spy(() => Promise.resolve(FAKE_SCREENSHOT))});
     feed = new TopSitesFeed();
     feed.store = {dispatch: sinon.spy(), getState() { return {TopSites: {rows: Array(12).fill("site")}}; }};
     links = FAKE_LINKS;
     clock = sinon.useFakeTimers();
   });
   afterEach(() => {
--- a/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
+++ b/browser/extensions/activity-stream/test/unit/lib/init-store.test.js
@@ -1,25 +1,22 @@
 const initStore = require("content-src/lib/init-store");
 const {GlobalOverrider, addNumberReducer} = require("test/unit/utils");
 const {actionCreators: ac} = require("common/Actions.jsm");
 
 describe("initStore", () => {
   let globals;
   let store;
-  before(() => {
+  beforeEach(() => {
     globals = new GlobalOverrider();
     globals.set("sendAsyncMessage", globals.sandbox.spy());
     globals.set("addMessageListener", globals.sandbox.spy());
-  });
-  beforeEach(() => {
     store = initStore({number: addNumberReducer});
   });
-  afterEach(() => globals.reset());
-  after(() => globals.restore());
+  afterEach(() => globals.restore());
   it("should create a store with the provided reducers", () => {
     assert.ok(store);
     assert.property(store.getState(), "number");
   });
   it("should add a listener for incoming actions", () => {
     assert.calledWith(global.addMessageListener, initStore.INCOMING_MESSAGE_NAME);
     const callback = global.addMessageListener.firstCall.args[1];
     globals.sandbox.spy(store, "dispatch");
--- a/browser/extensions/activity-stream/test/unit/unit-entry.js
+++ b/browser/extensions/activity-stream/test/unit/unit-entry.js
@@ -1,44 +1,59 @@
-const {GlobalOverrider} = require("test/unit/utils");
+const {GlobalOverrider, FakePrefs} = require("test/unit/utils");
+const {chaiAssertions} = require("test/schemas/pings");
 
-const req = require.context(".", true, /\.test\.js$/);
+const req = require.context(".", true, /\.test\.jsx?$/);
 const files = req.keys();
 
 // This exposes sinon assertions to chai.assert
 sinon.assert.expose(assert, {prefix: ""});
 
+chai.use(chaiAssertions);
+
 let overrider = new GlobalOverrider();
 overrider.set({
   Components: {
     interfaces: {},
     utils: {
-      import: overrider.sandbox.spy(),
-      importGlobalProperties: overrider.sandbox.spy(),
-      reportError: overrider.sandbox.spy(),
+      import() {},
+      importGlobalProperties() {},
+      reportError() {},
       now: () => window.performance.now()
     }
   },
+  // eslint-disable-next-line object-shorthand
+  ContentSearchUIController: function() {}, // NB: This is a function/constructor
+  dump() {},
+  fetch() {},
+  Preferences: FakePrefs,
+  Services: {
+    locale: {getRequestedLocale() {}},
+    mm: {
+      addMessageListener: (msg, cb) => cb(),
+      removeMessageListener() {}
+    },
+    obs: {
+      addObserver() {},
+      removeObserver() {}
+    },
+    prefs: {
+      getDefaultBranch() {
+        return {
+          setBoolPref() {},
+          setIntPref() {},
+          setStringPref() {},
+          clearUserPref() {}
+        };
+      }
+    }
+  },
   XPCOMUtils: {
-    defineLazyModuleGetter: overrider.sandbox.spy(),
-    defineLazyServiceGetter: overrider.sandbox.spy(),
-    generateQI: overrider.sandbox.stub().returns(() => {})
-  },
-  dump: overrider.sandbox.spy(),
-  fetch: overrider.sandbox.stub(),
-  Services: {
-    locale: {getRequestedLocale: overrider.sandbox.stub()},
-    mm: {
-      addMessageListener: overrider.sandbox.spy((msg, cb) => cb()),
-      removeMessageListener: overrider.sandbox.spy()
-    },
-    obs: {
-      addObserver: overrider.sandbox.spy(),
-      removeObserver: overrider.sandbox.spy()
-    }
+    defineLazyModuleGetter() {},
+    defineLazyServiceGetter() {},
+    generateQI() { return {}; }
   }
 });
 
 describe("activity-stream", () => {
-  afterEach(() => overrider.reset());
   after(() => overrider.restore());
   files.forEach(file => req(file));
 });
--- a/browser/extensions/activity-stream/test/unit/utils.js
+++ b/browser/extensions/activity-stream/test/unit/utils.js
@@ -49,16 +49,17 @@ class GlobalOverrider {
    */
   set(key, value) {
     if (!value && typeof key === "object") {
       const overrides = key;
       Object.keys(overrides).forEach(k => this._override(k, overrides[k]));
     } else {
       this._override(key, value);
     }
+    return value;
   }
 
   /**
    * reset - Reset the global sandbox, so all state on spies, stubs etc. is cleared.
    *         You probably want to call this after each test.
    */
   reset() {
     this.sandbox.reset();
--- a/browser/extensions/shield-recipe-client/jar.mn
+++ b/browser/extensions/shield-recipe-client/jar.mn
@@ -1,9 +1,9 @@
 # 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/.
 
 [features/shield-recipe-client@mozilla.org] chrome.jar:
 % resource shield-recipe-client %content/
   content/lib/ (./lib/*)
-  content/data/ (./data/*)
   content/node_modules/jexl/ (./node_modules/jexl/*)
+  content/skin/  (skin/*)
--- a/browser/extensions/shield-recipe-client/lib/Heartbeat.jsm
+++ b/browser/extensions/shield-recipe-client/lib/Heartbeat.jsm
@@ -1,30 +1,55 @@
 /* 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/. */
 
 "use strict";
 
-const {utils: Cu} = Components;
+const {utils: Cu, interfaces: Ci} = Components;
 
+Cu.import("resource://gre/modules/AppConstants.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/TelemetryController.jsm");
 Cu.import("resource://gre/modules/Timer.jsm");
 Cu.import("resource://shield-recipe-client/lib/CleanupManager.jsm");
 Cu.import("resource://shield-recipe-client/lib/EventEmitter.jsm");
 Cu.import("resource://shield-recipe-client/lib/LogManager.jsm");
 
 Cu.importGlobalProperties(["URL"]); /* globals URL */
 
 this.EXPORTED_SYMBOLS = ["Heartbeat"];
 
-const log = LogManager.getLogger("heartbeat");
 const PREF_SURVEY_DURATION = "browser.uitour.surveyDuration";
 const NOTIFICATION_TIME = 3000;
+const HEARTBEAT_CSS_URI = Services.io.newURI("resource://shield-recipe-client/skin/shared/Heartbeat.css");
+const HEARTBEAT_CSS_URI_OSX = Services.io.newURI("resource://shield-recipe-client/skin/osx/Heartbeat.css");
+
+const log = LogManager.getLogger("heartbeat");
+const windowsWithInjectedCss = new WeakSet();
+let anyWindowsWithInjectedCss = false;
+
+// Add cleanup handler for CSS injected into windows by Heartbeat
+CleanupManager.addCleanupHandler(() => {
+  if (anyWindowsWithInjectedCss) {
+    const windowEnumerator = Services.wm.getEnumerator("navigator:browser");
+    while (windowEnumerator.hasMoreElements()) {
+      const window = windowEnumerator.getNext();
+      if (windowsWithInjectedCss.has(window)) {
+        const utils = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+        utils.removeSheet(HEARTBEAT_CSS_URI, window.AGENT_SHEET);
+        if (AppConstants.platform === "macosx") {
+          utils.removeSheet(HEARTBEAT_CSS_URI_OSX, window.AGENT_SHEET);
+        }
+        windowsWithInjectedCss.delete(window);
+      }
+    }
+  }
+});
 
 /**
  * Show the Heartbeat UI to request user feedback.
  *
  * @param chromeWindow
  *        The chrome window that the heartbeat notification is displayed in.
  * @param sandboxManager
  *        The manager for the sandbox this was called from. Heartbeat will
@@ -92,16 +117,26 @@ this.Heartbeat = class {
 
     this.chromeWindow = chromeWindow;
     this.eventEmitter = new EventEmitter(sandboxManager);
     this.sandboxManager = sandboxManager;
     this.options = options;
     this.surveyResults = {};
     this.buttons = null;
 
+    if (!windowsWithInjectedCss.has(chromeWindow)) {
+      windowsWithInjectedCss.add(chromeWindow);
+      const utils = chromeWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+      utils.loadSheet(HEARTBEAT_CSS_URI, chromeWindow.AGENT_SHEET);
+      if (AppConstants.platform === "macosx") {
+        utils.loadSheet(HEARTBEAT_CSS_URI_OSX, chromeWindow.AGENT_SHEET);
+      }
+      anyWindowsWithInjectedCss = true;
+    }
+
     // so event handlers are consistent
     this.handleWindowClosed = this.handleWindowClosed.bind(this);
     this.close = this.close.bind(this);
 
     if (this.options.engagementButtonLabel) {
       this.buttons = [{
         label: this.options.engagementButtonLabel,
         callback: () => {
@@ -119,17 +154,17 @@ this.Heartbeat = class {
         },
       }];
     }
 
     this.notificationBox = this.chromeWindow.document.querySelector("#high-priority-global-notificationbox");
     this.notice = this.notificationBox.appendNotification(
       this.options.message,
       "heartbeat-" + this.options.flowId,
-      "chrome://browser/skin/heartbeat-icon.svg",
+      "resource://shield-recipe-client/skin/shared/heartbeat-icon.svg",
       this.notificationBox.PRIORITY_INFO_HIGH,
       this.buttons,
       eventType => {
         if (eventType !== "removed") {
           return;
         }
         this.maybeNotifyHeartbeat("NotificationClosed");
       }
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/skin/osx/Heartbeat.css
@@ -0,0 +1,24 @@
+/* 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/. */
+
+/* Notification overrides for Heartbeat UI */
+
+notification.heartbeat {
+  background-image: linear-gradient(-179deg, #FBFBFB 0%, #EBEBEB 100%);
+  border-bottom: 1px solid #C1C1C1;
+  height: 40px;
+}
+
+/* In themes/osx/global/notification.css the close icon is inverted because notifications
+   on OSX are usually dark. Heartbeat is light, so override that behaviour. */
+
+notification.heartbeat[type="info"] .close-icon:not(:hover) {
+  -moz-image-region: rect(0, 16px, 16px, 0) !important;
+}
+
+@media (min-resolution: 2dppx) {
+  notification.heartbeat[type="info"] .close-icon:not(:hover) {
+    -moz-image-region: rect(0, 32px, 32px, 0) !important;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/skin/shared/Heartbeat.css
@@ -0,0 +1,130 @@
+/* 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/. */
+
+/* Notification overrides for Heartbeat UI */
+
+notification.heartbeat {
+  background-color: #F1F1F1;
+  border-bottom: 1px solid #C1C1C1;
+  height: 40px;
+}
+
+/* In themes/osx/global/notification.css the close icon is inverted because notifications
+   on OSX are usually dark. Heartbeat is light, so override that behaviour. */
+
+@keyframes pulse-onshow {
+  0% {
+    opacity: 0;
+    transform: scale(1);
+  }
+
+  25% {
+    opacity: 1;
+    transform: scale(1.1);
+  }
+
+  50% {
+    transform: scale(1);
+  }
+
+  75% {
+    transform: scale(1.1);
+  }
+
+  100% {
+    transform: scale(1);
+  }
+}
+
+@keyframes pulse-twice {
+  0% {
+    transform: scale(1.1);
+  }
+
+  50% {
+    transform: scale(0.8);
+  }
+
+  100% {
+    transform: scale(1);
+  }
+}
+
+.messageText.heartbeat {
+  color: #333;
+  margin-inline-end: 12px !important; /* The !important is required to override OSX default style. */
+  margin-inline-start: 0;
+  text-shadow: none;
+}
+
+.messageImage.heartbeat {
+  height: 24px;
+  margin-inline-end: 8px;
+  margin-inline-start: 8px;
+  width: 24px;
+}
+
+.messageImage.heartbeat.pulse-onshow {
+  animation-duration: 1.5s;
+  animation-iteration-count: 1;
+  animation-name: pulse-onshow;
+  animation-timing-function: cubic-bezier(0.7, 1.8, 0.9, 1.1);
+}
+
+.messageImage.heartbeat.pulse-twice {
+  animation-duration: 1s;
+  animation-iteration-count: 2;
+  animation-name: pulse-twice;
+  animation-timing-function: linear;
+}
+
+/* Learn More link styles */
+.heartbeat > .text-link {
+  color: #0095DD;
+  margin-inline-start: 0;
+}
+
+.heartbeat > .text-link:hover {
+  color: #008ACB;
+  text-decoration: none;
+}
+
+.heartbeat > .text-link:hover:active {
+  color: #006B9D;
+}
+
+/* Heartbeat UI Rating Star Classes */
+.heartbeat > #star-rating-container {
+  display: -moz-box;
+  margin-bottom: 4px;
+}
+
+.heartbeat > #star-rating-container > #star5 {
+  -moz-box-ordinal-group: 5;
+}
+
+.heartbeat > #star-rating-container > #star4 {
+  -moz-box-ordinal-group: 4;
+}
+
+.heartbeat > #star-rating-container > #star3 {
+  -moz-box-ordinal-group: 3;
+}
+
+.heartbeat > #star-rating-container > #star2 {
+  -moz-box-ordinal-group: 2;
+}
+
+.heartbeat > #star-rating-container > .star-x {
+  background: url("resource://shield-recipe-client/skin/shared/heartbeat-star-off.svg");
+  cursor: pointer;
+  height: 16px;
+  margin-inline-end: 4px !important; /* Overrides the margin-inline-end for all platforms defined in the .plain class */
+  width: 16px;
+}
+
+.heartbeat > #star-rating-container > .star-x:hover,
+.heartbeat > #star-rating-container > .star-x:hover ~ .star-x {
+  background: url("resource://shield-recipe-client/skin/shared/heartbeat-star-lit.svg");
+}
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/skin/shared/heartbeat-icon.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-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 width="288px" height="248px" viewBox="0 0 288 248" xmlns="http://www.w3.org/2000/svg">
+  <path fill="#d74345" d="M144,248.571429 C141.214272,248.571429 138.857152,247.607152 136.928571,245.678571 L36.6428571,148.928571 C35.5714232,148.071424 34.0982237,146.678581 32.2232143,144.75 C30.3482049,142.821419 27.3750204,139.312525 23.3035714,134.223214 C19.2321225,129.133903 15.5893018,123.910741 12.375,118.553571 C9.16069821,113.196402 6.29465545,106.714324 3.77678571,99.1071429 C1.25891598,91.499962 0,84.1071788 0,76.9285714 C0,53.357025 6.80350339,34.9286379 20.4107143,21.6428571 C34.0179252,8.35707643 52.8213086,1.71428571 76.8214286,1.71428571 C83.4643189,1.71428571 90.2410369,2.86605991 97.1517857,5.16964286 C104.062535,7.4732258 110.491042,10.5803376 116.4375,14.4910714 C122.383958,18.4018053 127.499979,22.0714114 131.785714,25.5 C136.07145,28.9285886 140.142838,32.5714093 144,36.4285714 C147.857162,32.5714093 151.92855,28.9285886 156.214286,25.5 C160.500021,22.0714114 165.616042,18.4018053 171.5625,14.4910714 C177.508958,10.5803376 183.937465,7.4732258 190.848214,5.16964286 C197.758963,2.86605991 204.535681,1.71428571 211.178571,1.71428571 C235.178691,1.71428571 253.982075,8.35707643 267.589286,21.6428571 C281.196497,34.9286379 288,53.357025 288,76.9285714 C288,100.607261 275.732266,124.714163 251.196429,149.25 L151.071429,245.678571 C149.142847,247.607152 146.785728,248.571429 144,248.571429 L144,248.571429 Z" transform="translate(0,-1)"/>
+  <g transform="translate(0,-0.29)">
+    <mask id="mask" fill="#fff">
+      <path d="M144,246.857143 C141.214272,246.857143 138.857152,245.892867 136.928571,243.964286 L36.6428571,147.214286 C35.5714232,146.357139 34.0982237,144.964295 32.2232143,143.035714 C30.3482049,141.107133 27.3750204,137.59824 23.3035714,132.508929 C19.2321225,127.419617 15.5893018,122.196455 12.375,116.839286 C9.16069821,111.482116 6.29465545,105.000038 3.77678571,97.3928571 C1.25891598,89.7856763 0,82.392893 0,75.2142857 C0,51.6427393 6.80350339,33.2143521 20.4107143,19.9285714 C34.0179252,6.64279071 52.8213086,0 76.8214286,0 C83.4643189,0 90.2410369,1.1517742 97.1517857,3.45535714 C104.062535,5.75894009 110.491042,8.86605187 116.4375,12.7767857 C122.383958,16.6875196 127.499979,20.3571257 131.785714,23.7857143 C136.07145,27.2143029 140.142838,30.8571236 144,34.7142857 C147.857162,30.8571236 151.92855,27.2143029 156.214286,23.7857143 C160.500021,20.3571257 165.616042,16.6875196 171.5625,12.7767857 C177.508958,8.86605187 183.937465,5.75894009 190.848214,3.45535714 C197.758963,1.1517742 204.535681,0 211.178571,0 C235.178691,0 253.982075,6.64279071 267.589286,19.9285714 C281.196497,33.2143521 288,51.6427393 288,75.2142857 C288,98.8929755 275.732266,122.999877 251.196429,147.535714 L151.071429,243.964286 C149.142847,245.892867 146.785728,246.857143 144,246.857143 L144,246.857143 Z"/>
+    </mask>
+    <path fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" mask="url(#mask)" d="M-166,115.135254 C-166,115.135254 0.595052083,115.135254 2.9765625,115.135254 L91.9101562,115.135254 L97.9638977,100.101562 L105.430695,115.135254 L114.893585,115.135254 L131.129913,189.53125 L148.161163,57 L165.348663,131.027344 L172.272491,115.135254 L250.84967,115.135254 L428.259813,115.135254"/>
+  </g>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/skin/shared/heartbeat-star-lit.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-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" viewBox="0 0 16 16" width="100%" height="100%">
+  <path fill="#0095dd" d="M8,0C7.7,0,7.4,0.2,7.2,0.7l-2,4.1L0.9,5.5c-1,0.2-1.2,0.9-0.5,1.6l3.1,3.3l-0.7,4.6C2.7,15.6,3,16,3.4,16c0.2,0,0.4-0.1,0.6-0.2L8,13.7l3.9,2.1c0.2,0.1,0.5,0.2,0.6,0.2c0.5,0,0.8-0.4,0.7-1.1l-0.7-4.6l3.1-3.3c0.7-0.7,0.4-1.4-0.5-1.6l-4.3-0.7l-2-4.1C8.6,0.2,8.3,0,8,0L8,0z"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/extensions/shield-recipe-client/skin/shared/heartbeat-star-off.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-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" viewBox="0 0 16 16" width="100%" height="100%">
+  <path fill="#c0c0c0" d="M8,0C7.7,0,7.4,0.2,7.2,0.7l-2,4.1L0.9,5.5c-1,0.2-1.2,0.9-0.5,1.6l3.1,3.3l-0.7,4.6C2.7,15.6,3,16,3.4,16c0.2,0,0.4-0.1,0.6-0.2L8,13.7l3.9,2.1c0.2,0.1,0.5,0.2,0.6,0.2c0.5,0,0.8-0.4,0.7-1.1l-0.7-4.6l3.1-3.3c0.7-0.7,0.4-1.4-0.5-1.6l-4.3-0.7l-2-4.1C8.6,0.2,8.3,0,8,0L8,0z"/>
+</svg>
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -683,18 +683,17 @@ you can use these alternative items. Oth
 <!ENTITY quitApplicationCmdWin2.label       "Exit">
 <!ENTITY quitApplicationCmdWin2.accesskey   "x">
 <!ENTITY quitApplicationCmdWin2.tooltip     "Exit &brandShorterName;">
 <!ENTITY goBackCmd.commandKey "[">
 <!ENTITY goForwardCmd.commandKey "]">
 <!ENTITY quitApplicationCmd.label       "Quit"> 
 <!ENTITY quitApplicationCmd.accesskey   "Q">
 <!ENTITY quitApplicationCmdMac2.label   "Quit &brandShorterName;">
-<!-- LOCALIZATION NOTE(quitApplicationCmdUnix.key): This keyboard shortcut is used by both Linux and OSX builds. -->
-<!ENTITY quitApplicationCmdUnix.key     "Q">
+<!ENTITY quitApplicationCmd.key         "Q">
 
 <!ENTITY closeCmd.label                 "Close">  
 <!ENTITY closeCmd.key                   "W">  
 <!ENTITY closeCmd.accesskey             "C">
 
 <!ENTITY toggleMuteCmd.key              "M">
 
 <!ENTITY pageStyleMenu.label "Page Style">
--- a/browser/themes/shared/UITour.inc.css
+++ b/browser/themes/shared/UITour.inc.css
@@ -148,146 +148,8 @@
   color: white;
   padding-left: 30px;
   padding-right: 30px;
 }
 
 #UITourTooltipButtons > button.button-primary:not(:active):hover {
   background-color: rgb(105,173,61);
 }
-
-/* Notification overrides for Heartbeat UI */
-
-notification.heartbeat {
-%ifdef XP_MACOSX
-  background-image: linear-gradient(-179deg, #FBFBFB 0%, #EBEBEB 100%);
-%else
-  background-color: #F1F1F1;
-%endif
-  border-bottom: 1px solid #C1C1C1;
-  height: 40px;
-}
-
-/* In themes/osx/global/notification.css the close icon is inverted because notifications
-   on OSX are usually dark. Heartbeat is light, so override that behaviour. */
-
-%ifdef XP_MACOSX
-notification.heartbeat[type="info"] .close-icon:not(:hover) {
-  -moz-image-region: rect(0, 16px, 16px, 0px) !important;
-}
-@media (min-resolution: 2dppx) {
-  notification.heartbeat[type="info"] .close-icon:not(:hover) {
-    -moz-image-region: rect(0, 32px, 32px, 0px) !important;
-  }
-}
-%endif
-
-@keyframes pulse-onshow {
- 0% {
-   opacity: 0;
-   transform: scale(1.0);
- }
- 25% {
-   opacity: 1;
-   transform: scale(1.1);
- }
- 50% {
-   transform: scale(1.0);
- }
- 75% {
-   transform: scale(1.1);
- }
- 100% {
-   transform: scale(1.0);
- }
-}
-
-@keyframes pulse-twice {
- 0% {
-   transform: scale(1.1);
- }
- 50% {
-   transform: scale(0.8);
- }
- 100% {
-   transform: scale(1);
- }
-}
-
-.messageText.heartbeat {
-  color: #333333;
-  text-shadow: none;
-  margin-inline-start: 0px;
-  /* The !important is required to override OSX default style. */
-  margin-inline-end: 12px !important;
-}
-
-.messageImage.heartbeat {
-  width: 24px;
-  height: 24px;
-  margin-inline-start: 8px;
-  margin-inline-end: 8px;
-}
-
-.messageImage.heartbeat.pulse-onshow {
-  animation-name: pulse-onshow;
-  animation-duration: 1.5s;
-  animation-iteration-count: 1;
-  animation-timing-function: cubic-bezier(.7,1.8,.9,1.1);
-}
-
-.messageImage.heartbeat.pulse-twice {
-  animation-name: pulse-twice;
-  animation-duration: 1s;
-  animation-iteration-count: 2;
-  animation-timing-function: linear;
-}
-
-/* Learn More link styles */
-.heartbeat > .text-link {
-  color: #0095DD;
-  margin-inline-start: 0px;
-}
-
-.heartbeat > .text-link:hover {
-  color: #008ACB;
-  text-decoration: none;
-}
-
-.heartbeat > .text-link:hover:active {
-  color: #006B9D;
-}
-
-/* Heartbeat UI Rating Star Classes */
-.heartbeat > #star-rating-container {
-  display: -moz-box;
-  margin-bottom: 4px;
-}
-
-.heartbeat > #star-rating-container > #star5 {
-  -moz-box-ordinal-group: 5;
-}
-
-.heartbeat > #star-rating-container > #star4 {
-  -moz-box-ordinal-group: 4;
-}
-
-.heartbeat > #star-rating-container > #star3 {
-  -moz-box-ordinal-group: 3;
-}
-
-.heartbeat > #star-rating-container > #star2 {
-  -moz-box-ordinal-group: 2;
-}
-
-.heartbeat > #star-rating-container > .star-x  {
-  background: url("chrome://browser/skin/heartbeat-star-off.svg");
-  cursor: pointer;
-  /* Overrides the margin-inline-end for all platforms defined in the .plain class */
-  margin-inline-end: 4px !important;
-  width: 16px;
-  height: 16px;
-}
-
-.heartbeat > #star-rating-container > .star-x:hover,
-.heartbeat > #star-rating-container > .star-x:hover ~ .star-x {
-  background: url("chrome://browser/skin/heartbeat-star-lit.svg");
-}
deleted file mode 100644
--- a/browser/themes/shared/heartbeat-icon.svg
+++ /dev/null
@@ -1,13 +0,0 @@
-<?xml version="1.0" encoding="UTF-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 width="288px" height="248px" viewBox="0 0 288 248" xmlns="http://www.w3.org/2000/svg">
-  <path fill="#d74345" d="M144,248.571429 C141.214272,248.571429 138.857152,247.607152 136.928571,245.678571 L36.6428571,148.928571 C35.5714232,148.071424 34.0982237,146.678581 32.2232143,144.75 C30.3482049,142.821419 27.3750204,139.312525 23.3035714,134.223214 C19.2321225,129.133903 15.5893018,123.910741 12.375,118.553571 C9.16069821,113.196402 6.29465545,106.714324 3.77678571,99.1071429 C1.25891598,91.499962 0,84.1071788 0,76.9285714 C0,53.357025 6.80350339,34.9286379 20.4107143,21.6428571 C34.0179252,8.35707643 52.8213086,1.71428571 76.8214286,1.71428571 C83.4643189,1.71428571 90.2410369,2.86605991 97.1517857,5.16964286 C104.062535,7.4732258 110.491042,10.5803376 116.4375,14.4910714 C122.383958,18.4018053 127.499979,22.0714114 131.785714,25.5 C136.07145,28.9285886 140.142838,32.5714093 144,36.4285714 C147.857162,32.5714093 151.92855,28.9285886 156.214286,25.5 C160.500021,22.0714114 165.616042,18.4018053 171.5625,14.4910714 C177.508958,10.5803376 183.937465,7.4732258 190.848214,5.16964286 C197.758963,2.86605991 204.535681,1.71428571 211.178571,1.71428571 C235.178691,1.71428571 253.982075,8.35707643 267.589286,21.6428571 C281.196497,34.9286379 288,53.357025 288,76.9285714 C288,100.607261 275.732266,124.714163 251.196429,149.25 L151.071429,245.678571 C149.142847,247.607152 146.785728,248.571429 144,248.571429 L144,248.571429 Z" transform="translate(0,-1)"/>
-  <g transform="translate(0,-0.29)">
-    <mask id="mask" fill="#fff">
-      <path d="M144,246.857143 C141.214272,246.857143 138.857152,245.892867 136.928571,243.964286 L36.6428571,147.214286 C35.5714232,146.357139 34.0982237,144.964295 32.2232143,143.035714 C30.3482049,141.107133 27.3750204,137.59824 23.3035714,132.508929 C19.2321225,127.419617 15.5893018,122.196455 12.375,116.839286 C9.16069821,111.482116 6.29465545,105.000038 3.77678571,97.3928571 C1.25891598,89.7856763 0,82.392893 0,75.2142857 C0,51.6427393 6.80350339,33.2143521 20.4107143,19.9285714 C34.0179252,6.64279071 52.8213086,0 76.8214286,0 C83.4643189,0 90.2410369,1.1517742 97.1517857,3.45535714 C104.062535,5.75894009 110.491042,8.86605187 116.4375,12.7767857 C122.383958,16.6875196 127.499979,20.3571257 131.785714,23.7857143 C136.07145,27.2143029 140.142838,30.8571236 144,34.7142857 C147.857162,30.8571236 151.92855,27.2143029 156.214286,23.7857143 C160.500021,20.3571257 165.616042,16.6875196 171.5625,12.7767857 C177.508958,8.86605187 183.937465,5.75894009 190.848214,3.45535714 C197.758963,1.1517742 204.535681,0 211.178571,0 C235.178691,0 253.982075,6.64279071 267.589286,19.9285714 C281.196497,33.2143521 288,51.6427393 288,75.2142857 C288,98.8929755 275.732266,122.999877 251.196429,147.535714 L151.071429,243.964286 C149.142847,245.892867 146.785728,246.857143 144,246.857143 L144,246.857143 Z"/>
-    </mask>
-    <path fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" mask="url(#mask)" d="M-166,115.135254 C-166,115.135254 0.595052083,115.135254 2.9765625,115.135254 L91.9101562,115.135254 L97.9638977,100.101562 L105.430695,115.135254 L114.893585,115.135254 L131.129913,189.53125 L148.161163,57 L165.348663,131.027344 L172.272491,115.135254 L250.84967,115.135254 L428.259813,115.135254"/>
-  </g>
-</svg>
deleted file mode 100644
--- a/browser/themes/shared/heartbeat-star-lit.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-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" viewBox="0 0 16 16" width="100%" height="100%">
-  <path fill="#0095dd" d="M8,0C7.7,0,7.4,0.2,7.2,0.7l-2,4.1L0.9,5.5c-1,0.2-1.2,0.9-0.5,1.6l3.1,3.3l-0.7,4.6C2.7,15.6,3,16,3.4,16c0.2,0,0.4-0.1,0.6-0.2L8,13.7l3.9,2.1c0.2,0.1,0.5,0.2,0.6,0.2c0.5,0,0.8-0.4,0.7-1.1l-0.7-4.6l3.1-3.3c0.7-0.7,0.4-1.4-0.5-1.6l-4.3-0.7l-2-4.1C8.6,0.2,8.3,0,8,0L8,0z"/>
-</svg>
deleted file mode 100644
--- a/browser/themes/shared/heartbeat-star-off.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-<?xml version="1.0" encoding="UTF-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" viewBox="0 0 16 16" width="100%" height="100%">
-  <path fill="#c0c0c0" d="M8,0C7.7,0,7.4,0.2,7.2,0.7l-2,4.1L0.9,5.5c-1,0.2-1.2,0.9-0.5,1.6l3.1,3.3l-0.7,4.6C2.7,15.6,3,16,3.4,16c0.2,0,0.4-0.1,0.6-0.2L8,13.7l3.9,2.1c0.2,0.1,0.5,0.2,0.6,0.2c0.5,0,0.8-0.4,0.7-1.1l-0.7-4.6l3.1-3.3c0.7-0.7,0.4-1.4-0.5-1.6l-4.3-0.7l-2-4.1C8.6,0.2,8.3,0,8,0L8,0z"/>
-</svg>
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/icons/quit.svg
@@ -0,0 +1,6 @@
+<!-- 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="16" height="16" viewBox="0 0 16 16">
+  <path fill="context-fill" d="M8 6a1 1 0 0 0 1-1V1a1 1 0 0 0-2 0v4a1 1 0 0 0 1 1zm3.5-4.032a1 1 0 0 0-1 1.732A4.946 4.946 0 0 1 13 8 5 5 0 0 1 3 8a4.946 4.946 0 0 1 2.5-4.3 1 1 0 0 0-1-1.732 7 7 0 1 0 7.006 0z"/>
+</svg>
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -51,19 +51,16 @@
   skin/classic/browser/customizableui/whimsy.png               (../shared/customizableui/whimsy.png)
   skin/classic/browser/customizableui/whimsy@2x.png            (../shared/customizableui/whimsy@2x.png)
   skin/classic/browser/downloads/contentAreaDownloadsView.css  (../shared/downloads/contentAreaDownloadsView.css)
   skin/classic/browser/downloads/download-blocked.svg          (../shared/downloads/download-blocked.svg)
   skin/classic/browser/downloads/download-summary.svg          (../shared/downloads/download-summary.svg)
   skin/classic/browser/drm-icon.svg                            (../shared/drm-icon.svg)
   skin/classic/browser/fullscreen/insecure.svg                 (../shared/fullscreen/insecure.svg)
   skin/classic/browser/fullscreen/secure.svg                   (../shared/fullscreen/secure.svg)
-  skin/classic/browser/heartbeat-icon.svg                      (../shared/heartbeat-icon.svg)
-  skin/classic/browser/heartbeat-star-lit.svg                  (../shared/heartbeat-star-lit.svg)
-  skin/classic/browser/heartbeat-star-off.svg                  (../shared/heartbeat-star-off.svg)
   skin/classic/browser/connection-secure.svg                   (../shared/identity-block/connection-secure.svg)
   skin/classic/browser/connection-mixed-passive-loaded.svg     (../shared/identity-block/connection-mixed-passive-loaded.svg)
   skin/classic/browser/connection-mixed-active-loaded.svg      (../shared/identity-block/connection-mixed-active-loaded.svg)
   skin/classic/browser/identity-icon.svg                       (../shared/identity-block/identity-icon.svg)
   skin/classic/browser/identity-icon-hover.svg                 (../shared/identity-block/identity-icon-hover.svg)
   skin/classic/browser/identity-icon-notice.svg                (../shared/identity-block/identity-icon-notice.svg)
   skin/classic/browser/identity-icon-notice-hover.svg          (../shared/identity-block/identity-icon-notice-hover.svg)
   skin/classic/browser/info.svg                                (../shared/info.svg)
@@ -136,16 +133,17 @@
   skin/classic/browser/mail.svg                       (../shared/icons/mail.svg)
   skin/classic/browser/menu.svg                       (../shared/icons/menu.svg)
   skin/classic/browser/new-tab.svg                    (../shared/icons/new-tab.svg)
   skin/classic/browser/new-window.svg                 (../shared/icons/new-window.svg)
   skin/classic/browser/open.svg                       (../shared/icons/open.svg)
   skin/classic/browser/page-action.svg                (../shared/icons/page-action.svg)
   skin/classic/browser/print.svg                      (../shared/icons/print.svg)
   skin/classic/browser/privateBrowsing.svg            (../shared/icons/privateBrowsing.svg)
+  skin/classic/browser/quit.svg                       (../shared/icons/quit.svg)
   skin/classic/browser/reload.svg                     (../shared/icons/reload.svg)
   skin/classic/browser/save.svg                       (../shared/icons/save.svg)
   skin/classic/browser/settings.svg                   (../shared/icons/settings.svg)
   skin/classic/browser/share.svg                      (../shared/icons/share.svg)
   skin/classic/browser/sidebars.svg                   (../shared/icons/sidebars.svg)
   skin/classic/browser/stop.svg                       (../shared/icons/stop.svg)
   skin/classic/browser/sync.svg                       (../shared/icons/sync.svg)
   skin/classic/browser/webIDE.svg                     (../shared/icons/webIDE.svg)
--- a/browser/themes/shared/menupanel.inc.css
+++ b/browser/themes/shared/menupanel.inc.css
@@ -215,20 +215,21 @@ toolbarpaletteitem[place="palette"] > #z
 }
 
 #appMenu-help-button {
   list-style-image: url(chrome://browser/skin/help.svg);
 }
 
 #appMenu-cut-button {
   list-style-image: url(chrome://browser/skin/edit-cut.svg);
-  -moz-context-properties: fill;
 }
 
 #appMenu-copy-button {
   list-style-image: url(chrome://browser/skin/edit-copy.svg);
-  -moz-context-properties: fill;
 }
 
 #appMenu-paste-button {
   list-style-image: url(chrome://browser/skin/edit-paste.svg);
-  -moz-context-properties: fill;
 }
+
+#appMenu-quit-button {
+  list-style-image: url(chrome://browser/skin/quit.svg);
+}
--- a/browser/themes/shared/sidebar.inc.css
+++ b/browser/themes/shared/sidebar.inc.css
@@ -1,24 +1,24 @@
 %if 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/. */
 %endif
 
 #sidebar-box {
-  --icon-fill: rgba(12, 12, 13, 0.8);
   --header-background-color: #F2F2F2;
   --header-background-color-hover: rgba(204, 204, 204, 0.6);
 }
 
 .sidebar-header,
 #sidebar-header {
   padding: 4px;
   background-color: var(--header-background-color);
+  color: -moz-dialogText;
   text-shadow: none;
 }
 
 .sidebar-splitter {
   -moz-appearance: none;
   border: 0 solid #ccc;
   border-inline-end-width: 1px;
   min-width: 1px;
@@ -48,40 +48,43 @@
   }
 }
 
 #sidebar-title {
   margin: 0;
   padding: 0;
   padding-inline-start: 8px;
   padding-inline-end: 4px;
-  color: -moz-DialogText;
+}
+
+#sidebar-icon,
+#sidebar-switcher-arrow,
+#sidebar-close > .toolbarbutton-icon {
+  -moz-context-properties: fill;
+  fill: currentColor;
+  opacity: 0.8;
 }
 
 #sidebar-switcher-arrow {
-  -moz-context-properties: fill;
-  fill: var(--icon-fill);
   list-style-image: url(chrome://browser/skin/arrow-dropdown.svg);
   width: 12px;
   height: 12px;
 }
 
 #sidebar-close {
   -moz-appearance: none;
-  -moz-context-properties: fill;
-  fill: var(--icon-fill);
   list-style-image: url(chrome://browser/skin/sidebar/close.svg);
   margin: 0;
   padding: 4px;
 }
 
 #sidebar-switcher-target {
   -moz-appearance: none;
   padding: 4px;
-  margin-inline-end: 4px;
+  color: inherit;
 }
 
 #sidebar-box #sidebar-switcher-target:hover,
 #sidebar-switcher-target.active,
 #sidebar-close:hover {
   background: var(--header-background-color-hover);
 }
 
@@ -100,27 +103,22 @@
 }
 #sidebarMenu-popup .subviewbutton-iconic > .toolbarbutton-text {
   padding-inline-start: 0;
 }
 %endif
 
 /* Use bookmarks star as default icon for the sidebar box (including when opening a web page) */
 #sidebar-switcher-bookmarks > .toolbarbutton-icon,
-#sidebar-box #sidebar-icon {
-  -moz-context-properties: fill;
-  fill: var(--icon-fill);
+#sidebar-box[sidebarcommand="viewWebPanelsSidebar"] > #sidebar-header > #sidebar-switcher-target > #sidebar-icon,
+#sidebar-box[sidebarcommand="viewBookmarksSidebar"] > #sidebar-header > #sidebar-switcher-target > #sidebar-icon {
   list-style-image: url(chrome://browser/skin/bookmark.svg);
 }
 
 #sidebar-switcher-history > .toolbarbutton-icon,
-#sidebar-box[sidebarcommand="viewHistorySidebar"] #sidebar-icon {
-  -moz-context-properties: fill;
-  fill: var(--icon-fill);
+#sidebar-box[sidebarcommand="viewHistorySidebar"] > #sidebar-header > #sidebar-switcher-target > #sidebar-icon {
   list-style-image: url(chrome://browser/skin/history.svg);
 }
 
 #sidebar-switcher-tabs > .toolbarbutton-icon,
-#sidebar-box[sidebarcommand="viewTabsSidebar"] #sidebar-icon {
-  -moz-context-properties: fill;
-  fill: var(--icon-fill);
+#sidebar-box[sidebarcommand="viewTabsSidebar"] > #sidebar-header > #sidebar-switcher-target > #sidebar-icon {
   list-style-image: url(chrome://browser/skin/sync.svg);
 }
--- a/devtools/client/definitions.js
+++ b/devtools/client/definitions.js
@@ -296,17 +296,17 @@ Tools.memory = {
   build: function (frame, target) {
     return new MemoryPanel(frame, target);
   }
 };
 
 Tools.netMonitor = {
   id: "netmonitor",
   accesskey: l10n("netmonitor.accesskey"),
-  key: l10n("netmonitor.commandkey"),
+  key: l10n("netmonitor.commandkey2"),
   ordinal: 9,
   modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift",
   visibilityswitch: "devtools.netmonitor.enabled",
   icon: "chrome://devtools/skin/images/tool-network.svg",
   invertIconForDarkTheme: true,
   url: "chrome://devtools/content/netmonitor/index.html",
   label: l10n("netmonitor.label"),
   panelLabel: l10n("netmonitor.panelLabel"),
--- a/devtools/client/locales/en-US/startup.properties
+++ b/devtools/client/locales/en-US/startup.properties
@@ -170,19 +170,19 @@ inspector.tooltip2=DOM and Style Inspect
 # This string is displayed in the title of the tab when the Network Monitor is
 # displayed inside the developer tools window and in the Developer Tools Menu.
 netmonitor.label=Network
 
 # LOCALIZATION NOTE (netmonitor.panelLabel):
 # This is used as the label for the toolbox panel.
 netmonitor.panelLabel=Network Panel
 
-# LOCALIZATION NOTE (netmonitor.commandkey, netmonitor.accesskey)
+# LOCALIZATION NOTE (netmonitor.commandkey2, netmonitor.accesskey)
 # Used for the menuitem in the tool menu
-netmonitor.commandkey=Q
+netmonitor.commandkey2=E
 netmonitor.accesskey=N
 
 # LOCALIZATION NOTE (netmonitor.tooltip2):
 # This string is displayed in the tooltip of the tab when the Network Monitor is
 # displayed inside the developer tools window.
 # Keyboard shortcut for Network Monitor will be shown inside the brackets.
 netmonitor.tooltip2=Network Monitor (%S)
 
--- a/dom/animation/Keyframe.h
+++ b/dom/animation/Keyframe.h
@@ -29,16 +29,22 @@ struct PropertyValuePair
   // The specified value for the property. For shorthand properties or invalid
   // property values, we store the specified property value as a token stream
   // (string).
   nsCSSValue mValue;
 
   // The specified value when using the Servo backend.
   RefPtr<RawServoDeclarationBlock> mServoDeclarationBlock;
 
+#ifdef DEBUG
+  // Flag to indicate that when we call StyleAnimationValue::ComputeValues on
+  // this value we should behave as if that function had failed.
+  bool mSimulateComputeValuesFailure;
+#endif
+
   bool operator==(const PropertyValuePair&) const;
 };
 
 /**
  * A single keyframe.
  *
  * This is the canonical form in which keyframe effects are stored and
  * corresponds closely to the type of objects returned via the getKeyframes()
--- a/dom/animation/KeyframeUtils.cpp
+++ b/dom/animation/KeyframeUtils.cpp
@@ -384,21 +384,23 @@ AppendValueAsString(JSContext* aCx,
 
 static PropertyValuePair
 MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue,
                       nsCSSParser& aParser, nsIDocument* aDocument);
 
 static bool
 HasValidOffsets(const nsTArray<Keyframe>& aKeyframes);
 
+#ifdef DEBUG
 static void
 MarkAsComputeValuesFailureKey(PropertyValuePair& aPair);
 
 static bool
 IsComputeValuesFailureKey(const PropertyValuePair& aPair);
+#endif
 
 static void
 BuildSegmentsFromValueEntries(nsTArray<KeyframeValueEntry>& aEntries,
                               nsTArray<AnimationProperty>& aResult);
 
 static void
 GetKeyframeListFromPropertyIndexedKeyframe(JSContext* aCx,
                                            nsIDocument* aDocument,
@@ -635,20 +637,25 @@ KeyframeUtils::GetComputedKeyframeValues
       nsTArray<PropertyStyleAnimationValuePair> values;
 
       // For shorthands, we store the string as a token stream so we need to
       // extract that first.
       if (nsCSSProps::IsShorthand(pair.mProperty)) {
         nsCSSValueTokenStream* tokenStream = pair.mValue.GetTokenStreamValue();
         if (!StyleAnimationValue::ComputeValues(pair.mProperty,
               CSSEnabledState::eForAllContent, aElement, aStyleContext,
-              tokenStream->mTokenStream, /* aUseSVGMode */ false, values) ||
-            IsComputeValuesFailureKey(pair)) {
+              tokenStream->mTokenStream, /* aUseSVGMode */ false, values)) {
           continue;
         }
+
+#ifdef DEBUG
+        if (IsComputeValuesFailureKey(pair)) {
+          continue;
+        }
+#endif
       } else if (pair.mValue.GetUnit() == eCSSUnit_Null) {
         // An uninitialized nsCSSValue represents the underlying value which
         // we represent as an uninitialized AnimationValue so we just leave
         // neutralPair->mValue as-is.
         PropertyStyleAnimationValuePair* neutralPair = values.AppendElement();
         neutralPair->mProperty = pair.mProperty;
       } else {
         if (!StyleAnimationValue::ComputeValues(pair.mProperty,
@@ -868,26 +875,28 @@ ConvertKeyframeSequence(JSContext* aCx,
     }
 
     for (PropertyValuesPair& pair : propertyValuePairs) {
       MOZ_ASSERT(pair.mValues.Length() == 1);
       keyframe->mPropertyValues.AppendElement(
         MakePropertyValuePair(pair.mProperty, pair.mValues[0], parser,
                               aDocument));
 
+#ifdef DEBUG
       // When we go to convert keyframes into arrays of property values we
       // call StyleAnimation::ComputeValues. This should normally return true
       // but in order to test the case where it does not, BaseKeyframeDict
       // includes a chrome-only member that can be set to indicate that
       // ComputeValues should fail for shorthand property values on that
       // keyframe.
       if (nsCSSProps::IsShorthand(pair.mProperty) &&
           keyframeDict.mSimulateComputeValuesFailure) {
         MarkAsComputeValuesFailureKey(keyframe->mPropertyValues.LastElement());
       }
+#endif
     }
   }
 
   return true;
 }
 
 /**
  * Reads the property-values pairs from the specified JS object.
@@ -1028,16 +1037,20 @@ static PropertyValuePair
 MakePropertyValuePair(nsCSSPropertyID aProperty, const nsAString& aStringValue,
                       nsCSSParser& aParser, nsIDocument* aDocument)
 {
   MOZ_ASSERT(aDocument);
   PropertyValuePair result;
 
   result.mProperty = aProperty;
 
+#ifdef DEBUG
+  result.mSimulateComputeValuesFailure = false;
+#endif
+
   if (aDocument->GetStyleBackendType() == StyleBackendType::Servo) {
     RefPtr<RawServoDeclarationBlock> servoDeclarationBlock =
       KeyframeUtils::ParseProperty(aProperty, aStringValue, aDocument);
 
     if (servoDeclarationBlock) {
       result.mServoDeclarationBlock = servoDeclarationBlock.forget();
     }
     return result;
@@ -1100,60 +1113,50 @@ HasValidOffsets(const nsTArray<Keyframe>
         return false;
       }
       offset = thisOffset;
     }
   }
   return true;
 }
 
+#ifdef DEBUG
 /**
  * Takes a property-value pair for a shorthand property and modifies the
  * value to indicate that when we call StyleAnimationValue::ComputeValues on
  * that value we should behave as if that function had failed.
  *
  * @param aPair The PropertyValuePair to modify. |aPair.mProperty| must be
  *              a shorthand property.
  */
 static void
 MarkAsComputeValuesFailureKey(PropertyValuePair& aPair)
 {
   MOZ_ASSERT(nsCSSProps::IsShorthand(aPair.mProperty),
              "Only shorthand property values can be marked as failure values");
 
-  // We store shorthand values as nsCSSValueTokenStream objects whose mProperty
-  // and mShorthandPropertyID are eCSSProperty_UNKNOWN and whose mTokenStream
-  // member contains the shorthand property's value as a string.
-  //
-  // We need to leave mShorthandPropertyID as eCSSProperty_UNKNOWN so that
-  // nsCSSValue::AppendToString returns the mTokenStream value, but we can
-  // update mPropertyID to a special value to indicate that this is
-  // a special failure sentinel.
-  nsCSSValueTokenStream* tokenStream = aPair.mValue.GetTokenStreamValue();
-  MOZ_ASSERT(tokenStream->mPropertyID == eCSSProperty_UNKNOWN,
-             "Shorthand value should initially have an unknown property ID");
-  tokenStream->mPropertyID = eCSSPropertyExtra_no_properties;
+  aPair.mSimulateComputeValuesFailure = true;
 }
 
 /**
  * Returns true if |aPair| is a property-value pair on which we have
  * previously called MarkAsComputeValuesFailureKey (and hence we should
  * simulate failure when calling StyleAnimationValue::ComputeValues using its
  * value).
  *
  * @param aPair The property-value pair to test.
  * @return True if |aPair| represents a failure value.
  */
 static bool
 IsComputeValuesFailureKey(const PropertyValuePair& aPair)
 {
   return nsCSSProps::IsShorthand(aPair.mProperty) &&
-         aPair.mValue.GetTokenStreamValue()->mPropertyID ==
-           eCSSPropertyExtra_no_properties;
+         aPair.mSimulateComputeValuesFailure;
 }
+#endif
 
 static void
 AppendInitialSegment(AnimationProperty* aAnimationProperty,
                      const KeyframeValueEntry& aFirstEntry)
 {
   AnimationPropertySegment* segment =
     aAnimationProperty->mSegments.AppendElement();
   segment->mFromKey        = 0.0f;
--- a/dom/animation/test/chrome.ini
+++ b/dom/animation/test/chrome.ini
@@ -13,8 +13,9 @@ support-files =
 [chrome/test_animation_performance_warning.html]
 [chrome/test_animation_properties.html]
 [chrome/test_cssanimation_missing_keyframes.html]
 [chrome/test_generated_content_getAnimations.html]
 [chrome/test_restyles.html]
 skip-if = os == 'android' && processor == 'x86' # bug 1335986
 [chrome/test_running_on_compositor.html]
 [chrome/test_simulate_compute_values_failure.html]
+skip-if = !debug
--- a/dom/base/DOMMatrix.h
+++ b/dom/base/DOMMatrix.h
@@ -38,16 +38,22 @@ public:
   {
     if (other.mMatrix2D) {
       mMatrix2D = new gfx::Matrix(*other.mMatrix2D);
     } else {
       mMatrix3D = new gfx::Matrix4x4(*other.mMatrix3D);
     }
   }
 
+  DOMMatrixReadOnly(nsISupports* aParent, const gfx::Matrix4x4& aMatrix)
+    : mParent(aParent)
+  {
+    mMatrix3D = new gfx::Matrix4x4(aMatrix);
+  }
+
   NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(DOMMatrixReadOnly)
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(DOMMatrixReadOnly)
 
 #define GetMatrixMember(entry2D, entry3D, default) \
 { \
   if (mMatrix3D) { \
     return mMatrix3D->entry3D; \
   } \
@@ -150,16 +156,20 @@ public:
   explicit DOMMatrix(nsISupports* aParent)
     : DOMMatrixReadOnly(aParent)
   {}
 
   DOMMatrix(nsISupports* aParent, const DOMMatrixReadOnly& other)
     : DOMMatrixReadOnly(aParent, other)
   {}
 
+  DOMMatrix(nsISupports* aParent, const gfx::Matrix4x4& aMatrix)
+    : DOMMatrixReadOnly(aParent, aMatrix)
+  {}
+
   static already_AddRefed<DOMMatrix>
   Constructor(const GlobalObject& aGlobal, ErrorResult& aRv);
   static already_AddRefed<DOMMatrix>
   Constructor(const GlobalObject& aGlobal, const nsAString& aTransformList, ErrorResult& aRv);
   static already_AddRefed<DOMMatrix>
   Constructor(const GlobalObject& aGlobal, const DOMMatrixReadOnly& aOther, ErrorResult& aRv);
   static already_AddRefed<DOMMatrix>
   Constructor(const GlobalObject& aGlobal, const Float32Array& aArray32, ErrorResult& aRv);
--- a/dom/base/Element.cpp
+++ b/dom/base/Element.cpp
@@ -149,16 +149,18 @@
 #include "mozilla/IntegerPrintfMacros.h"
 #include "mozilla/Preferences.h"
 #include "nsComputedDOMStyle.h"
 #include "nsDOMStringMap.h"
 #include "DOMIntersectionObserver.h"
 
 #include "nsISpeculativeConnect.h"
 
+#include "DOMMatrix.h"
+
 using namespace mozilla;
 using namespace mozilla::dom;
 
 //
 // Verify sizes of elements on 64-bit platforms. This should catch most memory
 // regressions, and is easy to verify locally since most developers are on
 // 64-bit machines. We use a template rather than a direct static assert so
 // that the error message actually displays the sizes.
@@ -3426,16 +3428,68 @@ Element::GetGridFragments(nsTArray<RefPt
   while (frame) {
     aResult.AppendElement(
       new Grid(this, frame)
     );
     frame = static_cast<nsGridContainerFrame*>(frame->GetNextInFlow());
   }
 }
 
+already_AddRefed<DOMMatrixReadOnly>
+Element::GetTransformToAncestor(Element& aAncestor)
+{
+  nsIFrame* primaryFrame = GetPrimaryFrame();
+  nsIFrame* ancestorFrame = aAncestor.GetPrimaryFrame();
+
+  Matrix4x4 transform;
+  if (primaryFrame) {
+    // If aAncestor is not actually an ancestor of this (including nullptr),
+    // then the call to GetTransformToAncestor will return the transform
+    // all the way up through the parent chain.
+    transform = nsLayoutUtils::GetTransformToAncestor(primaryFrame,
+      ancestorFrame, true);
+  }
+
+  DOMMatrixReadOnly* matrix = new DOMMatrix(this, transform);
+  RefPtr<DOMMatrixReadOnly> result(matrix);
+  return result.forget();
+}
+
+already_AddRefed<DOMMatrixReadOnly>
+Element::GetTransformToParent()
+{
+  nsIFrame* primaryFrame = GetPrimaryFrame();
+
+  Matrix4x4 transform;
+  if (primaryFrame) {
+    nsIFrame* parentFrame = primaryFrame->GetParent();
+    transform = nsLayoutUtils::GetTransformToAncestor(primaryFrame,
+      parentFrame, true);
+  }
+
+  DOMMatrixReadOnly* matrix = new DOMMatrix(this, transform);
+  RefPtr<DOMMatrixReadOnly> result(matrix);
+  return result.forget();
+}
+
+already_AddRefed<DOMMatrixReadOnly>
+Element::GetTransformToViewport()
+{
+  nsIFrame* primaryFrame = GetPrimaryFrame();
+  Matrix4x4 transform;
+  if (primaryFrame) {
+    transform = nsLayoutUtils::GetTransformToAncestor(primaryFrame,
+      nsLayoutUtils::GetDisplayRootFrame(primaryFrame), true);
+  }
+
+  DOMMatrixReadOnly* matrix = new DOMMatrix(this, transform);
+  RefPtr<DOMMatrixReadOnly> result(matrix);
+  return result.forget();
+}
+
 already_AddRefed<Animation>
 Element::Animate(JSContext* aContext,
                  JS::Handle<JSObject*> aKeyframes,
                  const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
                  ErrorResult& aError)
 {
   Nullable<ElementOrCSSPseudoElement> target;
   target.SetValue().SetAsElement() = this;
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -62,16 +62,17 @@ class nsDOMStringMap;
 
 namespace mozilla {
 class DeclarationBlock;
 namespace dom {
   struct AnimationFilter;
   struct ScrollIntoViewOptions;
   struct ScrollToOptions;
   class DOMIntersectionObserver;
+  class DOMMatrixReadOnly;
   class ElementOrCSSPseudoElement;
   class UnrestrictedDoubleOrKeyframeAnimationOptions;
   enum class CallerType : uint32_t;
 } // namespace dom
 } // namespace mozilla
 
 
 already_AddRefed<nsContentList>
@@ -1061,16 +1062,20 @@ public:
     nsIScrollableFrame* sf = GetScrollFrame();
     return sf ?
            nsPresContext::AppUnitsToIntCSSPixels(sf->GetScrollRange().XMost()) :
            0;
   }
 
   void GetGridFragments(nsTArray<RefPtr<Grid>>& aResult);
 
+  already_AddRefed<DOMMatrixReadOnly> GetTransformToAncestor(Element& aAncestor);
+  already_AddRefed<DOMMatrixReadOnly> GetTransformToParent();
+  already_AddRefed<DOMMatrixReadOnly> GetTransformToViewport();
+
   already_AddRefed<Animation>
   Animate(JSContext* aContext,
           JS::Handle<JSObject*> aKeyframes,
           const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions,
           ErrorResult& aError);
 
   // A helper method that factors out the common functionality needed by
   // Element::Animate and CSSPseudoElement::Animate
--- a/dom/base/Navigator.cpp
+++ b/dom/base/Navigator.cpp
@@ -41,16 +41,17 @@
 #include "mozilla/dom/FlyWebService.h"
 #include "mozilla/dom/Permissions.h"
 #include "mozilla/dom/Presentation.h"
 #include "mozilla/dom/ServiceWorkerContainer.h"
 #include "mozilla/dom/StorageManager.h"
 #include "mozilla/dom/TCPSocket.h"
 #include "mozilla/dom/URLSearchParams.h"
 #include "mozilla/dom/VRDisplay.h"
+#include "mozilla/dom/VRDisplayEvent.h"
 #include "mozilla/dom/VRServiceTest.h"
 #include "mozilla/dom/WebAuthentication.h"
 #include "mozilla/dom/workers/RuntimeService.h"
 #include "mozilla/Hal.h"
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/SSE.h"
 #include "mozilla/StaticPtr.h"
 #include "Connection.h"
@@ -1562,16 +1563,37 @@ Navigator::RequestVRServiceTest()
   win->NotifyVREventListenerAdded();
 
   if (!mVRServiceTest) {
     mVRServiceTest = VRServiceTest::CreateTestService(mWindow);
   }
   return mVRServiceTest;
 }
 
+bool
+Navigator::IsWebVRContentDetected() const
+{
+  nsGlobalWindow* win = nsGlobalWindow::Cast(mWindow);
+  return win->IsVRContentDetected();
+}
+
+bool
+Navigator::IsWebVRContentPresenting() const
+{
+  nsGlobalWindow* win = nsGlobalWindow::Cast(mWindow);
+  return win->IsVRContentPresenting();
+}
+
+void
+Navigator::RequestVRPresentation(VRDisplay& aDisplay)
+{
+  nsGlobalWindow* win = nsGlobalWindow::Cast(mWindow);
+  win->DispatchVRDisplayActivate(aDisplay.DisplayId(), VRDisplayEventReason::Requested);
+}
+
 //*****************************************************************************
 //    Navigator::nsIMozNavigatorNetwork
 //*****************************************************************************
 
 NS_IMETHODIMP
 Navigator::GetProperties(nsINetworkProperties** aProperties)
 {
   ErrorResult rv;
--- a/dom/base/Navigator.h
+++ b/dom/base/Navigator.h
@@ -193,16 +193,19 @@ public:
   network::Connection* GetConnection(ErrorResult& aRv);
   MediaDevices* GetMediaDevices(ErrorResult& aRv);
 
   void GetGamepads(nsTArray<RefPtr<Gamepad> >& aGamepads, ErrorResult& aRv);
   GamepadServiceTest* RequestGamepadServiceTest();
   already_AddRefed<Promise> GetVRDisplays(ErrorResult& aRv);
   void GetActiveVRDisplays(nsTArray<RefPtr<VRDisplay>>& aDisplays) const;
   VRServiceTest* RequestVRServiceTest();
+  bool IsWebVRContentDetected() const;
+  bool IsWebVRContentPresenting() const;
+  void RequestVRPresentation(VRDisplay& aDisplay);
 #ifdef MOZ_TIME_MANAGER
   time::TimeManager* GetMozTime(ErrorResult& aRv);
 #endif // MOZ_TIME_MANAGER
 
   Presentation* GetPresentation(ErrorResult& aRv);
 
   bool SendBeacon(const nsAString& aUrl,
                   const Nullable<fetch::BodyInit>& aData,
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -198,16 +198,17 @@
 
 #include "mozilla/dom/IDBFactory.h"
 #include "mozilla/dom/MessageChannel.h"
 #include "mozilla/dom/Promise.h"
 
 #include "mozilla/dom/Gamepad.h"
 #include "mozilla/dom/GamepadManager.h"
 
+#include "gfxVR.h"
 #include "mozilla/dom/VRDisplay.h"
 #include "mozilla/dom/VRDisplayEvent.h"
 #include "mozilla/dom/VRDisplayEventBinding.h"
 #include "mozilla/dom/VREventObserver.h"
 
 #include "nsRefreshDriver.h"
 #include "Layers.h"
 
@@ -1567,16 +1568,17 @@ nsGlobalWindow::nsGlobalWindow(nsGlobalW
     mIsChrome(false),
     mCleanMessageManager(false),
     mNeedsFocus(true),
     mHasFocus(false),
     mShowFocusRingForContent(false),
     mFocusByKeyOccurred(false),
     mHasGamepad(false),
     mHasVREvents(false),
+    mHasVRDisplayActivateEvents(false),
     mHasSeenGamepadInput(false),
     mNotifiedIDDestroyed(false),
     mAllowScriptsToClose(false),
     mTopLevelOuterContentWindow(false),
     mSuspendDepth(0),
     mFreezeDepth(0),
     mFocusMethod(0),
     mSerial(0),
@@ -1996,23 +1998,25 @@ nsGlobalWindow::CleanUp()
     }
   }
 
   if (IsInnerWindow()) {
     DisableGamepadUpdates();
     mHasGamepad = false;
     DisableVRUpdates();
     mHasVREvents = false;
+    mHasVRDisplayActivateEvents = false;
 #ifdef MOZ_B2G
     DisableTimeChangeNotifications();
 #endif
     DisableIdleCallbackRequests();
   } else {
     MOZ_ASSERT(!mHasGamepad);
     MOZ_ASSERT(!mHasVREvents);
+    MOZ_ASSERT(!mHasVRDisplayActivateEvents);
   }
 
   if (mCleanMessageManager) {
     MOZ_ASSERT(mIsChrome, "only chrome should have msg manager cleaned");
     nsGlobalChromeWindow *asChrome = static_cast<nsGlobalChromeWindow*>(this);
     if (asChrome->mMessageManager) {
       static_cast<nsFrameMessageManager*>(
         asChrome->mMessageManager.get())->Disconnect();
@@ -2165,16 +2169,17 @@ nsGlobalWindow::FreeInnerObjects()
   }
   mAudioContexts.Clear();
 
   DisableGamepadUpdates();
   mHasGamepad = false;
   mGamepads.Clear();
   DisableVRUpdates();
   mHasVREvents = false;
+  mHasVRDisplayActivateEvents = false;
   mVRDisplays.Clear();
 
   if (mTabChild) {
     while (mBeforeUnloadListenerCount-- > 0) {
       mTabChild->BeforeUnloadRemoved();
     }
   }
 }
@@ -13518,16 +13523,20 @@ nsGlobalWindow::EventListenerAdded(nsIAt
   if (aType == nsGkAtoms::onvrdisplayactivate ||
       aType == nsGkAtoms::onvrdisplayconnect ||
       aType == nsGkAtoms::onvrdisplaydeactivate ||
       aType == nsGkAtoms::onvrdisplaydisconnect ||
       aType == nsGkAtoms::onvrdisplaypresentchange) {
     NotifyVREventListenerAdded();
   }
 
+  if (aType == nsGkAtoms::onvrdisplayactivate) {
+    mHasVRDisplayActivateEvents = true;
+  }
+
   if (aType == nsGkAtoms::onbeforeunload &&
       mTabChild &&
       (!mDoc || !(mDoc->GetSandboxFlags() & SANDBOXED_MODALS))) {
     MOZ_ASSERT(IsInnerWindow());
     mBeforeUnloadListenerCount++;
     MOZ_ASSERT(mBeforeUnloadListenerCount > 0);
     mTabChild->BeforeUnloadAdded();
   }
@@ -13561,19 +13570,42 @@ nsGlobalWindow::NotifyVREventListenerAdd
   EnableVRUpdates();
 }
 
 bool
 nsGlobalWindow::HasUsedVR() const
 {
   MOZ_ASSERT(IsInnerWindow());
 
+  // Returns true only if any WebVR API call or related event
+  // has been used
   return mHasVREvents;
 }
 
+bool
+nsGlobalWindow::IsVRContentDetected() const
+{
+  MOZ_ASSERT(IsInnerWindow());
+
+  // Returns true only if the content will respond to
+  // the VRDisplayActivate event.
+  return mHasVRDisplayActivateEvents;
+}
+
+bool
+nsGlobalWindow::IsVRContentPresenting() const
+{
+  for (auto display : mVRDisplays) {
+    if (display->IsAnyPresenting(gfx::kVRGroupAll)) {
+      return true;
+    }
+  }
+  return false;
+}
+
 void
 nsGlobalWindow::EnableTimeChangeNotifications()
 {
   mozilla::time::AddWindowListener(AsInner());
 }
 
 void
 nsGlobalWindow::DisableTimeChangeNotifications()
@@ -13758,17 +13790,17 @@ void
 nsGlobalWindow::DispatchVRDisplayActivate(uint32_t aDisplayID,
                                           mozilla::dom::VRDisplayEventReason aReason)
 {
   // Search for the display identified with aDisplayID and fire the
   // event if found.
   for (auto display : mVRDisplays) {
     if (display->DisplayId() == aDisplayID) {
       if (aReason != VRDisplayEventReason::Navigation &&
-          display->IsAnyPresenting()) {
+          display->IsAnyPresenting(gfx::kVRGroupContent)) {
         // We only want to trigger this event if nobody is presenting to the
         // display already or when a page is loaded by navigating away
         // from a page with an active VR Presentation.
         continue;
       }
 
       VRDisplayEventInit init;
       init.mBubbles = false;
--- a/dom/base/nsGlobalWindow.h
+++ b/dom/base/nsGlobalWindow.h
@@ -452,16 +452,18 @@ public:
   bool SetWidgetFullscreen(FullscreenReason aReason, bool aIsFullscreen,
                            nsIWidget* aWidget, nsIScreen* aScreen);
   bool FullScreen() const;
 
   // Inner windows only.
   virtual void SetHasGamepadEventListener(bool aHasGamepad = true) override;
   void NotifyVREventListenerAdded();
   bool HasUsedVR() const;
+  bool IsVRContentDetected() const;
+  bool IsVRContentPresenting() const;
 
   using EventTarget::EventListenerAdded;
   virtual void EventListenerAdded(nsIAtom* aType) override;
   using EventTarget::EventListenerRemoved;
   virtual void EventListenerRemoved(nsIAtom* aType) override;
 
   // nsIInterfaceRequestor
   NS_DECL_NSIINTERFACEREQUESTOR
@@ -1902,16 +1904,20 @@ protected:
 
   // Inner windows only.
   // Indicates whether this window wants gamepad input events
   bool                   mHasGamepad : 1;
 
   // Inner windows only.
   // Indicates whether this window wants VR events
   bool                   mHasVREvents : 1;
+
+  // Inner windows only.
+  // Indicates whether this window wants VRDisplayActivate events
+  bool                   mHasVRDisplayActivateEvents : 1;
   nsCheapSet<nsUint32HashKey> mGamepadIndexSet;
   nsRefPtrHashtable<nsUint32HashKey, mozilla::dom::Gamepad> mGamepads;
   bool mHasSeenGamepadInput;
 
   // whether we've sent the destroy notification for our window id
   bool                   mNotifiedIDDestroyed : 1;
   // whether scripts may close the window,
   // even if "dom.allow_scripts_to_close_windows" is false.
--- a/dom/media/webm/WebMDemuxer.cpp
+++ b/dom/media/webm/WebMDemuxer.cpp
@@ -1106,17 +1106,17 @@ WebMTrackDemuxer::Seek(const TimeUnit& a
   SetNextKeyFrameTime();
 
   return SeekPromise::CreateAndResolve(seekTime, __func__);
 }
 
 nsresult
 WebMTrackDemuxer::NextSample(RefPtr<MediaRawData>& aData)
 {
-  nsresult rv;
+  nsresult rv = NS_ERROR_DOM_MEDIA_END_OF_STREAM;;
   while (mSamples.GetSize() < 1 &&
          NS_SUCCEEDED((rv = mParent->GetNextPacket(mType, &mSamples)))) {
   }
   if (mSamples.GetSize()) {
     aData = mSamples.PopFront();
     return NS_OK;
   }
   return rv;
--- a/dom/plugins/base/nsPluginTags.cpp
+++ b/dom/plugins/base/nsPluginTags.cpp
@@ -430,20 +430,19 @@ nsPluginTag::InitSandboxLevel()
   sandboxPref.Append(GetNiceFileName());
   if (NS_FAILED(Preferences::GetInt(sandboxPref.get(), &mSandboxLevel))) {
     mSandboxLevel = Preferences::GetInt("dom.ipc.plugins.sandbox-level.default"
 );
   }
 
 #if defined(_AMD64_)
   // As level 2 is now the default NPAPI sandbox level for 64-bit flash, we
-  // don't want to allow a lower setting unless this environment variable is
-  // set. This should be changed if the firefox.js pref file is changed.
-  if (mIsFlashPlugin &&
-      !PR_GetEnv("MOZ_ALLOW_WEAKER_SANDBOX") && mSandboxLevel < 2) {
+  // don't want to allow a lower setting. This should be changed if the
+  // firefox.js pref file is changed.
+  if (mIsFlashPlugin && mSandboxLevel < 2) {
     mSandboxLevel = 2;
   }
 #endif
 #endif
 }
 
 #if !defined(XP_WIN) && !defined(XP_MACOSX)
 static nsresult ConvertToUTF8(nsIUnicodeDecoder *aUnicodeDecoder,
@@ -482,17 +481,17 @@ nsresult nsPluginTag::EnsureMembersAreUT
   nsAutoCString charset;
   rv = pcs->GetCharset(kPlatformCharsetSel_FileName, charset);
   NS_ENSURE_SUCCESS(rv, rv);
   if (!charset.LowerCaseEqualsLiteral("utf-8")) {
     decoder = EncodingUtils::DecoderForEncoding(charset);
     ConvertToUTF8(decoder, mFileName);
     ConvertToUTF8(decoder, mFullPath);
   }
-  
+
   // The description of the plug-in and the various MIME type descriptions
   // should be encoded in the standard plain text file encoding for this system.
   // XXX should we add kPlatformCharsetSel_PluginResource?
   rv = pcs->GetCharset(kPlatformCharsetSel_PlainTextInFile, charset);
   NS_ENSURE_SUCCESS(rv, rv);
   if (!charset.LowerCaseEqualsLiteral("utf-8")) {
     decoder = EncodingUtils::DecoderForEncoding(charset);
     ConvertToUTF8(decoder, mName);
--- a/dom/tests/mochitest/chrome/chrome.ini
+++ b/dom/tests/mochitest/chrome/chrome.ini
@@ -58,16 +58,17 @@ skip-if = os == 'linux' && !debug # bug 
 [test_focus_docnav.xul]
 [test_focus_switchbinding.xul]
 [test_focused_link_scroll.xul]
 [test_fullscreen.xul]
 tags = fullscreen
 # disabled on linux for timeouts--bug-867745
 skip-if = os == 'linux'
 [test_geolocation.xul]
+[test_getTransformTo.html]
 [test_indexedSetter.html]
 [test_intlUtils_getDisplayNames.html]
 [test_intlUtils_getLocaleInfo.html]
 [test_moving_nodeList.xul]
 [test_moving_xhr.xul]
 [test_MozDomFullscreen_event.xul]
 tags = fullscreen
 # disabled on OS X for intermittent failures--bug-798848
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/chrome/test_getTransformTo.html
@@ -0,0 +1,117 @@
+<!doctype html>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Test Element::getTransformToViewport</title>
+<script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+<style>
+body {
+  margin: 0px;
+}
+.box {
+  background-color: red;
+  height: 20px;
+  width: 80px;
+}
+.a {
+  margin: 10px;
+}
+.b {
+  margin: 20px;
+}
+.c {
+  transform: translate(11px, -11px);
+}
+.d {
+  transform: skewx(-45deg);
+}
+
+</style>
+<script>
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+
+function testTransformToParent() {
+  let expectedData = [
+    ["boxA", "1,0,0,0,0,1,0,0,0,0,1,0,10,0,0,1"],
+    ["boxB", "1,0,0,0,0,1,0,0,0,0,1,0,20,0,0,1"],
+    ["boxC", "1,0,0,0,0,1,0,0,0,0,1,0,11,-11,0,1"],
+    ["boxD", "1,0,0,0,-1,1,0,0,0,0,1,0,10,0,0,1"],
+  ];
+
+  // Test transform to parent.
+  for (let i = 0; i < expectedData.length; ++i) {
+    let expected = expectedData[i];
+    let element = document.getElementById(expected[0]);
+
+    let transform = element.getTransformToParent();
+    let transformFloats = transform.toFloat32Array();
+    let transformString = transformFloats.toString();
+    is(transformString, expected[1], "Element " + expected[0] + " has expected transform to parent.");
+  }
+}
+
+function testTransformToAncestorAndViewport() {
+  let expectedData = [
+    ["boxA", "1,0,0,0,0,1,0,0,0,0,1,0,10,10,0,1"],
+    ["boxB", "1,0,0,0,0,1,0,0,0,0,1,0,20,50,0,1"],
+    ["boxC", "1,0,0,0,0,1,0,0,0,0,1,0,11,79,0,1"],
+  ];
+
+  // Test transform to document (an actual ancestor unchanged by embedding within the mochitest framework).
+  for (let i = 0; i < expectedData.length; ++i) {
+    let expected = expectedData[i];
+    let element = document.getElementById(expected[0]);
+
+    let transform = element.getTransformToAncestor(document.documentElement);
+    let transformFloats = transform.toFloat32Array();
+    let transformString = transformFloats.toString();
+    is(transformString, expected[1], "Element " + expected[0] + " has expected transform to ancestor.");
+  }
+
+  // Test transform to a non-ancestor is equivalent to transform to viewport.
+  let nonAncestorElement = document.getElementById("nonAncestor");
+  for (let i = 0; i < expectedData.length; ++i) {
+    let expected = expectedData[i];
+    let element = document.getElementById(expected[0]);
+
+    let transform = element.getTransformToAncestor(nonAncestorElement);
+    let transformFloats = transform.toFloat32Array();
+    let transformString = transformFloats.toString();
+
+    let transformToViewport = element.getTransformToViewport();
+    let transformToViewportFloats = transformToViewport.toFloat32Array();
+    let transformToViewportString = transformToViewportFloats.toString();
+    is(transformString, transformToViewportString, "Element " + expected[0] + " transform to non-ancestor is equivalent to transform to viewport.");
+  }
+}
+
+function runTests() {
+  testTransformToParent();
+  testTransformToAncestorAndViewport();
+
+  SimpleTest.finish();
+}
+</script>
+</head>
+<body onLoad="runTests();">
+
+<div id="boxAParent">
+  <div id="boxA" class="box a">boxA</div>
+</div>
+<div id="boxBParent">
+  <div id="boxB" class="box b">boxB</div>
+</div>
+<div id="boxCParent">
+  <div id="boxC" class="box c">boxC</div>
+</div>
+<div id="boxDParent">
+  <div id="boxD" class="box d">boxD</div>
+</div>
+
+<div id="nonAncestor">This div is not an ancestor of any of the boxes.</div>
+
+</body>
+</html>
\ No newline at end of file
--- a/dom/vr/VRDisplay.cpp
+++ b/dom/vr/VRDisplay.cpp
@@ -530,34 +530,42 @@ VRDisplay::RequestPresent(const nsTArray
   if (!global) {
     aRv.Throw(NS_ERROR_FAILURE);
     return nullptr;
   }
 
   RefPtr<Promise> promise = Promise::Create(global, aRv);
   NS_ENSURE_TRUE(!aRv.Failed(), nullptr);
 
+  bool isChromePresentation = aCallerType == CallerType::System;
+  uint32_t presentationGroup = isChromePresentation ? gfx::kVRGroupChrome : gfx::kVRGroupContent;
+
   if (!EventStateManager::IsHandlingUserInput() &&
-      aCallerType != CallerType::System &&
+      !isChromePresentation &&
       !IsHandlingVRNavigationEvent() &&
       gfxPrefs::VRRequireGesture()) {
     // The WebVR API states that if called outside of a user gesture, the
     // promise must be rejected.  We allow VR presentations to start within
     // trusted events such as vrdisplayactivate, which triggers in response to
     // HMD proximity sensors and when navigating within a VR presentation.
+    // This user gesture requirement is not enforced for chrome/system code.
     promise->MaybeRejectWithUndefined();
-  } else if (!IsPresenting() && IsAnyPresenting()) {
-    // Only one presentation allowed per VRDisplay
-    // on a first-come-first-serve basis.
+  } else if (!IsPresenting() && IsAnyPresenting(presentationGroup)) {
+    // Only one presentation allowed per VRDisplay on a
+    // first-come-first-serve basis.
     // If this Javascript context is presenting, then we can replace our
     // presentation with a new one containing new layers but we should never
     // replace the presentation of another context.
+    // Simultaneous presentations in other groups are allowed in separate
+    // Javascript contexts to enable browser UI from chrome/system contexts.
+    // Eventually, this restriction will be loosened to enable multitasking
+    // use cases.
     promise->MaybeRejectWithUndefined();
   } else {
-    mPresentation = mClient->BeginPresentation(aLayers);
+    mPresentation = mClient->BeginPresentation(aLayers, presentationGroup);
     mFrameInfo.Clear();
     promise->MaybeResolve(JS::UndefinedHandleValue);
   }
   return promise.forget();
 }
 
 NS_IMETHODIMP
 VRDisplay::Observe(nsISupports* aSubject, const char* aTopic,
@@ -672,29 +680,54 @@ bool
 VRDisplay::IsPresenting() const
 {
   // IsPresenting returns true only if this Javascript context is presenting
   // and will return false if another context is presenting.
   return mPresentation != nullptr;
 }
 
 bool
-VRDisplay::IsAnyPresenting() const
+VRDisplay::IsAnyPresenting(uint32_t aGroupMask) const
 {
-  // IsAnyPresenting returns true if any Javascript context is presenting
-  // even if this context is not presenting.
-  return IsPresenting() || mClient->GetIsPresenting();
+  // IsAnyPresenting returns true if either this VRDisplay object or any other
+  // from anther Javascript context is presenting with a group matching
+  // aGroupMask.
+  if (mPresentation && (mPresentation->GetGroup() & aGroupMask)) {
+    return true;
+  }
+  if (mClient->GetDisplayInfo().GetPresentingGroups() & aGroupMask) {
+    return true;
+  }
+  return false;
 }
 
 bool
 VRDisplay::IsConnected() const
 {
   return mClient->GetIsConnected();
 }
 
+uint32_t
+VRDisplay::PresentingGroups() const
+{
+  return mClient->GetDisplayInfo().GetPresentingGroups();
+}
+
+uint32_t
+VRDisplay::GroupMask() const
+{
+  return mClient->GetDisplayInfo().GetGroupMask();
+}
+
+void
+VRDisplay::SetGroupMask(const uint32_t& aGroupMask)
+{
+  mClient->SetGroupMask(aGroupMask);
+}
+
 NS_IMPL_CYCLE_COLLECTION_INHERITED(VRDisplay, DOMEventTargetHelper, mCapabilities, mStageParameters)
 
 NS_IMPL_ADDREF_INHERITED(VRDisplay, DOMEventTargetHelper)
 NS_IMPL_RELEASE_INHERITED(VRDisplay, DOMEventTargetHelper)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(VRDisplay)
 NS_INTERFACE_MAP_ENTRY(nsIObserver)
 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, DOMEventTargetHelper)
--- a/dom/vr/VRDisplay.h
+++ b/dom/vr/VRDisplay.h
@@ -303,18 +303,21 @@ class VRDisplay final : public DOMEventT
 {
 public:
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_NSIOBSERVER
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(VRDisplay, DOMEventTargetHelper)
 
   virtual JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
+  uint32_t PresentingGroups() const;
+  uint32_t GroupMask() const;
+  void SetGroupMask(const uint32_t& aGroupMask);
+  bool IsAnyPresenting(uint32_t aGroupMask) const;
   bool IsPresenting() const;
-  bool IsAnyPresenting() const;
   bool IsConnected() const;
 
   VRDisplayCapabilities* Capabilities();
   VRStageParameters* GetStageParameters();
 
   uint32_t DisplayId() const { return mDisplayId; }
   void GetDisplayName(nsAString& aDisplayName) const { aDisplayName = mDisplayName; }
 
--- a/dom/webidl/Element.webidl
+++ b/dom/webidl/Element.webidl
@@ -160,16 +160,23 @@ interface Element : Node {
 
   /**
    * If this element has a display:grid or display:inline-grid style,
    * this property returns an object with computed values for grid
    * tracks and lines.
    */
   [ChromeOnly, Pure]
   sequence<Grid> getGridFragments();
+
+  [ChromeOnly]
+  DOMMatrixReadOnly getTransformToAncestor(Element ancestor);
+  [ChromeOnly]
+  DOMMatrixReadOnly getTransformToParent();
+  [ChromeOnly]
+  DOMMatrixReadOnly getTransformToViewport();
 };
 
 // http://dev.w3.org/csswg/cssom-view/
 enum ScrollLogicalPosition { "start", "end" };
 dictionary ScrollIntoViewOptions : ScrollOptions {
   ScrollLogicalPosition block = "start";
 };
 
--- a/dom/webidl/Navigator.webidl
+++ b/dom/webidl/Navigator.webidl
@@ -268,16 +268,22 @@ partial interface Navigator {
 };
 
 partial interface Navigator {
   [Throws, Pref="dom.vr.enabled"]
   Promise<sequence<VRDisplay>> getVRDisplays();
   // TODO: Use FrozenArray once available. (Bug 1236777)
   [Frozen, Cached, Pure, Pref="dom.vr.enabled"]
   readonly attribute sequence<VRDisplay> activeVRDisplays;
+  [ChromeOnly, Pref="dom.vr.enabled"]
+  readonly attribute boolean isWebVRContentDetected;
+  [ChromeOnly, Pref="dom.vr.enabled"]
+  readonly attribute boolean isWebVRContentPresenting;
+  [ChromeOnly, Pref="dom.vr.enabled"]
+  void requestVRPresentation(VRDisplay display);
 };
 partial interface Navigator {
   [Pref="dom.vr.test.enabled"]
   VRServiceTest requestVRServiceTest();
 };
 
 #ifdef MOZ_TIME_MANAGER
 // nsIDOMMozNavigatorTime
--- a/dom/webidl/VRDisplay.webidl
+++ b/dom/webidl/VRDisplay.webidl
@@ -178,16 +178,36 @@ interface VREyeParameters {
    */
   [Constant] readonly attribute unsigned long renderWidth;
   [Constant] readonly attribute unsigned long renderHeight;
 };
 
 [Pref="dom.vr.enabled",
  HeaderFile="mozilla/dom/VRDisplay.h"]
 interface VRDisplay : EventTarget {
+  /**
+   * presentingGroups is a bitmask indicating which VR session groups
+   * have an active VR presentation.
+   */
+  [ChromeOnly] readonly attribute unsigned long presentingGroups;
+  /**
+   * Setting groupMask causes submitted frames by VR sessions that
+   * aren't included in the bitmasked groups to be ignored.
+   * Non-chrome content is not aware of the value of groupMask.
+   * VRDisplay.RequestAnimationFrame will still fire for VR sessions
+   * that are hidden by groupMask, enabling their performance to be
+   * measured by chrome UI that is presented in other groups.
+   * This is expected to be used in cases where chrome UI is presenting
+   * information during link traversal or presenting options when content
+   * performance is too low for comfort.
+   * The VR refresh / VSync cycle is driven by the visible content
+   * and the non-visible content may have a throttled refresh rate.
+   */
+  [ChromeOnly] attribute unsigned long groupMask;
+
   readonly attribute boolean isConnected;
   readonly attribute boolean isPresenting;
 
   /**
    * Dictionary of capabilities describing the VRDisplay.
    */
   [Constant] readonly attribute VRDisplayCapabilities capabilities;
 
--- a/dom/webidl/XMLHttpRequest.webidl
+++ b/dom/webidl/XMLHttpRequest.webidl
@@ -150,11 +150,16 @@ interface XMLHttpRequest : XMLHttpReques
   attribute ByteString? networkInterfaceId;
 
   [Throws, ChromeOnly, Exposed=Window]
   any getInterface(IID iid);
 
   [ChromeOnly, Exposed=Window]
   void setOriginAttributes(optional OriginAttributesDictionary originAttributes);
 
+  // Only works on MainThread.
+  // Its permanence is to be evaluated in bug 1368540 for Firefox 60.
+  [ChromeOnly]
+  readonly attribute unsigned short errorCode;
+
   readonly attribute boolean mozAnon;
   readonly attribute boolean mozSystem;
 };
--- a/dom/xhr/XMLHttpRequest.h
+++ b/dom/xhr/XMLHttpRequest.h
@@ -163,16 +163,19 @@ public:
   virtual void
   GetInterface(JSContext* aCx, nsIJSID* aIID,
                JS::MutableHandle<JS::Value> aRetval,
                ErrorResult& aRv) = 0;
 
   virtual void
   SetOriginAttributes(const mozilla::dom::OriginAttributesDictionary& aAttrs) = 0;
 
+  virtual uint16_t
+  ErrorCode() const = 0;
+
   virtual bool
   MozAnon() const = 0;
 
   virtual bool
   MozSystem() const = 0;
 
   virtual JSObject*
   WrapObject(JSContext *aCx, JS::Handle<JSObject*> aGivenProto) override
--- a/dom/xhr/XMLHttpRequestMainThread.cpp
+++ b/dom/xhr/XMLHttpRequestMainThread.cpp
@@ -184,17 +184,17 @@ XMLHttpRequestMainThread::XMLHttpRequest
     mState(State::unsent),
     mFlagSynchronous(false), mFlagAborted(false), mFlagParseBody(false),
     mFlagSyncLooping(false), mFlagBackgroundRequest(false),
     mFlagHadUploadListenersOnSend(false), mFlagACwithCredentials(false),
     mFlagTimedOut(false), mFlagDeleted(false), mFlagSend(false),
     mUploadTransferred(0), mUploadTotal(0), mUploadComplete(true),
     mProgressSinceLastProgressEvent(false),
     mRequestSentTime(0), mTimeoutMilliseconds(0),
-    mErrorLoad(false), mErrorParsingXML(false),
+    mErrorLoad(ErrorType::eOK), mErrorParsingXML(false),
     mWaitingForOnStopRequest(false),
     mProgressTimerIsActive(false),
     mIsHtml(false),
     mWarnAboutSyncHtml(false),
     mLoadTotal(-1),
     mIsSystem(false),
     mIsAnon(false),
     mFirstStartRequestSeen(false),
@@ -937,17 +937,17 @@ XMLHttpRequestMainThread::GetStatus(Erro
     return 0;
   }
 
   uint16_t readyState = ReadyState();
   if (readyState == UNSENT || readyState == OPENED) {
     return 0;
   }
 
-  if (mErrorLoad) {
+  if (mErrorLoad != ErrorType::eOK) {
     // Let's simulate the http protocol for jar/app requests:
     nsCOMPtr<nsIJARChannel> jarChannel = GetCurrentJARChannel();
     if (jarChannel) {
       nsresult status;
       mChannel->GetStatus(&status);
 
       if (status == NS_ERROR_FILE_NOT_FOUND) {
         return 404; // Not Found
@@ -999,17 +999,17 @@ XMLHttpRequestMainThread::GetStatusText(
   // value.  This check is to prevent the status text for redirects from being
   // available before all the redirects have been followed and HTTP headers have
   // been received.
   uint16_t readyState = ReadyState();
   if (readyState == UNSENT || readyState == OPENED) {
     return;
   }
 
-  if (mErrorLoad) {
+  if (mErrorLoad != ErrorType::eOK) {
     return;
   }
 
   nsCOMPtr<nsIHttpChannel> httpChannel = GetCurrentHttpChannel();
   if (httpChannel) {
     Unused << httpChannel->GetResponseStatusText(aStatusText);
   } else {
     aStatusText.AssignLiteral("OK");
@@ -1216,17 +1216,17 @@ XMLHttpRequestMainThread::GetAllResponse
   aResponseHeaders.Truncate();
 
   // If the state is UNSENT or OPENED,
   // return the empty string and terminate these steps.
   if (mState == State::unsent || mState == State::opened) {
     return;
   }
 
-  if (mErrorLoad) {
+  if (mErrorLoad != ErrorType::eOK) {
     return;
   }
 
   if (nsCOMPtr<nsIHttpChannel> httpChannel = GetCurrentHttpChannel()) {
     RefPtr<nsHeaderVisitor> visitor =
       new nsHeaderVisitor(*this, WrapNotNull(httpChannel));
     if (NS_SUCCEEDED(httpChannel->VisitResponseHeaders(visitor))) {
       aResponseHeaders = visitor->Headers();
@@ -1938,21 +1938,23 @@ XMLHttpRequestMainThread::OnStartRequest
     return NS_OK;
   }
 
   nsCOMPtr<nsIChannel> channel(do_QueryInterface(request));
   NS_ENSURE_TRUE(channel, NS_ERROR_UNEXPECTED);
 
   nsresult status;
   request->GetStatus(&status);
-  mErrorLoad = mErrorLoad || NS_FAILED(status);
+  if (mErrorLoad == ErrorType::eOK && NS_FAILED(status)) {
+    mErrorLoad = ErrorType::eRequest;
+  }
 
   // Upload phase is now over. If we were uploading anything,
   // stop the timer and fire any final progress events.
-  if (mUpload && !mUploadComplete && !mErrorLoad && !mFlagSynchronous) {
+  if (mUpload && !mUploadComplete && mErrorLoad == ErrorType::eOK && !mFlagSynchronous) {
     StopProgressEventTimer();
 
     mUploadTransferred = mUploadTotal;
 
     if (mProgressSinceLastProgressEvent) {
       DispatchProgressEvent(mUpload, ProgressEventType::progress,
                             mUploadTransferred, mUploadTotal);
       mProgressSinceLastProgressEvent = false;
@@ -2320,17 +2322,17 @@ XMLHttpRequestMainThread::OnStopRequest(
   // update our charset and decoder to match mResponseXML,
   // before it is possibly nulled out
   MatchCharsetAndDecoderToResponseDocument();
 
   if (NS_FAILED(status)) {
     // This can happen if the server is unreachable. Other possible
     // reasons are that the user leaves the page or hits the ESC key.
 
-    mErrorLoad = true;
+    mErrorLoad = ErrorType::eUnreachable;
     mResponseXML = nullptr;
   }
 
   // If we're uninitialized at this point, we encountered an error
   // earlier and listeners have already been notified. Also we do
   // not want to do this if we already completed.
   if (mState == State::unsent || mState == State::done) {
     return NS_OK;
@@ -2425,23 +2427,24 @@ XMLHttpRequestMainThread::ChangeStateToD
   // Per spec, if we failed in the upload phase, fire a final error
   // and loadend events for the upload after readystatechange=4/done.
   if (!mFlagSynchronous && mUpload && !mUploadComplete) {
     DispatchProgressEvent(mUpload, ProgressEventType::error, 0, -1);
   }
 
   // Per spec, fire download's load/error and loadend events after
   // readystatechange=4/done (and of course all upload events).
-  DispatchProgressEvent(this,
-                        mErrorLoad ? ProgressEventType::error :
-                                     ProgressEventType::load,
-                        mErrorLoad ? 0 : mLoadTransferred,
-                        mErrorLoad ? -1 : mLoadTotal);
-
-  if (mErrorLoad) {
+  if (mErrorLoad != ErrorType::eOK) {
+    DispatchProgressEvent(this, ProgressEventType::error, 0, -1);
+  } else {
+    DispatchProgressEvent(this, ProgressEventType::load,
+                          mLoadTransferred, mLoadTotal);
+  }
+
+  if (mErrorLoad != ErrorType::eOK) {
     // By nulling out channel here we make it so that Send() can test
     // for that and throw. Also calling the various status
     // methods/members will not throw.
     // This matches what IE does.
     mChannel = nullptr;
   }
 }
 
@@ -2780,17 +2783,17 @@ XMLHttpRequestMainThread::InitiateFetch(
   rv = mChannel->AsyncOpen2(listener);
   listener = nullptr;
   if (NS_WARN_IF(NS_FAILED(rv))) {
     // Drop our ref to the channel to avoid cycles. Also drop channel's
     // ref to us to be extra safe.
     mChannel->SetNotificationCallbacks(mNotificationCallbacks);
     mChannel = nullptr;
 
-    mErrorLoad = true;
+    mErrorLoad = ErrorType::eChannelOpen;
 
     // Per spec, we throw on sync errors, but not async.
     if (mFlagSynchronous) {
       mState = State::done;
       return NS_ERROR_DOM_NETWORK_ERR;
     }
   }
 
@@ -2928,17 +2931,17 @@ XMLHttpRequestMainThread::SendInternal(c
   // XXX We should probably send a warning to the JS console
   //     if there are no event listeners set and we are doing
   //     an asynchronous call.
 
   mUploadTransferred = 0;
   mUploadTotal = 0;
   // By default we don't have any upload, so mark upload complete.
   mUploadComplete = true;
-  mErrorLoad = false;
+  mErrorLoad = ErrorType::eOK;
   mLoadTotal = -1;
   nsCOMPtr<nsIInputStream> uploadStream;
   nsAutoCString uploadContentType;
   nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mChannel));
   if (aBody && httpChannel &&
       !mRequestMethod.EqualsLiteral("GET") &&
       !mRequestMethod.EqualsLiteral("HEAD")) {
 
@@ -3440,17 +3443,17 @@ XMLHttpRequestMainThread::OnRedirectVeri
     mChannel = mNewRedirectChannel;
 
     nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(mChannel));
     if (httpChannel) {
       // Ensure all original headers are duplicated for the new channel (bug #553888)
       mAuthorRequestHeaders.ApplyToChannel(httpChannel);
     }
   } else {
-    mErrorLoad = true;
+    mErrorLoad = ErrorType::eRedirect;
   }
 
   mNewRedirectChannel = nullptr;
 
   mRedirectCallback->OnRedirectVerifyCallback(result);
   mRedirectCallback = nullptr;
 
   // It's important that we return success here. If we return the result code
@@ -3685,17 +3688,17 @@ XMLHttpRequestMainThread::HandleProgress
 {
   // Don't fire the progress event if mLoadTotal is 0, see XHR spec step 6.1
   if (!mLoadTotal && mLoadTransferred) {
     return;
   }
 
   mProgressTimerIsActive = false;
 
-  if (!mProgressSinceLastProgressEvent || mErrorLoad) {
+  if (!mProgressSinceLastProgressEvent || mErrorLoad != ErrorType::eOK) {
     return;
   }
 
   if (InUploadPhase()) {
     if (mUpload && !mUploadComplete) {
       DispatchProgressEvent(mUpload, ProgressEventType::progress,
                             mUploadTransferred, mUploadTotal);
     }
--- a/dom/xhr/XMLHttpRequestMainThread.h
+++ b/dom/xhr/XMLHttpRequestMainThread.h
@@ -179,16 +179,25 @@ public:
     error,
     abort,
     timeout,
     load,
     loadend,
     ENUM_MAX
   };
 
+  enum class ErrorType : uint16_t {
+    eOK,
+    eRequest,
+    eUnreachable,
+    eChannelOpen,
+    eRedirect,
+    ENUM_MAX
+  };
+
   XMLHttpRequestMainThread();
 
   void Construct(nsIPrincipal* aPrincipal,
                  nsIGlobalObject* aGlobalObject,
                  nsIURI* aBaseURI = nullptr,
                  nsILoadGroup* aLoadGroup = nullptr)
   {
     MOZ_ASSERT(aPrincipal);
@@ -465,16 +474,22 @@ public:
   GetResponseXML(ErrorResult& aRv) override;
 
   virtual bool
   MozBackgroundRequest() const override;
 
   virtual void
   SetMozBackgroundRequest(bool aMozBackgroundRequest, ErrorResult& aRv) override;
 
+  virtual uint16_t
+  ErrorCode() const override
+  {
+    return static_cast<uint16_t>(mErrorLoad);
+  }
+
   virtual bool
   MozAnon() const override;
 
   virtual bool
   MozSystem() const override;
 
   virtual nsIChannel*
   GetChannel() const override
@@ -754,17 +769,17 @@ protected:
     eTimerStarted,
     eNoTimerNeeded
   };
 
   SyncTimeoutType MaybeStartSyncTimeoutTimer();
   void HandleSyncTimeoutTimer();
   void CancelSyncTimeoutTimer();
 
-  bool mErrorLoad;
+  ErrorType mErrorLoad;
   bool mErrorParsingXML;
   bool mWaitingForOnStopRequest;
   bool mProgressTimerIsActive;
   bool mIsHtml;
   bool mWarnAboutMultipartHtml;
   bool mWarnAboutSyncHtml;
   int64_t mLoadTotal; // -1 if not known.
   // Amount of script-exposed (i.e. after undoing gzip compresion) data
--- a/dom/xhr/XMLHttpRequestWorker.h
+++ b/dom/xhr/XMLHttpRequestWorker.h
@@ -280,16 +280,21 @@ public:
 
   void
   NullResponseText()
   {
     mStateData.mResponseText.SetVoid();
     mStateData.mResponse.setNull();
   }
 
+  virtual uint16_t ErrorCode() const override
+  {
+    return 0; // eOK
+  }
+
   virtual bool MozAnon() const override
   {
     return mMozAnon;
   }
 
   virtual bool MozSystem() const override
   {
     return mMozSystem;
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -974,26 +974,30 @@ APZCTreeManager::ReceiveInputEvent(Input
               thumbTransform = ComputeTransformForNode(hitScrollbarNode);
             }
             // Only consider the translation, since we do not support both
             // zooming and scrollbar dragging on any platform.
             CSSCoord thumbStart = thumbData.mThumbStart
                                 + ((thumbData.mDirection == ScrollDirection::HORIZONTAL)
                                    ? thumbTransform._41 : thumbTransform._42);
             dragStart -= thumbStart;
+
+            // Content can't prevent scrollbar dragging with preventDefault(),
+            // so we don't need to wait for a content response. It's important
+            // to do this before calling ConfirmDragBlock() since that can
+            // potentially process and consume the block.
+            dragBlock->SetContentResponse(false);
+
             mInputQueue->ConfirmDragBlock(
                 dragBlockId, apzc,
                 AsyncDragMetrics(apzc->GetGuid().mScrollId,
                                  apzc->GetGuid().mPresShellId,
                                  dragBlockId,
                                  dragStart,
                                  thumbData.mDirection));
-            // Content can't prevent scrollbar dragging with preventDefault(),
-            // so we don't need to wait for a content response.
-            dragBlock->SetContentResponse(false);
           }
         }
 
         if (result == nsEventStatus_eConsumeDoDefault) {
           // This input event is part of a drag block, so whether or not it is
           // directed at a scrollbar depends on whether the drag block started
           // on a scrollbar.
           hitScrollbar = mInputQueue->IsDragOnScrollbar(hitScrollbar);
--- a/gfx/vr/VRDisplayClient.cpp
+++ b/gfx/vr/VRDisplayClient.cpp
@@ -27,83 +27,67 @@
 using namespace mozilla;
 using namespace mozilla::gfx;
 
 VRDisplayClient::VRDisplayClient(const VRDisplayInfo& aDisplayInfo)
   : mDisplayInfo(aDisplayInfo)
   , bLastEventWasMounted(false)
   , bLastEventWasPresenting(false)
   , mPresentationCount(0)
+  , mLastEventFrameId(0)
 {
   MOZ_COUNT_CTOR(VRDisplayClient);
 }
 
 VRDisplayClient::~VRDisplayClient() {
   MOZ_COUNT_DTOR(VRDisplayClient);
 }
 
 void
 VRDisplayClient::UpdateDisplayInfo(const VRDisplayInfo& aDisplayInfo)
 {
   mDisplayInfo = aDisplayInfo;
+  FireEvents();
 }
 
 already_AddRefed<VRDisplayPresentation>
-VRDisplayClient::BeginPresentation(const nsTArray<mozilla::dom::VRLayer>& aLayers)
+VRDisplayClient::BeginPresentation(const nsTArray<mozilla::dom::VRLayer>& aLayers,
+                                   uint32_t aGroup)
 {
   ++mPresentationCount;
-  RefPtr<VRDisplayPresentation> presentation = new VRDisplayPresentation(this, aLayers);
+  RefPtr<VRDisplayPresentation> presentation = new VRDisplayPresentation(this, aLayers, aGroup);
   return presentation.forget();
 }
 
 void
 VRDisplayClient::PresentationDestroyed()
 {
   --mPresentationCount;
 }
 
 void
 VRDisplayClient::ZeroSensor()
 {
   VRManagerChild *vm = VRManagerChild::Get();
   vm->SendResetSensor(mDisplayInfo.mDisplayID);
 }
 
-VRHMDSensorState
-VRDisplayClient::GetSensorState()
-{
-  VRHMDSensorState sensorState;
-  VRManagerChild *vm = VRManagerChild::Get();
-  Unused << vm->SendGetSensorState(mDisplayInfo.mDisplayID, &sensorState);
-  return sensorState;
-}
-
-const double kVRDisplayRAFMaxDuration = 32; // milliseconds
-
 void
-VRDisplayClient::NotifyVsync()
+VRDisplayClient::SetGroupMask(uint32_t aGroupMask)
 {
   VRManagerChild *vm = VRManagerChild::Get();
-
-  bool isPresenting = GetIsPresenting();
+  vm->SendSetGroupMask(mDisplayInfo.mDisplayID, aGroupMask);
+}
 
-  bool bShouldCallback = !isPresenting;
-  if (mLastVSyncTime.IsNull()) {
-    bShouldCallback = true;
-  } else {
-    TimeDuration duration = TimeStamp::Now() - mLastVSyncTime;
-    if (duration.ToMilliseconds() > kVRDisplayRAFMaxDuration) {
-      bShouldCallback = true;
-    }
-  }
-
-  if (bShouldCallback) {
-    vm->RunFrameRequestCallbacks();
-    mLastVSyncTime = TimeStamp::Now();
-  }
+void
+VRDisplayClient::FireEvents()
+{
+  VRManagerChild *vm = VRManagerChild::Get();
+  // Only fire these events for non-chrome VR sessions
+  bool isPresenting = (mDisplayInfo.mPresentingGroups & kVRGroupContent) != 0;
 
   // Check if we need to trigger onVRDisplayPresentChange event
   if (bLastEventWasPresenting != isPresenting) {
     bLastEventWasPresenting = isPresenting;
     vm->FireDOMVRDisplayPresentChangeEvent(mDisplayInfo.mDisplayID);
   }
 
   // Check if we need to trigger onvrdisplayactivate event
@@ -116,38 +100,36 @@ VRDisplayClient::NotifyVsync()
 
   // Check if we need to trigger onvrdisplaydeactivate event
   if (bLastEventWasMounted && !mDisplayInfo.mIsMounted) {
     bLastEventWasMounted = false;
     if (gfxPrefs::VRAutoActivateEnabled()) {
       vm->FireDOMVRDisplayUnmountedEvent(mDisplayInfo.mDisplayID);
     }
   }
+
+  // Check if we need to trigger VRDisplay.requestAnimationFrame
+  if (mLastEventFrameId != mDisplayInfo.mFrameId) {
+    mLastEventFrameId = mDisplayInfo.mFrameId;
+    vm->RunFrameRequestCallbacks();
+  }
 }
 
-void
-VRDisplayClient::NotifyVRVsync()
+VRHMDSensorState
+VRDisplayClient::GetSensorState()
 {
-  VRManagerChild *vm = VRManagerChild::Get();
-  vm->RunFrameRequestCallbacks();
-  mLastVSyncTime = TimeStamp::Now();
+  return mDisplayInfo.GetSensorState();
 }
 
 bool
 VRDisplayClient::GetIsConnected() const
 {
   return mDisplayInfo.GetIsConnected();
 }
 
-bool
-VRDisplayClient::GetIsPresenting() const
-{
-  return mDisplayInfo.GetIsPresenting();
-}
-
 void
 VRDisplayClient::NotifyDisconnected()
 {
   mDisplayInfo.mIsConnected = false;
 }
 
 void
 VRDisplayClient::UpdateSubmitFrameResult(const VRSubmitFrameResultInfo& aResult)
--- a/gfx/vr/VRDisplayClient.h
+++ b/gfx/vr/VRDisplayClient.h
@@ -29,38 +29,37 @@ public:
   void UpdateSubmitFrameResult(const VRSubmitFrameResultInfo& aResult);
 
   const VRDisplayInfo& GetDisplayInfo() const { return mDisplayInfo; }
   virtual VRHMDSensorState GetSensorState();
   void GetSubmitFrameResult(VRSubmitFrameResultInfo& aResult);
 
   virtual void ZeroSensor();
 
-  already_AddRefed<VRDisplayPresentation> BeginPresentation(const nsTArray<dom::VRLayer>& aLayers);
+  already_AddRefed<VRDisplayPresentation> BeginPresentation(const nsTArray<dom::VRLayer>& aLayers,
+                                                            uint32_t aGroup);
   void PresentationDestroyed();
 
-  void NotifyVsync();
-  void NotifyVRVsync();
-
   bool GetIsConnected() const;
-  bool GetIsPresenting() const;
 
   void NotifyDisconnected();
+  void SetGroupMask(uint32_t aGroupMask);
 
 protected:
   virtual ~VRDisplayClient();
 
+  void FireEvents();
+
   VRDisplayInfo mDisplayInfo;
 
   bool bLastEventWasMounted;
   bool bLastEventWasPresenting;
 
-  TimeStamp mLastVSyncTime;
   int mPresentationCount;
-
+  uint32_t mLastEventFrameId;
 private:
   VRSubmitFrameResultInfo mSubmitFrameResult;
 };
 
 } // namespace gfx
 } // namespace mozilla
 
 #endif /* GFX_VR_DISPLAY_CLIENT_H */
--- a/gfx/vr/VRDisplayHost.cpp
+++ b/gfx/vr/VRDisplayHost.cpp
@@ -1,101 +1,165 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * 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/. */
 
 #include "VRDisplayHost.h"
 #include "gfxVR.h"
+#include "ipc/VRLayerParent.h"
 
 #if defined(XP_WIN)
 
 #include <d3d11.h>
 #include "gfxWindowsPlatform.h"
 #include "../layers/d3d11/CompositorD3D11.h"
 #include "mozilla/layers/TextureD3D11.h"
 
 #endif
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 using namespace mozilla::layers;
 
 VRDisplayHost::VRDisplayHost(VRDeviceType aType)
-  : mInputFrameID(0)
 {
   MOZ_COUNT_CTOR(VRDisplayHost);
   mDisplayInfo.mType = aType;
   mDisplayInfo.mDisplayID = VRSystemManager::AllocateDisplayID();
-  mDisplayInfo.mIsPresenting = false;
+  mDisplayInfo.mPresentingGroups = 0;
+  mDisplayInfo.mGroupMask = kVRGroupContent;
+  mDisplayInfo.mFrameId = 0;
 }
 
 VRDisplayHost::~VRDisplayHost()
 {
   MOZ_COUNT_DTOR(VRDisplayHost);
 }
 
 void
+VRDisplayHost::SetGroupMask(uint32_t aGroupMask)
+{
+  mDisplayInfo.mGroupMask = aGroupMask;
+}
+
+void
 VRDisplayHost::AddLayer(VRLayerParent *aLayer)
 {
   mLayers.AppendElement(aLayer);
+  mDisplayInfo.mPresentingGroups |= aLayer->GetGroup();
   if (mLayers.Length() == 1) {
     StartPresentation();
   }
-  mDisplayInfo.mIsPresenting = mLayers.Length() > 0;
 
   // Ensure that the content process receives the change immediately
   VRManager* vm = VRManager::Get();
   vm->RefreshVRDisplays();
 }
 
 void
 VRDisplayHost::RemoveLayer(VRLayerParent *aLayer)
 {
   mLayers.RemoveElement(aLayer);
   if (mLayers.Length() == 0) {
     StopPresentation();
   }
-  mDisplayInfo.mIsPresenting = mLayers.Length() > 0;
+  mDisplayInfo.mPresentingGroups = 0;
+  for (auto layer : mLayers) {
+    mDisplayInfo.mPresentingGroups |= layer->GetGroup();
+  }
 
   // Ensure that the content process receives the change immediately
   VRManager* vm = VRManager::Get();
   vm->RefreshVRDisplays();
 }
 
+void
+VRDisplayHost::StartFrame()
+{
+  mLastFrameStart = TimeStamp::Now();
+  ++mDisplayInfo.mFrameId;
+  mDisplayInfo.mLastSensorState[mDisplayInfo.mFrameId % kVRMaxLatencyFrames] = GetSensorState();
+}
+
+void
+VRDisplayHost::NotifyVSync()
+{
+  /**
+   * We will trigger a new frame immediately after a successful frame texture
+   * submission.  If content fails to call VRDisplay.submitFrame after
+   * kVRDisplayRAFMaxDuration milliseconds has elapsed since the last
+   * VRDisplay.requestAnimationFrame, we act as a "watchdog" and kick-off
+   * a new VRDisplay.requestAnimationFrame to avoid a render loop stall and
+   * to give content a chance to recover.
+   *
+   * If the lower level VR platform API's are rejecting submitted frames,
+   * such as when the Oculus "Health and Safety Warning" is displayed,
+   * we will not kick off the next frame immediately after VRDisplay.submitFrame
+   * as it would result in an unthrottled render loop that would free run at
+   * potentially extreme frame rates.  To ensure that content has a chance to
+   * resume its presentation when the frames are accepted once again, we rely
+   * on this "watchdog" to act as a VR refresh driver cycling at a rate defined
+   * by kVRDisplayRAFMaxDuration.
+   *
+   * kVRDisplayRAFMaxDuration is the number of milliseconds since last frame
+   * start before triggering a new frame.  When content is failing to submit
+   * frames on time or the lower level VR platform API's are rejecting frames,
+   * kVRDisplayRAFMaxDuration determines the rate at which RAF callbacks
+   * will be called.
+   *
+   * This number must be larger than the slowest expected frame time during
+   * normal VR presentation, but small enough not to break content that
+   * makes assumptions of reasonably minimal VSync rate.
+   *
+   * The slowest expected refresh rate for a VR display currently is an
+   * Oculus CV1 when ASW (Asynchronous Space Warp) is enabled, at 45hz.
+   * A kVRDisplayRAFMaxDuration value of 50 milliseconds results in a 20hz
+   * rate, which avoids inadvertent triggering of the watchdog during
+   * Oculus ASW even if every second frame is dropped.
+   */
+  const double kVRDisplayRAFMaxDuration = 50;
+
+  bool bShouldStartFrame = false;
+
+  if (mDisplayInfo.mPresentingGroups == 0) {
+    // If this display isn't presenting, refresh the sensors and trigger
+    // VRDisplay.requestAnimationFrame at the normal 2d display refresh rate.
+    bShouldStartFrame = true;
+  } else {
+    // If content fails to call VRDisplay.submitFrame, we must eventually
+    // time-out and trigger a new frame.
+    if (mLastFrameStart.IsNull()) {
+      bShouldStartFrame = true;
+    } else {
+      TimeDuration duration = TimeStamp::Now() - mLastFrameStart;
+      if (duration.ToMilliseconds() > kVRDisplayRAFMaxDuration) {
+        bShouldStartFrame = true;
+      }
+    }
+  }
+
+  if (bShouldStartFrame) {
+    VRManager *vm = VRManager::Get();
+    MOZ_ASSERT(vm);
+    vm->NotifyVRVsync(mDisplayInfo.mDisplayID);
+  }
+}
+
 #if defined(XP_WIN)
 
 void
-VRDisplayHost::SubmitFrame(VRLayerParent* aLayer, const int32_t& aInputFrameID,
-  PTextureParent* aTexture, const gfx::Rect& aLeftEyeRect,
-  const gfx::Rect& aRightEyeRect)
+VRDisplayHost::SubmitFrame(VRLayerParent* aLayer, PTextureParent* aTexture,
+                           const gfx::Rect& aLeftEyeRect,
+                           const gfx::Rect& aRightEyeRect)
 {
-  // aInputFrameID is no longer controlled by content with the WebVR 1.1 API
-  // update; however, we will later use this code to enable asynchronous
-  // submission of multiple layers to be composited.  This will enable
-  // us to build browser UX that remains responsive even when content does
-  // not consistently submit frames.
-
-  int32_t inputFrameID = aInputFrameID;
-  if (inputFrameID == 0) {
-    inputFrameID = mInputFrameID;
+  if ((mDisplayInfo.mGroupMask & aLayer->GetGroup()) == 0) {
+    // Suppress layers hidden by the group mask
+    return;
   }
-  if (inputFrameID < 0) {
-    // Sanity check to prevent invalid memory access on builds with assertions
-    // disabled.
-    inputFrameID = 0;
-  }
-
-  VRHMDSensorState sensorState = mLastSensorState[inputFrameID % kMaxLatencyFrames];
-  // It is possible to get a cache miss on mLastSensorState if latency is
-  // longer than kMaxLatencyFrames.  An optimization would be to find a frame
-  // that is closer than the one selected with the modulus.
-  // If we hit this; however, latency is already so high that the site is
-  // un-viewable and a more accurate pose prediction is not likely to
-  // compensate.
 
   TextureHost* th = TextureHost::AsTextureHost(aTexture);
   // WebVR doesn't use the compositor to compose the frame, so use
   // AutoLockTextureHostWithoutCompositor here.
   AutoLockTextureHostWithoutCompositor autoLock(th);
   if (autoLock.Failed()) {
     NS_WARNING("Failed to lock the VR layer texture");
     return;
@@ -111,25 +175,41 @@ VRDisplayHost::SubmitFrame(VRLayerParent
   IntSize texSize = source->GetSize();
 
   TextureSourceD3D11* sourceD3D11 = source->AsSourceD3D11();
   if (!sourceD3D11) {
     NS_WARNING("WebVR support currently only implemented for D3D11");
     return;
   }
 
-  SubmitFrame(sourceD3D11, texSize, sensorState, aLeftEyeRect, aRightEyeRect);
+  if (!SubmitFrame(sourceD3D11, texSize, aLeftEyeRect, aRightEyeRect)) {
+    return;
+  }
+
+  /**
+   * Trigger the next VSync immediately after we are successfully
+   * submitting frames.  As SubmitFrame is responsible for throttling
+   * the render loop, if we don't successfully call it, we shouldn't trigger
+   * NotifyVRVsync immediately, as it will run unbounded.
+   * If NotifyVRVsync is not called here due to SubmitFrame failing, the
+   * fallback "watchdog" code in VRDisplayHost::NotifyVSync() will cause
+   * frames to continue at a lower refresh rate until frame submission
+   * succeeds again.
+   */
+  VRManager *vm = VRManager::Get();
+  MOZ_ASSERT(vm);
+  vm->NotifyVRVsync(mDisplayInfo.mDisplayID);
 }
 
 #else
 
 void
-VRDisplayHost::SubmitFrame(VRLayerParent* aLayer, const int32_t& aInputFrameID,
-  PTextureParent* aTexture, const gfx::Rect& aLeftEyeRect,
-  const gfx::Rect& aRightEyeRect)
+VRDisplayHost::SubmitFrame(VRLayerParent* aLayer, PTextureParent* aTexture,
+                           const gfx::Rect& aLeftEyeRect,
+                           const gfx::Rect& aRightEyeRect)
 {
   NS_WARNING("WebVR only supported in Windows.");
 }
 
 #endif
 
 bool
 VRDisplayHost::CheckClearDisplayInfoDirty()
--- a/gfx/vr/VRDisplayHost.h
+++ b/gfx/vr/VRDisplayHost.h
@@ -32,60 +32,57 @@ class VRDisplayHost {
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VRDisplayHost)
 
   const VRDisplayInfo& GetDisplayInfo() const { return mDisplayInfo; }
 
   void AddLayer(VRLayerParent* aLayer);
   void RemoveLayer(VRLayerParent* aLayer);
 
-  virtual VRHMDSensorState GetSensorState() = 0;
   virtual void ZeroSensor() = 0;
   virtual void StartPresentation() = 0;
   virtual void StopPresentation() = 0;
-  virtual void NotifyVSync() { };
+  virtual void NotifyVSync();
 
+  void StartFrame();
   void SubmitFrame(VRLayerParent* aLayer,
-                   const int32_t& aInputFrameID,
                    mozilla::layers::PTextureParent* aTexture,
                    const gfx::Rect& aLeftEyeRect,
                    const gfx::Rect& aRightEyeRect);
 
   bool CheckClearDisplayInfoDirty();
+  void SetGroupMask(uint32_t aGroupMask);
 
 protected:
   explicit VRDisplayHost(VRDeviceType aType);
   virtual ~VRDisplayHost();
 
 #if defined(XP_WIN)
-  virtual void SubmitFrame(mozilla::layers::TextureSourceD3D11* aSource,
+  // Subclasses should override this SubmitFrame function.
+  // Returns true if the SubmitFrame call will block as necessary
+  // to control timing of the next frame and throttle the render loop
+  // for the needed framerate.
+  virtual bool SubmitFrame(mozilla::layers::TextureSourceD3D11* aSource,
                            const IntSize& aSize,
-                           const VRHMDSensorState& aSensorState,
                            const gfx::Rect& aLeftEyeRect,
                            const gfx::Rect& aRightEyeRect) = 0;
 #endif
 
   VRDisplayInfo mDisplayInfo;
 
   nsTArray<RefPtr<VRLayerParent>> mLayers;
-  // Weak reference to mLayers entries are cleared in VRLayerParent destructor
+  // Weak reference to mLayers entries are cleared in
+  // VRLayerParent destructor
 
-  // The maximum number of frames of latency that we would expect before we
-  // should give up applying pose prediction.
-  // If latency is greater than one second, then the experience is not likely
-  // to be corrected by pose prediction.  Setting this value too
-  // high may result in unnecessary memory allocation.
-  // As the current fastest refresh rate is 90hz, 100 is selected as a
-  // conservative value.
-  static const int kMaxLatencyFrames = 100;
-  VRHMDSensorState mLastSensorState[kMaxLatencyFrames];
-  int32_t mInputFrameID;
+protected:
+  virtual VRHMDSensorState GetSensorState() = 0;
 
 private:
   VRDisplayInfo mLastUpdateDisplayInfo;
+  TimeStamp mLastFrameStart;
 };
 
 class VRControllerHost {
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VRControllerHost)
 
   const VRControllerInfo& GetControllerInfo() const;
   void SetButtonPressed(uint64_t aBit);
--- a/gfx/vr/VRDisplayPresentation.cpp
+++ b/gfx/vr/VRDisplayPresentation.cpp
@@ -9,23 +9,31 @@
 #include "mozilla/Unused.h"
 #include "VRDisplayClient.h"
 #include "VRLayerChild.h"
 
 using namespace mozilla;
 using namespace mozilla::gfx;
 
 VRDisplayPresentation::VRDisplayPresentation(VRDisplayClient *aDisplayClient,
-                                             const nsTArray<mozilla::dom::VRLayer>& aLayers)
+                                             const nsTArray<mozilla::dom::VRLayer>& aLayers,
+                                             uint32_t aGroup)
   : mDisplayClient(aDisplayClient)
   , mDOMLayers(aLayers)
+  , mGroup(aGroup)
 {
   CreateLayers();
 }
 
+uint32_t
+VRDisplayPresentation::GetGroup() const
+{
+  return mGroup;
+}
+
 void
 VRDisplayPresentation::CreateLayers()
 {
   if (mLayers.Length()) {
     return;
   }
 
   for (dom::VRLayer& layer : mDOMLayers) {
@@ -75,17 +83,18 @@ VRDisplayPresentation::CreateLayers()
     nsIDocument* doc;
     doc = canvasElement->OwnerDoc();
     if (doc) {
       target = doc->EventTargetFor(TaskCategory::Other);
     }
 
     RefPtr<VRLayerChild> vrLayer =
       static_cast<VRLayerChild*>(manager->CreateVRLayer(mDisplayClient->GetDisplayInfo().GetDisplayID(),
-                                                        leftBounds, rightBounds, target));
+                                                        leftBounds, rightBounds, target,
+                                                        mGroup));
     if (!vrLayer) {
       NS_WARNING("CreateVRLayer returned null!");
       continue;
     }
 
     vrLayer->Initialize(canvasElement);
 
     mLayers.AppendElement(vrLayer);
--- a/gfx/vr/VRDisplayPresentation.h
+++ b/gfx/vr/VRDisplayPresentation.h
@@ -14,26 +14,30 @@ namespace gfx {
 class VRDisplayClient;
 class VRLayerChild;
 
 class VRDisplayPresentation final
 {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VRDisplayPresentation)
 
 public:
-  VRDisplayPresentation(VRDisplayClient *aDisplayClient, const nsTArray<dom::VRLayer>& aLayers);
+  VRDisplayPresentation(VRDisplayClient *aDisplayClient,
+                        const nsTArray<dom::VRLayer>& aLayers,
+                        uint32_t aGroup);
   void SubmitFrame();
   void GetDOMLayers(nsTArray<dom::VRLayer>& result);
+  uint32_t GetGroup() const;
 
 private:
   ~VRDisplayPresentation();
   void CreateLayers();
   void DestroyLayers();
 
   RefPtr<VRDisplayClient> mDisplayClient;
   nsTArray<dom::VRLayer> mDOMLayers;
   nsTArray<RefPtr<VRLayerChild>> mLayers;
+  uint32_t mGroup;
 };
 
 } // namespace gfx
 } // namespace mozilla
 
 #endif /* GFX_VR_DISPLAY_PRESENTAITON_H */
--- a/gfx/vr/VRManager.cpp
+++ b/gfx/vr/VRManager.cpp
@@ -163,25 +163,27 @@ VRManager::NotifyVsync(const TimeStamp& 
   const double kVRDisplayRefreshMaxDuration = 5000; // milliseconds
   const double kVRDisplayInactiveMaxDuration = 30000; // milliseconds
 
   bool bHaveEventListener = false;
   bool bHaveControllerListener = false;
 
   for (auto iter = mVRManagerParents.Iter(); !iter.Done(); iter.Next()) {
     VRManagerParent *vmp = iter.Get()->GetKey();
-    if (mVRDisplays.Count()) {
-      Unused << vmp->SendNotifyVSync();
-    }
     bHaveEventListener |= vmp->HaveEventListener();
     bHaveControllerListener |= vmp->HaveControllerListener();
   }
 
+  // VRDisplayHost::NotifyVSync may modify mVRDisplays, so we iterate
+  // through a local copy here.
+  nsTArray<RefPtr<VRDisplayHost>> displays;
   for (auto iter = mVRDisplays.Iter(); !iter.Done(); iter.Next()) {
-    gfx::VRDisplayHost* display = iter.UserData();
+    displays.AppendElement(iter.UserData());
+  }
+  for (const auto& display: displays) {
     display->NotifyVSync();
   }
 
   if (bHaveEventListener) {
     // If content has set an EventHandler to be notified of VR display events
     // we must continually refresh the VR display enumeration to check
     // for events that we must fire such as Window.onvrdisplayconnect
     // Note that enumeration itself may activate display hardware, such
@@ -234,19 +236,22 @@ void
 VRManager::NotifyVRVsync(const uint32_t& aDisplayID)
 {
   for (const auto& manager: mManagers) {
     if (manager->GetIsPresenting()) {
       manager->HandleInput();
     }
   }
 
-  for (auto iter = mVRManagerParents.Iter(); !iter.Done(); iter.Next()) {
-    Unused << iter.Get()->GetKey()->SendNotifyVRVSync(aDisplayID);
+  RefPtr<VRDisplayHost> display = GetDisplay(aDisplayID);
+  if (display) {
+    display->StartFrame();
   }
+
+  RefreshVRDisplays();
 }
 
 void
 VRManager::RefreshVRDisplays(bool aMustDispatch)
 {
   nsTArray<RefPtr<gfx::VRDisplayHost> > displays;
 
   /** We don't wish to enumerate the same display from multiple managers,
@@ -334,17 +339,17 @@ void
 VRManager::SubmitFrame(VRLayerParent* aLayer, layers::PTextureParent* aTexture,
                        const gfx::Rect& aLeftEyeRect,
                        const gfx::Rect& aRightEyeRect)
 {
   TextureHost* th = TextureHost::AsTextureHost(aTexture);
   mLastFrame = th;
   RefPtr<VRDisplayHost> display = GetDisplay(aLayer->GetDisplayID());
   if (display) {
-    display->SubmitFrame(aLayer, 0, aTexture, aLeftEyeRect, aRightEyeRect);
+    display->SubmitFrame(aLayer, aTexture, aLeftEyeRect, aRightEyeRect);
   }
 }
 
 RefPtr<gfx::VRControllerHost>
 VRManager::GetController(const uint32_t& aControllerID)
 {
   RefPtr<gfx::VRControllerHost> controller;
   if (mVRControllers.Get(aControllerID, getter_AddRefs(controller))) {
--- a/gfx/vr/gfxVR.h
+++ b/gfx/vr/gfxVR.h
@@ -127,90 +127,138 @@ struct VRFieldOfView {
   Matrix4x4 ConstructProjectionMatrix(float zNear, float zFar, bool rightHanded) const;
 
   double upDegrees;
   double rightDegrees;
   double downDegrees;
   double leftDegrees;
 };
 
+struct VRHMDSensorState {
+  VRHMDSensorState()
+  {
+    Clear();
+  }
+  int32_t inputFrameID;
+  double timestamp;
+  VRDisplayCapabilityFlags flags;
+
+  // These members will only change with inputFrameID:
+  float orientation[4];
+  float position[3];
+  float angularVelocity[3];
+  float angularAcceleration[3];
+  float linearVelocity[3];
+  float linearAcceleration[3];
+
+  void Clear() {
+    memset(this, 0, sizeof(VRHMDSensorState));
+  }
+
+  bool operator==(const VRHMDSensorState& other) const {
+    return inputFrameID == other.inputFrameID &&
+           timestamp == other.timestamp;
+  }
+
+  bool operator!=(const VRHMDSensorState& other) const {
+    return !(*this == other);
+  }
+};
+
+// The maximum number of frames of latency that we would expect before we
+// should give up applying pose prediction.
+// If latency is greater than one second, then the experience is not likely
+// to be corrected by pose prediction.  Setting this value too
+// high may result in unnecessary memory allocation.
+// As the current fastest refresh rate is 90hz, 100 is selected as a
+// conservative value.
+static const int kVRMaxLatencyFrames = 100;
+
+// We assign VR presentations to groups with a bitmask.
+// Currently, we will only display either content or chrome.
+// Later, we will have more groups to support VR home spaces and
+// multitasking environments.
+// These values are not exposed to regular content and only affect
+// chrome-only API's.  They may be changed at any time.
+static const uint32_t kVRGroupNone = 0;
+static const uint32_t kVRGroupContent = 1 << 0;
+static const uint32_t kVRGroupChrome = 1 << 1;
+static const uint32_t kVRGroupAll = 0xffffffff;
+
 struct VRDisplayInfo
 {
   VRDeviceType GetType() const { return mType; }
   uint32_t GetDisplayID() const { return mDisplayID; }
   const nsCString& GetDisplayName() const { return mDisplayName; }
   VRDisplayCapabilityFlags GetCapabilities() const { return mCapabilityFlags; }
 
   const IntSize& SuggestedEyeResolution() const { return mEyeResolution; }
   const Point3D& GetEyeTranslation(uint32_t whichEye) const { return mEyeTranslation[whichEye]; }
   const VRFieldOfView& GetEyeFOV(uint32_t whichEye) const { return mEyeFOV[whichEye]; }
   bool GetIsConnected() const { return mIsConnected; }
   bool GetIsMounted() const { return mIsMounted; }
-  bool GetIsPresenting() const { return mIsPresenting; }
+  uint32_t GetPresentingGroups() const { return mPresentingGroups; }
+  uint32_t GetGroupMask() const { return mGroupMask; }
   const Size& GetStageSize() const { return mStageSize; }
   const Matrix4x4& GetSittingToStandingTransform() const { return mSittingToStandingTransform; }
+  uint32_t GetFrameId() const { return mFrameId; }
 
   enum Eye {
     Eye_Left,
     Eye_Right,
     NumEyes
   };
 
   uint32_t mDisplayID;
   VRDeviceType mType;
   nsCString mDisplayName;
   VRDisplayCapabilityFlags mCapabilityFlags;
   VRFieldOfView mEyeFOV[VRDisplayInfo::NumEyes];
   Point3D mEyeTranslation[VRDisplayInfo::NumEyes];
   IntSize mEyeResolution;
   bool mIsConnected;
   bool mIsMounted;
-  bool mIsPresenting;
+  uint32_t mPresentingGroups;
+  uint32_t mGroupMask;
   Size mStageSize;
   Matrix4x4 mSittingToStandingTransform;
+  uint32_t mFrameId;
+  VRHMDSensorState mLastSensorState[kVRMaxLatencyFrames];
 
   bool operator==(const VRDisplayInfo& other) const {
+    for (size_t i = 0; i < kVRMaxLatencyFrames; i++) {
+      if (mLastSensorState[i] != other.mLastSensorState[i]) {
+        return false;
+      }
+    }
     return mType == other.mType &&
            mDisplayID == other.mDisplayID &&
            mDisplayName == other.mDisplayName &&
            mCapabilityFlags == other.mCapabilityFlags &&
            mEyeResolution == other.mEyeResolution &&
            mIsConnected == other.mIsConnected &&
            mIsMounted == other.mIsMounted &&
-           mIsPresenting == other.mIsPresenting &&
+           mPresentingGroups == other.mPresentingGroups &&
+           mGroupMask == other.mGroupMask &&
            mEyeFOV[0] == other.mEyeFOV[0] &&
            mEyeFOV[1] == other.mEyeFOV[1] &&
            mEyeTranslation[0] == other.mEyeTranslation[0] &&
            mEyeTranslation[1] == other.mEyeTranslation[1] &&
            mStageSize == other.mStageSize &&
-           mSittingToStandingTransform == other.mSittingToStandingTransform;
+           mSittingToStandingTransform == other.mSittingToStandingTransform &&
+           mFrameId == other.mFrameId;
   }
 
   bool operator!=(const VRDisplayInfo& other) const {
     return !(*this == other);
   }
-};
 
-struct VRHMDSensorState {
-  VRHMDSensorState()
+  const VRHMDSensorState& GetSensorState() const
   {
-    Clear();
-  }
-  double timestamp;
-  int32_t inputFrameID;
-  VRDisplayCapabilityFlags flags;
-  float orientation[4];
-  float position[3];
-  float angularVelocity[3];
-  float angularAcceleration[3];
-  float linearVelocity[3];
-  float linearAcceleration[3];
-
-  void Clear() {
-    memset(this, 0, sizeof(VRHMDSensorState));
+    return mLastSensorState[mFrameId % kVRMaxLatencyFrames];
   }
 };
 
 struct VRSubmitFrameResultInfo
 {
   VRSubmitFrameResultInfo()
    : mFrameNum(0),
      mWidth(0),
--- a/gfx/vr/gfxVROSVR.cpp
+++ b/gfx/vr/gfxVROSVR.cpp
@@ -294,16 +294,17 @@ VRDisplayOSVR::GetSensorState()
   OSVR_TimeValue timestamp;
 
   OSVR_OrientationState orientation;
 
   OSVR_ReturnCode ret =
     osvr_GetOrientationState(*m_iface, &timestamp, &orientation);
 
   result.timestamp = timestamp.seconds;
+  result.inputFrameID = mDisplayInfo.mFrameId;
 
   if (ret == OSVR_RETURN_SUCCESS) {
     result.flags |= VRDisplayCapabilityFlags::Cap_Orientation;
     result.orientation[0] = orientation.data[1];
     result.orientation[1] = orientation.data[2];
     result.orientation[2] = orientation.data[3];
     result.orientation[3] = orientation.data[0];
   }
@@ -317,24 +318,24 @@ VRDisplayOSVR::GetSensorState()
     result.position[2] = position.data[2];
   }
 
   return result;
 }
 
 #if defined(XP_WIN)
 
-void
+bool
 VRDisplayOSVR::SubmitFrame(TextureSourceD3D11* aSource,
   const IntSize& aSize,
-  const VRHMDSensorState& aSensorState,
   const gfx::Rect& aLeftEyeRect,
   const gfx::Rect& aRightEyeRect)
 {
   // XXX Add code to submit frame
+  return false;
 }
 
 #endif
 
 void
 VRDisplayOSVR::StartPresentation()
 {
   // XXX Add code to start VR Presentation
@@ -531,17 +532,17 @@ VRSystemManagerOSVR::GetHMDs(nsTArray<Re
   }
 }
 
 bool
 VRSystemManagerOSVR::GetIsPresenting()
 {
   if (mHMDInfo) {
     VRDisplayInfo displayInfo(mHMDInfo->GetDisplayInfo());
-    return displayInfo.GetIsPresenting();
+    return displayInfo.GetPresentingGroups() != kVRGroupNone;
   }
 
   return false;
 }
 
 void
 VRSystemManagerOSVR::HandleInput()
 {
--- a/gfx/vr/gfxVROSVR.h
+++ b/gfx/vr/gfxVROSVR.h
@@ -20,27 +20,26 @@
 
 namespace mozilla {
 namespace gfx {
 namespace impl {
 
 class VRDisplayOSVR : public VRDisplayHost
 {
 public:
-  VRHMDSensorState GetSensorState() override;
   void ZeroSensor() override;
 
 protected:
+  VRHMDSensorState GetSensorState() override;
   virtual void StartPresentation() override;
   virtual void StopPresentation() override;
 
 #if defined(XP_WIN)
-  virtual void SubmitFrame(TextureSourceD3D11* aSource,
+  virtual bool SubmitFrame(TextureSourceD3D11* aSource,
     const IntSize& aSize,
-    const VRHMDSensorState& aSensorState,
     const gfx::Rect& aLeftEyeRect,
     const gfx::Rect& aRightEyeRect) override;
 #endif
 
 public:
   explicit VRDisplayOSVR(OSVR_ClientContext* context,
                          OSVR_ClientInterface* iface,
                          OSVR_DisplayConfig* display);
--- a/gfx/vr/gfxVROculus.cpp
+++ b/gfx/vr/gfxVROculus.cpp
@@ -433,30 +433,28 @@ VRDisplayOculus::ZeroSensor()
 {
   ovr_RecenterTrackingOrigin(mSession);
   UpdateStageParameters();
 }
 
 VRHMDSensorState
 VRDisplayOculus::GetSensorState()
 {
-  mInputFrameID++;
-
   VRHMDSensorState result;
   double frameDelta = 0.0f;
   if (gfxPrefs::VRPosePredictionEnabled()) {
     // XXX We might need to call ovr_GetPredictedDisplayTime even if we don't use the result.
     // If we don't call it, the Oculus driver will spew out many warnings...
     double predictedFrameTime = ovr_GetPredictedDisplayTime(mSession, 0);
     frameDelta = predictedFrameTime - ovr_GetTimeInSeconds();
   }
   result = GetSensorState(frameDelta);
-  result.inputFrameID = mInputFrameID;
-  mLastSensorState[result.inputFrameID % kMaxLatencyFrames] = result;
+  result.inputFrameID = mDisplayInfo.mFrameId;
   result.position[1] -= mEyeHeight;
+  mDisplayInfo.mLastSensorState[result.inputFrameID % kVRMaxLatencyFrames] = result;
   return result;
 }
 
 VRHMDSensorState
 VRDisplayOculus::GetSensorState(double timeOffset)
 {
   VRHMDSensorState result;
 
@@ -690,36 +688,35 @@ VRDisplayOculus::UpdateConstantBuffers()
 
   ID3D11Buffer *buffer = mVSConstantBuffer;
   mContext->VSSetConstantBuffers(0, 1, &buffer);
   buffer = mPSConstantBuffer;
   mContext->PSSetConstantBuffers(0, 1, &buffer);
   return true;
 }
 
-void
+bool
 VRDisplayOculus::SubmitFrame(TextureSourceD3D11* aSource,
   const IntSize& aSize,
-  const VRHMDSensorState& aSensorState,
   const gfx::Rect& aLeftEyeRect,
   const gfx::Rect& aRightEyeRect)
 {
   if (!mIsPresenting) {
-    return;
+    return false;
   }
   if (mRenderTargets.IsEmpty()) {
     /**
      * XXX - We should resolve fail the promise returned by
      *       VRDisplay.requestPresent() when the DX11 resources fail allocation
      *       in VRDisplayOculus::StartPresentation().
      *       Bailing out here prevents the crash but content should be aware
      *       that frames are not being presented.
      *       See Bug 1299309.
      **/
-    return;
+    return false;
   }
   MOZ_ASSERT(mDevice);
   MOZ_ASSERT(mContext);
 
   RefPtr<CompositingRenderTargetD3D11> surface = GetNextRenderTarget();
 
   surface->BindRenderTarget(mContext);
 
@@ -770,25 +767,25 @@ VRDisplayOculus::SubmitFrame(TextureSour
   mContext->PSSetShaderResources(0 /* 0 == TexSlot::RGB */, 1, &srView);
   // XXX Use Constant from TexSlot in CompositorD3D11.cpp?
 
   ID3D11SamplerState *sampler = mLinearSamplerState;
   mContext->PSSetSamplers(0, 1, &sampler);
 
   if (!UpdateConstantBuffers()) {
     NS_WARNING("Failed to update constant buffers for Oculus");
-    return;
+    return false;
   }
 
   mContext->Draw(4, 0);
 
   ovrResult orv = ovr_CommitTextureSwapChain(mSession, mTextureSet);
   if (orv != ovrSuccess) {
     NS_WARNING("ovr_CommitTextureSwapChain failed.\n");
-    return;
+    return false;
   }
 
   ovrLayerEyeFov layer;
   memset(&layer, 0, sizeof(layer));
   layer.Header.Type = ovrLayerType_EyeFov;
   layer.Header.Flags = 0;
   layer.ColorTexture[0] = mTextureSet;
   layer.ColorTexture[1] = nullptr;
@@ -803,51 +800,62 @@ VRDisplayOculus::SubmitFrame(TextureSour
   layer.Viewport[1].Size.w = aSize.width * aRightEyeRect.width;
   layer.Viewport[1].Size.h = aSize.height * aRightEyeRect.height;
 
   const Point3D& l = mDisplayInfo.mEyeTranslation[0];
   const Point3D& r = mDisplayInfo.mEyeTranslation[1];
   const ovrVector3f hmdToEyeViewOffset[2] = { { l.x, l.y, l.z },
                                               { r.x, r.y, r.z } };
 
+  const VRHMDSensorState& sensorState = mDisplayInfo.GetSensorState();
+
   for (uint32_t i = 0; i < 2; ++i) {
-    Quaternion o(aSensorState.orientation[0],
-      aSensorState.orientation[1],
-      aSensorState.orientation[2],
-      aSensorState.orientation[3]);
+    Quaternion o(sensorState.orientation[0],
+      sensorState.orientation[1],
+      sensorState.orientation[2],
+      sensorState.orientation[3]);
     Point3D vo(hmdToEyeViewOffset[i].x, hmdToEyeViewOffset[i].y, hmdToEyeViewOffset[i].z);
     Point3D p = o.RotatePoint(vo);
     layer.RenderPose[i].Orientation.x = o.x;
     layer.RenderPose[i].Orientation.y = o.y;
     layer.RenderPose[i].Orientation.z = o.z;
     layer.RenderPose[i].Orientation.w = o.w;
-    layer.RenderPose[i].Position.x = p.x + aSensorState.position[0];
-    layer.RenderPose[i].Position.y = p.y + aSensorState.position[1];
-    layer.RenderPose[i].Position.z = p.z + aSensorState.position[2];
+    layer.RenderPose[i].Position.x = p.x + sensorState.position[0];
+    layer.RenderPose[i].Position.y = p.y + sensorState.position[1];
+    layer.RenderPose[i].Position.z = p.z + sensorState.position[2];
   }
 
   ovrLayerHeader *layers = &layer.Header;
-  orv = ovr_SubmitFrame(mSession, aSensorState.inputFrameID, nullptr, &layers, 1);
+  orv = ovr_SubmitFrame(mSession, mDisplayInfo.mFrameId, nullptr, &layers, 1);
+  // ovr_SubmitFrame will fail during the Oculus health and safety warning.
+  // and will start succeeding once the warning has been dismissed by the user.
 
-  if (orv != ovrSuccess) {
-    printf_stderr("ovr_SubmitFrame failed.\n");
+  if (!OVR_UNQUALIFIED_SUCCESS(orv)) {
+    /**
+     * We wish to throttle the framerate for any case that the rendered
+     * result is not visible.  In some cases, such as during the Oculus
+     * "health and safety warning", orv will be > 0 (OVR_SUCCESS but not
+     * OVR_UNQUALIFIED_SUCCESS) and ovr_SubmitFrame will not block.
+     * In this case, returning true would have resulted in an unthrottled
+     * render loop hiting excessive frame rates and consuming resources.
+     */
+    return false;
   }
 
-  // Trigger the next VSync immediately
-  VRManager *vm = VRManager::Get();
-  MOZ_ASSERT(vm);
-  vm->NotifyVRVsync(mDisplayInfo.mDisplayID);
+  return true;
 }
 
 void
 VRDisplayOculus::NotifyVSync()
 {
   ovrSessionStatus sessionStatus;
   ovrResult ovr = ovr_GetSessionStatus(mSession, &sessionStatus);
   mDisplayInfo.mIsConnected = (ovr == ovrSuccess && sessionStatus.HmdPresent);
+
+  VRDisplayHost::NotifyVSync();
 }
 
 VRControllerOculus::VRControllerOculus(dom::GamepadHand aHand)
   : VRControllerHost(VRDeviceType::Oculus)
   , mIndexTrigger(0.0f)
   , mHandTrigger(0.0f)
   , mVibrateThread(nullptr)
   , mIsVibrateStopped(false)
@@ -1162,17 +1170,17 @@ VRSystemManagerOculus::GetHMDs(nsTArray<
   }
 }
 
 bool
 VRSystemManagerOculus::GetIsPresenting()
 {
   if (mHMDInfo) {
     VRDisplayInfo displayInfo(mHMDInfo->GetDisplayInfo());
-    return displayInfo.GetIsPresenting();
+    return displayInfo.GetPresentingGroups() != 0;
   }
 
   return false;
 }
 
 void
 VRSystemManagerOculus::HandleInput()
 {
--- a/gfx/vr/gfxVROculus.h
+++ b/gfx/vr/gfxVROculus.h
@@ -32,25 +32,24 @@ enum class OculusControllerAxisType : ui
   ThumbstickYAxis,
   NumVRControllerAxisType
 };
 
 class VRDisplayOculus : public VRDisplayHost
 {
 public:
   virtual void NotifyVSync() override;
-  virtual VRHMDSensorState GetSensorState() override;
   void ZeroSensor() override;
 
 protected:
+  virtual VRHMDSensorState GetSensorState() override;
   virtual void StartPresentation() override;
   virtual void StopPresentation() override;
-  virtual void SubmitFrame(mozilla::layers::TextureSourceD3D11* aSource,
+  virtual bool SubmitFrame(mozilla::layers::TextureSourceD3D11* aSource,
                            const IntSize& aSize,
-                           const VRHMDSensorState& aSensorState,
                            const gfx::Rect& aLeftEyeRect,
                            const gfx::Rect& aRightEyeRect) override;
   void UpdateStageParameters();
 
 public:
   explicit VRDisplayOculus(ovrSession aSession);
 
 protected:
--- a/gfx/vr/gfxVROpenVR.cpp
+++ b/gfx/vr/gfxVROpenVR.cpp
@@ -168,22 +168,16 @@ VRDisplayOpenVR::UpdateStageParameters()
 
 void
 VRDisplayOpenVR::ZeroSensor()
 {
   mVRSystem->ResetSeatedZeroPose();
   UpdateStageParameters();
 }
 
-VRHMDSensorState
-VRDisplayOpenVR::GetSensorState()
-{
-  return GetSensorState(0.0f);
-}
-
 void
 VRDisplayOpenVR::PollEvents()
 {
   ::vr::VREvent_t event;
   while (mVRSystem->PollNextEvent(&event, sizeof(event))) {
     if (event.trackedDeviceIndex == ::vr::k_unTrackedDeviceIndex_Hmd) {
       switch (event.eventType) {
       case ::vr::VREvent_TrackedDeviceUserInteractionStarted:
@@ -196,17 +190,17 @@ VRDisplayOpenVR::PollEvents()
         // ignore
         break;
       }
     }
   }
 }
 
 VRHMDSensorState
-VRDisplayOpenVR::GetSensorState(double timeOffset)
+VRDisplayOpenVR::GetSensorState()
 {
   PollEvents();
 
   ::vr::TrackedDevicePose_t poses[::vr::k_unMaxTrackedDeviceCount];
   // Note: We *must* call WaitGetPoses in order for any rendering to happen at all
   mVRCompositor->WaitGetPoses(poses, ::vr::k_unMaxTrackedDeviceCount, nullptr, 0);
 
   VRHMDSensorState result;
@@ -251,16 +245,17 @@ VRDisplayOpenVR::GetSensorState(double t
     result.position[0] = m._41;
     result.position[1] = m._42;
     result.position[2] = m._43;
     result.linearVelocity[0] = pose.vVelocity.v[0];
     result.linearVelocity[1] = pose.vVelocity.v[1];
     result.linearVelocity[2] = pose.vVelocity.v[2];
   }
 
+  result.inputFrameID = mDisplayInfo.mFrameId;
   return result;
 }
 
 void
 VRDisplayOpenVR::StartPresentation()
 {
   if (mIsPresenting) {
     return;
@@ -278,25 +273,24 @@ VRDisplayOpenVR::StopPresentation()
   mVRCompositor->ClearLastSubmittedFrame();
 
   mIsPresenting = false;
 }
 
 
 #if defined(XP_WIN)
 
-void
+bool
 VRDisplayOpenVR::SubmitFrame(TextureSourceD3D11* aSource,
   const IntSize& aSize,
-  const VRHMDSensorState& aSensorState,
   const gfx::Rect& aLeftEyeRect,
   const gfx::Rect& aRightEyeRect)
 {
   if (!mIsPresenting) {
-    return;
+    return false;
   }
 
   ::vr::Texture_t tex;
   tex.handle = (void *)aSource->GetD3D11Texture();
   tex.eType = ::vr::ETextureType::TextureType_DirectX;
   tex.eColorSpace = ::vr::EColorSpace::ColorSpace_Auto;
 
   ::vr::VRTextureBounds_t bounds;
@@ -318,32 +312,31 @@ VRDisplayOpenVR::SubmitFrame(TextureSour
 
   err = mVRCompositor->Submit(::vr::EVREye::Eye_Right, &tex, &bounds);
   if (err != ::vr::EVRCompositorError::VRCompositorError_None) {
     printf_stderr("OpenVR Compositor Submit() failed.\n");
   }
 
   mVRCompositor->PostPresentHandoff();
 
-  // Trigger the next VSync immediately
-  VRManager *vm = VRManager::Get();
-  MOZ_ASSERT(vm);
-  vm->NotifyVRVsync(mDisplayInfo.mDisplayID);
+  return true;
 }
 
 #endif
 
 void
 VRDisplayOpenVR::NotifyVSync()
 {
   // We update mIsConneced once per frame.
   mDisplayInfo.mIsConnected = ::vr::VR_IsHmdPresent();
 
   // Make sure we respond to OpenVR events even when not presenting
   PollEvents();
+
+  VRDisplayHost::NotifyVSync();
 }
 
 VRControllerOpenVR::VRControllerOpenVR(dom::GamepadHand aHand, uint32_t aNumButtons,
                                        uint32_t aNumAxes, ::vr::ETrackedDeviceClass aDeviceType)
   : VRControllerHost(VRDeviceType::OpenVR)
   , mTrigger(0)
   , mAxisMove(aNumAxes)
   , mVibrateThread(nullptr)
@@ -590,17 +583,17 @@ VRSystemManagerOpenVR::GetHMDs(nsTArray<
   }
 }
 
 bool
 VRSystemManagerOpenVR::GetIsPresenting()
 {
   if (mOpenVRHMD) {
     VRDisplayInfo displayInfo(mOpenVRHMD->GetDisplayInfo());
-    return displayInfo.GetIsPresenting();
+    return displayInfo.GetPresentingGroups() != kVRGroupNone;
   }
 
   return false;
 }
 
 void
 VRSystemManagerOpenVR::HandleInput()
 {
--- a/gfx/vr/gfxVROpenVR.h
+++ b/gfx/vr/gfxVROpenVR.h
@@ -21,41 +21,38 @@
 namespace mozilla {
 namespace gfx {
 namespace impl {
 
 class VRDisplayOpenVR : public VRDisplayHost
 {
 public:
   virtual void NotifyVSync() override;
-  virtual VRHMDSensorState GetSensorState() override;
   void ZeroSensor() override;
 
 protected:
+  virtual VRHMDSensorState GetSensorState() override;
   virtual void StartPresentation() override;
   virtual void StopPresentation() override;
 #if defined(XP_WIN)
-  virtual void SubmitFrame(mozilla::layers::TextureSourceD3D11* aSource,
+  virtual bool SubmitFrame(mozilla::layers::TextureSourceD3D11* aSource,
                            const IntSize& aSize,
-                           const VRHMDSensorState& aSensorState,
                            const gfx::Rect& aLeftEyeRect,
                            const gfx::Rect& aRightEyeRect) override;
 #endif
 
 public:
   explicit VRDisplayOpenVR(::vr::IVRSystem *aVRSystem,
                            ::vr::IVRChaperone *aVRChaperone,
                            ::vr::IVRCompositor *aVRCompositor);
 
 protected:
   virtual ~VRDisplayOpenVR();
   void Destroy();
 
-  VRHMDSensorState GetSensorState(double timeOffset);
-
   // not owned by us; global from OpenVR
   ::vr::IVRSystem *mVRSystem;
   ::vr::IVRChaperone *mVRChaperone;
   ::vr::IVRCompositor *mVRCompositor;
 
   bool mIsPresenting;
 
   void UpdateStageParameters();
--- a/gfx/vr/gfxVRPuppet.cpp
+++ b/gfx/vr/gfxVRPuppet.cpp
@@ -51,17 +51,16 @@ static const uint32_t kPuppetAxes[] = {
 static const uint32_t kNumPuppetAxis = sizeof(kPuppetAxes) /
                                        sizeof(uint32_t);
 
 static const uint32_t kNumPuppetHaptcs = 1;
 
 VRDisplayPuppet::VRDisplayPuppet()
  : VRDisplayHost(VRDeviceType::Puppet)
  , mIsPresenting(false)
- , mFrameNum(0)
 {
   MOZ_COUNT_CTOR_INHERITED(VRDisplayPuppet, VRDisplayHost);
 
   mDisplayInfo.mDisplayName.AssignLiteral("Puppet HMD");
   mDisplayInfo.mIsConnected = true;
   mDisplayInfo.mIsMounted = false;
   mDisplayInfo.mCapabilityFlags = VRDisplayCapabilityFlags::Cap_None |
                                   VRDisplayCapabilityFlags::Cap_Orientation |
@@ -149,22 +148,17 @@ VRDisplayPuppet::Destroy()
 void
 VRDisplayPuppet::ZeroSensor()
 {
 }
 
 VRHMDSensorState
 VRDisplayPuppet::GetSensorState()
 {
-  return GetSensorState(0.0f);
-}
-
-VRHMDSensorState
-VRDisplayPuppet::GetSensorState(double timeOffset)
-{
+  mSensorState.inputFrameID = mDisplayInfo.mFrameId;
   return mSensorState;
 }
 
 void
 VRDisplayPuppet::SetSensorState(const VRHMDSensorState& aSensorState)
 {
   memcpy(&mSensorState, &aSensorState, sizeof(mSensorState));
 }
@@ -290,25 +284,24 @@ VRDisplayPuppet::UpdateConstantBuffers()
 
   ID3D11Buffer *buffer = mVSConstantBuffer;
   mContext->VSSetConstantBuffers(0, 1, &buffer);
   buffer = mPSConstantBuffer;
   mContext->PSSetConstantBuffers(0, 1, &buffer);
   return true;
 }
 
-void
+bool
 VRDisplayPuppet::SubmitFrame(TextureSourceD3D11* aSource,
                              const IntSize& aSize,
-                             const VRHMDSensorState& aSensorState,
                              const gfx::Rect& aLeftEyeRect,
                              const gfx::Rect& aRightEyeRect)
 {
   if (!mIsPresenting) {
-    return;
+    return false;
   }
 
   VRManager *vm = VRManager::Get();
   MOZ_ASSERT(vm);
 
   switch (gfxPrefs::VRPuppetSubmitFrame()) {
     case 0:
       // The VR frame is not displayed.
@@ -344,49 +337,48 @@ VRDisplayPuppet::SubmitFrame(TextureSour
           desc2.BindFlags = 0;
           desc2.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
           desc2.MiscFlags = 0;
 
           ID3D11Texture2D* stagingTexture = nullptr;
           hr = mDevice->CreateTexture2D(&desc2, nullptr, &stagingTexture);
           if (FAILED(hr)) {
             MOZ_ASSERT(false, "Failed to create a staging texture");
-            return;
+            return false;
           }
           // Copy the texture to a staging resource
           mContext->CopyResource(stagingTexture, texture);
           // Map the staging resource
           hr = mContext->Map(stagingTexture,
                              0,  // Subsource
                              D3D11_MAP_READ,
                              0,  // MapFlags
                              &mapInfo);
           if (FAILED(hr)) {
             MOZ_ASSERT(false, "Failed to map staging texture");
           }
           mappedTexture = stagingTexture;
-        }
-        else {
+        } else {
           MOZ_ASSERT(false, "Failed to map staging texture");
-          return;
+          return false;
         }
       } else {
         mappedTexture = texture;
       }
       // Ideally, we should convert the srcData to a PNG image and decode it
       // to a Base64 string here, but the GPU process does not have the privilege to
       // access the image library. So, we have to convert the RAW image data
       // to a base64 string and forward it to let the content process to
       // do the image conversion.
       char* srcData = static_cast<char*>(mapInfo.pData);
       VRSubmitFrameResultInfo result;
       result.mFormat = SurfaceFormat::B8G8R8A8;
       result.mWidth = desc.Width;
       result.mHeight = desc.Height;
-      result.mFrameNum = mFrameNum;
+      result.mFrameNum = mDisplayInfo.mFrameId;
       nsCString rawString(Substring((char*)srcData, mapInfo.RowPitch * desc.Height));
 
       if (Base64Encode(rawString, result.mBase64Image) != NS_OK) {
         MOZ_ASSERT(false, "Failed to encode base64 images.");
       }
       mContext->Unmap(mappedTexture, 0);
       // Dispatch the base64 encoded string to the DOM side, and it will be decoded
       // and convert to a PNG image there.
@@ -445,55 +437,56 @@ VRDisplayPuppet::SubmitFrame(TextureSour
       mContext->PSSetShaderResources(0 /* 0 == TexSlot::RGB */, 1, &srView);
       // XXX Use Constant from TexSlot in CompositorD3D11.cpp?
 
       ID3D11SamplerState *sampler = mLinearSamplerState;
       mContext->PSSetSamplers(0, 1, &sampler);
 
       if (!UpdateConstantBuffers()) {
         NS_WARNING("Failed to update constant buffers for Puppet");
-        return;
+        return false;
       }
       mContext->Draw(4, 0);
       break;
     }
   }
 
-  // Trigger the next VSync immediately
-  vm->NotifyVRVsync(mDisplayInfo.mDisplayID);
-  ++mFrameNum;
+  // We will always return false for gfxVRPuppet to ensure that the fallback "watchdog"
+  // code in VRDisplayHost::NotifyVSync() throttles the render loop.  This "watchdog" will
+  // result in a refresh rate that is quite low compared to real hardware, but should be
+  // sufficient for non-performance oriented tests.  If we wish to simulate realistic frame
+  // rates with VRDisplayPuppet, we should block here for the appropriate amount of time and
+  // return true to indicate that we have blocked.
+  return false;
 }
 #else
-void
+bool
 VRDisplayPuppet::SubmitFrame(TextureSourceOGL* aSource,
                              const IntSize& aSize,
-                             const VRHMDSensorState& aSensorState,
                              const gfx::Rect& aLeftEyeRect,
                              const gfx::Rect& aRightEyeRect)
 {
   if (!mIsPresenting) {
-    return;
+    return false;
   }
 
   // TODO: Bug 1343730, Need to block until the next simulated
   // vblank interval and capture frames for use in reftests.
 
-  // Trigger the next VSync immediately
-  VRManager *vm = VRManager::Get();
-  MOZ_ASSERT(vm);
-  vm->NotifyVRVsync(mDisplayInfo.mDisplayID);
-  ++mFrameNum;
+  return false;
 }
 #endif
 
 void
 VRDisplayPuppet::NotifyVSync()
 {
   // We update mIsConneced once per frame.
   mDisplayInfo.mIsConnected = true;
+
+  VRDisplayHost::NotifyVSync();
 }
 
 VRControllerPuppet::VRControllerPuppet(dom::GamepadHand aHand)
   : VRControllerHost(VRDeviceType::Puppet)
   , mButtonPressState(0)
 {
   MOZ_COUNT_CTOR_INHERITED(VRControllerPuppet, VRControllerHost);
   mControllerInfo.mControllerName.AssignLiteral("Puppet Gamepad");
@@ -630,17 +623,17 @@ VRSystemManagerPuppet::GetHMDs(nsTArray<
   aHMDResult.AppendElement(mPuppetHMD);
 }
 
 bool
 VRSystemManagerPuppet::GetIsPresenting()
 {
   if (mPuppetHMD) {
     VRDisplayInfo displayInfo(mPuppetHMD->GetDisplayInfo());
-    return displayInfo.GetIsPresenting();
+    return displayInfo.GetPresentingGroups() != kVRGroupNone;
   }
 
   return false;
 }
 
 void
 VRSystemManagerPuppet::HandleInput()
 {
--- a/gfx/vr/gfxVRPuppet.h
+++ b/gfx/vr/gfxVRPuppet.h
@@ -15,46 +15,42 @@ namespace mozilla {
 namespace gfx {
 namespace impl {
 
 class VRDisplayPuppet : public VRDisplayHost
 {
 public:
   void SetDisplayInfo(const VRDisplayInfo& aDisplayInfo);
   virtual void NotifyVSync() override;
-  virtual VRHMDSensorState GetSensorState() override;
   void SetSensorState(const VRHMDSensorState& aSensorState);
   void ZeroSensor() override;
 
 protected:
+  virtual VRHMDSensorState GetSensorState() override;
   virtual void StartPresentation() override;
   virtual void StopPresentation() override;
 #if defined(XP_WIN)
-  virtual void SubmitFrame(mozilla::layers::TextureSourceD3D11* aSource,
+  virtual bool SubmitFrame(mozilla::layers::TextureSourceD3D11* aSource,
                            const IntSize& aSize,
-                           const VRHMDSensorState& aSensorState,
                            const gfx::Rect& aLeftEyeRect,
                            const gfx::Rect& aRightEyeRect) override;
 #else
-  virtual void SubmitFrame(mozilla::layers::TextureSourceOGL* aSource,
+  virtual bool SubmitFrame(mozilla::layers::TextureSourceOGL* aSource,
                            const IntSize& aSize,
-                           const VRHMDSensorState& aSensorState,
                            const gfx::Rect& aLeftEyeRect,
                            const gfx::Rect& aRightEyeRect);
 #endif // XP_WIN
 
 public:
   explicit VRDisplayPuppet();
 
 protected:
   virtual ~VRDisplayPuppet();
   void Destroy();
 
-  VRHMDSensorState GetSensorState(double timeOffset);
-
   bool mIsPresenting;
 
 private:
 #if defined(XP_WIN)
   bool UpdateConstantBuffers();
 
   RefPtr<ID3D11Device> mDevice;
   RefPtr<ID3D11DeviceContext> mContext;
@@ -65,17 +61,16 @@ private:
   layers::PixelShaderConstants mPSConstants;
   RefPtr<ID3D11Buffer> mVSConstantBuffer;
   RefPtr<ID3D11Buffer> mPSConstantBuffer;
   RefPtr<ID3D11Buffer> mVertexBuffer;
   RefPtr<ID3D11InputLayout> mInputLayout;
 #endif
 
   VRHMDSensorState mSensorState;
-  uint32_t mFrameNum;
 };
 
 class VRControllerPuppet : public VRControllerHost
 {
 public:
   explicit VRControllerPuppet(dom::GamepadHand aHand);
   void SetButtonPressState(uint32_t aButton, bool aPressed);
   uint64_t GetButtonPressState();
--- a/gfx/vr/ipc/PVRManager.ipdl
+++ b/gfx/vr/ipc/PVRManager.ipdl
@@ -35,27 +35,30 @@ sync protocol PVRManager
 {
   manages PTexture;
   manages PVRLayer;
 
 parent:
   async PTexture(SurfaceDescriptor aSharedData, LayersBackend aBackend,
                  TextureFlags aTextureFlags, uint64_t aSerial);
 
-  async PVRLayer(uint32_t aDisplayID, float aLeftEyeX, float aLeftEyeY, float aLeftEyeWidth, float aLeftEyeHeight, float aRightEyeX, float aRightEyeY, float aRightEyeWidth, float aRightEyeHeight);
+  async PVRLayer(uint32_t aDisplayID, float aLeftEyeX, float aLeftEyeY,
+                 float aLeftEyeWidth, float aLeftEyeHeight, float aRightEyeX,
+                 float aRightEyeY, float aRightEyeWidth, float aRightEyeHeight,
+                 uint32_t aGroup);
 
   // (Re)Enumerate VR Displays.  An updated list of VR displays will be returned
   // asynchronously to children via UpdateDisplayInfo.
   async RefreshDisplays();
 
   // Reset the sensor of the display identified by aDisplayID so that the current
   // sensor state is the "Zero" position.
   async ResetSensor(uint32_t aDisplayID);
 
-  sync GetSensorState(uint32_t aDisplayID) returns(VRHMDSensorState aState);
+  async SetGroupMask(uint32_t aDisplayID, uint32_t aGroupMask);
   async SetHaveEventListener(bool aHaveEventListener);
 
   async ControllerListenerAdded();
   async ControllerListenerRemoved();
   async VibrateHaptic(uint32_t aControllerIdx, uint32_t aHapticIndex,
                       double aIntensity, double aDuration, uint32_t aPromiseID);
   async StopVibrateHaptic(uint32_t aControllerIdx);
 
@@ -76,18 +79,16 @@ child:
   async ParentAsyncMessages(AsyncParentMessageData[] aMessages);
 
   // Notify children of updated VR display enumeration and details.  This will
   // be sent to all children when the parent receives RefreshDisplays, even
   // if no changes have been detected.  This ensures that Promises exposed
   // through DOM calls are always resolved.
   async UpdateDisplayInfo(VRDisplayInfo[] aDisplayUpdates);
 
-  async NotifyVSync();
-  async NotifyVRVSync(uint32_t aDisplayID);
   async DispatchSubmitFrameResult(uint32_t aDisplayID, VRSubmitFrameResultInfo aResult);
   async GamepadUpdate(GamepadChangeEvent aGamepadEvent);
   async ReplyGamepadVibrateHaptic(uint32_t aPromiseID);
 
   async ReplyCreateVRServiceTestDisplay(nsCString aID, uint32_t aPromiseID,
                                         uint32_t aDeviceID);
   async ReplyCreateVRServiceTestController(nsCString aID, uint32_t aPromiseID,
                                            uint32_t aDeviceID);
--- a/gfx/vr/ipc/VRLayerParent.cpp
+++ b/gfx/vr/ipc/VRLayerParent.cpp
@@ -5,21 +5,22 @@
 
 
 #include "VRLayerParent.h"
 #include "mozilla/Unused.h"
 
 namespace mozilla {
 namespace gfx {
 
-VRLayerParent::VRLayerParent(uint32_t aVRDisplayID, const Rect& aLeftEyeRect, const Rect& aRightEyeRect)
+VRLayerParent::VRLayerParent(uint32_t aVRDisplayID, const Rect& aLeftEyeRect, const Rect& aRightEyeRect, const uint32_t aGroup)
   : mIPCOpen(true)
   , mVRDisplayID(aVRDisplayID)
   , mLeftEyeRect(aLeftEyeRect)
   , mRightEyeRect(aRightEyeRect)
+  , mGroup(aGroup)
 {
 }
 
 VRLayerParent::~VRLayerParent()
 {
   MOZ_COUNT_DTOR(VRLayerParent);
 }
 
--- a/gfx/vr/ipc/VRLayerParent.h
+++ b/gfx/vr/ipc/VRLayerParent.h
@@ -14,30 +14,33 @@
 
 namespace mozilla {
 namespace gfx {
 
 class VRLayerParent : public PVRLayerParent {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VRLayerParent)
 
 public:
-  VRLayerParent(uint32_t aVRDisplayID, const Rect& aLeftEyeRect, const Rect& aRightEyeRect);
+  VRLayerParent(uint32_t aVRDisplayID, const Rect& aLeftEyeRect,
+                const Rect& aRightEyeRect, const uint32_t aGroup);
   virtual mozilla::ipc::IPCResult RecvSubmitFrame(PTextureParent* texture) override;
   virtual mozilla::ipc::IPCResult RecvDestroy() override;
   uint32_t GetDisplayID() const { return mVRDisplayID; }
+  uint32_t GetGroup() const { return mGroup; }
 protected:
   virtual void ActorDestroy(ActorDestroyReason aWhy) override;
 
   virtual ~VRLayerParent();
   void Destroy();
 
   bool mIPCOpen;
 
   uint32_t mVRDisplayID;
   gfx::IntSize mSize;
   gfx::Rect mLeftEyeRect;
   gfx::Rect mRightEyeRect;
+  uint32_t mGroup;
 };
 
 } // namespace gfx
 } // namespace mozilla
 
 #endif
--- a/gfx/vr/ipc/VRManagerChild.cpp
+++ b/gfx/vr/ipc/VRManagerChild.cpp
@@ -35,17 +35,16 @@ static StaticRefPtr<VRManagerParent> sVR
 
 void ReleaseVRManagerParentSingleton() {
   sVRManagerParentSingleton = nullptr;
 }
 
 VRManagerChild::VRManagerChild()
   : TextureForwarder()
   , mDisplaysInitialized(false)
-  , mInputFrameID(-1)
   , mMessageLoop(MessageLoop::current())
   , mFrameRequestCallbackCounter(0)
   , mBackend(layers::LayersBackend::LAYERS_NONE)
   , mPromiseID(0)
   , mVRMockDisplay(nullptr)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
@@ -185,17 +184,18 @@ PVRLayerChild*
 VRManagerChild::AllocPVRLayerChild(const uint32_t& aDisplayID,
                                    const float& aLeftEyeX,
                                    const float& aLeftEyeY,
                                    const float& aLeftEyeWidth,
                                    const float& aLeftEyeHeight,
                                    const float& aRightEyeX,
                                    const float& aRightEyeY,
                                    const float& aRightEyeWidth,
-                                   const float& aRightEyeHeight)
+                                   const float& aRightEyeHeight,
+                                   const uint32_t& aGroup)
 {
   RefPtr<VRLayerChild> layer = new VRLayerChild(aDisplayID, this);
   return layer.forget().take();
 }
 
 bool
 VRManagerChild::DeallocPVRLayerChild(PVRLayerChild* actor)
 {
@@ -317,22 +317,16 @@ VRManagerChild::CreateVRServiceTestDispl
 void
 VRManagerChild::CreateVRServiceTestController(const nsCString& aID, dom::Promise* aPromise)
 {
   SendCreateVRServiceTestController(aID, mPromiseID);
   mPromiseList.Put(mPromiseID, aPromise);
   ++mPromiseID;
 }
 
-int
-VRManagerChild::GetInputFrameID()
-{
-  return mInputFrameID;
-}
-
 mozilla::ipc::IPCResult
 VRManagerChild::RecvParentAsyncMessages(InfallibleTArray<AsyncParentMessageData>&& aMessages)
 {
   for (InfallibleTArray<AsyncParentMessageData>::index_type i = 0; i < aMessages.Length(); ++i) {
     const AsyncParentMessageData& message = aMessages[i];
 
     switch (message.type()) {
       case AsyncParentMessageData::TOpNotifyNotUsed: {
@@ -400,33 +394,36 @@ VRManagerChild::DeallocShmem(ipc::Shmem&
 {
   return PVRManagerChild::DeallocShmem(aShmem);
 }
 
 PVRLayerChild*
 VRManagerChild::CreateVRLayer(uint32_t aDisplayID,
                               const Rect& aLeftEyeRect,
                               const Rect& aRightEyeRect,
-                              nsIEventTarget* aTarget)
+                              nsIEventTarget* aTarget,
+                              uint32_t aGroup)
 {
   PVRLayerChild* vrLayerChild = AllocPVRLayerChild(aDisplayID, aLeftEyeRect.x,
                                                    aLeftEyeRect.y, aLeftEyeRect.width,
                                                    aLeftEyeRect.height, aRightEyeRect.x,
                                                    aRightEyeRect.y, aRightEyeRect.width,
-                                                   aRightEyeRect.height);
+                                                   aRightEyeRect.height,
+                                                   aGroup);
   // Do the DOM labeling.
   if (aTarget) {
     SetEventTargetForActor(vrLayerChild, aTarget);
     MOZ_ASSERT(vrLayerChild->GetActorEventTarget());
   }
   return SendPVRLayerConstructor(vrLayerChild, aDisplayID, aLeftEyeRect.x,
                                  aLeftEyeRect.y, aLeftEyeRect.width,
                                  aLeftEyeRect.height, aRightEyeRect.x,
                                  aRightEyeRect.y, aRightEyeRect.width,
-                                 aRightEyeRect.height);
+                                 aRightEyeRect.height,
+                                 aGroup);
 }
 
 
 // XXX TODO - VRManagerChild::FrameRequest is the same as nsIDocument::FrameRequest, should we consolodate these?
 struct VRManagerChild::FrameRequest
 {
   FrameRequest(mozilla::dom::FrameRequestCallback& aCallback,
     int32_t aHandle) :
@@ -474,38 +471,16 @@ VRManagerChild::ScheduleFrameRequestCall
 void
 VRManagerChild::CancelFrameRequestCallback(int32_t aHandle)
 {
   // mFrameRequestCallbacks is stored sorted by handle
   mFrameRequestCallbacks.RemoveElementSorted(aHandle);
 }
 
 mozilla::ipc::IPCResult
-VRManagerChild::RecvNotifyVSync()
-{
-  for (auto& display : mDisplays) {
-    display->NotifyVsync();
-  }
-
-  return IPC_OK();
-}
-
-mozilla::ipc::IPCResult
-VRManagerChild::RecvNotifyVRVSync(const uint32_t& aDisplayID)
-{
-  for (auto& display : mDisplays) {
-    if (display->GetDisplayInfo().GetDisplayID() == aDisplayID) {
-      display->NotifyVRVsync();
-    }
-  }
-
-  return IPC_OK();
-}
-
-mozilla::ipc::IPCResult
 VRManagerChild::RecvGamepadUpdate(const GamepadChangeEvent& aGamepadEvent)
 {
   // VRManagerChild could be at other processes, but GamepadManager
   // only exists at the content process or the same process
   // in non-e10s mode.
   MOZ_ASSERT(XRE_IsContentProcess() || IsSameProcess());
 
   RefPtr<GamepadManager> gamepadManager(GamepadManager::GetService());
--- a/gfx/vr/ipc/VRManagerChild.h
+++ b/gfx/vr/ipc/VRManagerChild.h
@@ -43,17 +43,16 @@ public:
 
   static VRManagerChild* Get();
 
   // Indicate that an observer wants to receive VR events.
   void AddListener(dom::VREventObserver* aObserver);
   // Indicate that an observer should no longer receive VR events.
   void RemoveListener(dom::VREventObserver* aObserver);
 
-  int GetInputFrameID();
   bool GetVRDisplays(nsTArray<RefPtr<VRDisplayClient> >& aDisplays);
   bool RefreshVRDisplaysWithCallback(uint64_t aWindowId);
   void AddPromise(const uint32_t& aID, dom::Promise* aPromise);
 
   void CreateVRServiceTestDisplay(const nsCString& aID, dom::Promise* aPromise);
   void CreateVRServiceTestController(const nsCString& aID, dom::Promise* aPromise);
 
   static void InitSameProcess();
@@ -71,17 +70,18 @@ public:
     uint64_t aSerial,
     wr::MaybeExternalImageId& aExternalImageId,
     nsIEventTarget* aTarget = nullptr) override;
   virtual void CancelWaitForRecycle(uint64_t aTextureId) override;
 
   PVRLayerChild* CreateVRLayer(uint32_t aDisplayID,
                                const Rect& aLeftEyeRect,
                                const Rect& aRightEyeRect,
-                               nsIEventTarget* aTarget);
+                               nsIEventTarget* aTarget,
+                               uint32_t aGroup);
 
   static void IdentifyTextureHost(const layers::TextureFactoryIdentifier& aIdentifier);
   layers::LayersBackend GetBackendType() const;
   layers::SyncObject* GetSyncObject() { return mSyncObject; }
 
   virtual MessageLoop* GetMessageLoop() const override { return mMessageLoop; }
   virtual base::ProcessId GetParentPid() const override { return OtherPid(); }
 
@@ -114,25 +114,24 @@ protected:
   virtual PVRLayerChild* AllocPVRLayerChild(const uint32_t& aDisplayID,
                                             const float& aLeftEyeX,
                                             const float& aLeftEyeY,
                                             const float& aLeftEyeWidth,
                                             const float& aLeftEyeHeight,
                                             const float& aRightEyeX,
                                             const float& aRightEyeY,
                                             const float& aRightEyeWidth,
-                                            const float& aRightEyeHeight) override;
+                                            const float& aRightEyeHeight,
+                                            const uint32_t& aGroup) override;
   virtual bool DeallocPVRLayerChild(PVRLayerChild* actor) override;
 
   virtual mozilla::ipc::IPCResult RecvUpdateDisplayInfo(nsTArray<VRDisplayInfo>&& aDisplayUpdates) override;
 
   virtual mozilla::ipc::IPCResult RecvParentAsyncMessages(InfallibleTArray<AsyncParentMessageData>&& aMessages) override;
 
-  virtual mozilla::ipc::IPCResult RecvNotifyVSync() override;
-  virtual mozilla::ipc::IPCResult RecvNotifyVRVSync(const uint32_t& aDisplayID) override;
   virtual mozilla::ipc::IPCResult RecvDispatchSubmitFrameResult(const uint32_t& aDisplayID, const VRSubmitFrameResultInfo& aResult) override;
   virtual mozilla::ipc::IPCResult RecvGamepadUpdate(const GamepadChangeEvent& aGamepadEvent) override;
   virtual mozilla::ipc::IPCResult RecvReplyGamepadVibrateHaptic(const uint32_t& aPromiseID) override;
 
   virtual mozilla::ipc::IPCResult RecvReplyCreateVRServiceTestDisplay(const nsCString& aID,
                                                                       const uint32_t& aPromiseID,
                                                                       const uint32_t& aDeviceID) override;
   virtual mozilla::ipc::IPCResult RecvReplyCreateVRServiceTestController(const nsCString& aID,
@@ -170,18 +169,16 @@ private:
   * make sure if there is no newer usage.
   */
   void NotifyNotUsed(uint64_t aTextureId, uint64_t aFwdTransactionId);
 
   nsTArray<RefPtr<VRDisplayClient> > mDisplays;
   bool mDisplaysInitialized;
   nsTArray<uint64_t> mNavigatorCallbacks;
 
-  int32_t mInputFrameID;
-
   MessageLoop* mMessageLoop;
 
   struct FrameRequest;
 
   nsTArray<FrameRequest> mFrameRequestCallbacks;
   /**
   * The current frame request callback handle
   */
--- a/gfx/vr/ipc/VRManagerParent.cpp
+++ b/gfx/vr/ipc/VRManagerParent.cpp
@@ -60,22 +60,24 @@ PVRLayerParent*
 VRManagerParent::AllocPVRLayerParent(const uint32_t& aDisplayID,
                                      const float& aLeftEyeX,
                                      const float& aLeftEyeY,
                                      const float& aLeftEyeWidth,
                                      const float& aLeftEyeHeight,
                                      const float& aRightEyeX,
                                      const float& aRightEyeY,
                                      const float& aRightEyeWidth,
-                                     const float& aRightEyeHeight)
+                                     const float& aRightEyeHeight,
+                                     const uint32_t& aGroup)
 {
   RefPtr<VRLayerParent> layer;
   layer = new VRLayerParent(aDisplayID,
                             Rect(aLeftEyeX, aLeftEyeY, aLeftEyeWidth, aLeftEyeHeight),
-                            Rect(aRightEyeX, aRightEyeY, aRightEyeWidth, aRightEyeHeight));
+                            Rect(aRightEyeX, aRightEyeY, aRightEyeWidth, aRightEyeHeight),
+                            aGroup);
   VRManager* vm = VRManager::Get();
   RefPtr<gfx::VRDisplayHost> display = vm->GetDisplay(aDisplayID);
   if (display) {
     display->AddLayer(layer);
   }
   return layer.forget().take();
 }
 
@@ -249,22 +251,22 @@ VRManagerParent::RecvResetSensor(const u
   if (display != nullptr) {
     display->ZeroSensor();
   }
 
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
-VRManagerParent::RecvGetSensorState(const uint32_t& aDisplayID, VRHMDSensorState* aState)
+VRManagerParent::RecvSetGroupMask(const uint32_t& aDisplayID, const uint32_t& aGroupMask)
 {
   VRManager* vm = VRManager::Get();
   RefPtr<gfx::VRDisplayHost> display = vm->GetDisplay(aDisplayID);
   if (display != nullptr) {
-    *aState = display->GetSensorState();
+    display->SetGroupMask(aGroupMask);
   }
   return IPC_OK();
 }
 
 bool
 VRManagerParent::HaveEventListener()
 {
   return mHaveEventListener;
--- a/gfx/vr/ipc/VRManagerParent.h
+++ b/gfx/vr/ipc/VRManagerParent.h
@@ -75,32 +75,32 @@ protected:
   virtual PVRLayerParent* AllocPVRLayerParent(const uint32_t& aDisplayID,
                                               const float& aLeftEyeX,
                                               const float& aLeftEyeY,
                                               const float& aLeftEyeWidth,
                                               const float& aLeftEyeHeight,
                                               const float& aRightEyeX,
                                               const float& aRightEyeY,
                                               const float& aRightEyeWidth,
-                                              const float& aRightEyeHeight) override;
+                                              const float& aRightEyeHeight,
+                                              const uint32_t& aGroup) override;
   virtual bool DeallocPVRLayerParent(PVRLayerParent* actor) override;
 
   virtual void ActorDestroy(ActorDestroyReason why) override;
   void OnChannelConnected(int32_t pid) override;
 
   virtual mozilla::ipc::IPCResult RecvRefreshDisplays() override;
   virtual mozilla::ipc::IPCResult RecvResetSensor(const uint32_t& aDisplayID) override;
-  virtual mozilla::ipc::IPCResult RecvGetSensorState(const uint32_t& aDisplayID, VRHMDSensorState* aState) override;
+  virtual mozilla::ipc::IPCResult RecvSetGroupMask(const uint32_t& aDisplayID, const uint32_t& aGroupMask) override;
   virtual mozilla::ipc::IPCResult RecvSetHaveEventListener(const bool& aHaveEventListener) override;
   virtual mozilla::ipc::IPCResult RecvControllerListenerAdded() override;
   virtual mozilla::ipc::IPCResult RecvControllerListenerRemoved() override;
   virtual mozilla::ipc::IPCResult RecvVibrateHaptic(const uint32_t& aControllerIdx, const uint32_t& aHapticIndex,
                                                     const double& aIntensity, const double& aDuration, const uint32_t& aPromiseID) override;
   virtual mozilla::ipc::IPCResult RecvStopVibrateHaptic(const uint32_t& aControllerIdx) override;
-  
   virtual mozilla::ipc::IPCResult RecvCreateVRTestSystem() override;
   virtual mozilla::ipc::IPCResult RecvCreateVRServiceTestDisplay(const nsCString& aID, const uint32_t& aPromiseID) override;
   virtual mozilla::ipc::IPCResult RecvCreateVRServiceTestController(const nsCString& aID, const uint32_t& aPromiseID) override;
   virtual mozilla::ipc::IPCResult RecvSetDisplayInfoToMockDisplay(const uint32_t& aDeviceID,
                                                                   const VRDisplayInfo& aDisplayInfo) override;
   virtual mozilla::ipc::IPCResult RecvSetSensorStateToMockDisplay(const uint32_t& aDeviceID,
                                                                   const VRHMDSensorState& aSensorState) override;
   virtual mozilla::ipc::IPCResult RecvNewButtonEventToMockController(const uint32_t& aDeviceID, const long& aButton,
--- a/gfx/vr/ipc/VRMessageUtils.h
+++ b/gfx/vr/ipc/VRMessageUtils.h
@@ -35,45 +35,57 @@ struct ParamTraits<mozilla::gfx::VRDispl
   {
     WriteParam(aMsg, aParam.mType);
     WriteParam(aMsg, aParam.mDisplayID);
     WriteParam(aMsg, aParam.mDisplayName);
     WriteParam(aMsg, aParam.mCapabilityFlags);
     WriteParam(aMsg, aParam.mEyeResolution);
     WriteParam(aMsg, aParam.mIsConnected);
     WriteParam(aMsg, aParam.mIsMounted);
-    WriteParam(aMsg, aParam.mIsPresenting);
+    WriteParam(aMsg, aParam.mPresentingGroups);
+    WriteParam(aMsg, aParam.mGroupMask);
     WriteParam(aMsg, aParam.mStageSize);
     WriteParam(aMsg, aParam.mSittingToStandingTransform);
+    WriteParam(aMsg, aParam.mFrameId);
     for (int i = 0; i < mozilla::gfx::VRDisplayInfo::NumEyes; i++) {
       WriteParam(aMsg, aParam.mEyeFOV[i]);
       WriteParam(aMsg, aParam.mEyeTranslation[i]);
     }
+    for (int i = 0; i < mozilla::gfx::kVRMaxLatencyFrames; i++) {
+      WriteParam(aMsg, aParam.mLastSensorState[i]);
+    }
   }
 
   static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
   {
     if (!ReadParam(aMsg, aIter, &(aResult->mType)) ||
         !ReadParam(aMsg, aIter, &(aResult->mDisplayID)) ||
         !ReadParam(aMsg, aIter, &(aResult->mDisplayName)) ||
         !ReadParam(aMsg, aIter, &(aResult->mCapabilityFlags)) ||
         !ReadParam(aMsg, aIter, &(aResult->mEyeResolution)) ||
         !ReadParam(aMsg, aIter, &(aResult->mIsConnected)) ||
         !ReadParam(aMsg, aIter, &(aResult->mIsMounted)) ||
-        !ReadParam(aMsg, aIter, &(aResult->mIsPresenting)) ||
+        !ReadParam(aMsg, aIter, &(aResult->mPresentingGroups)) ||
+        !ReadParam(aMsg, aIter, &(aResult->mGroupMask)) ||
         !ReadParam(aMsg, aIter, &(aResult->mStageSize)) ||
-        !ReadParam(aMsg, aIter, &(aResult->mSittingToStandingTransform))) {
+        !ReadParam(aMsg, aIter, &(aResult->mSittingToStandingTransform)) ||
+        !ReadParam(aMsg, aIter, &(aResult->mFrameId))) {
       return false;
     }
     for (int i = 0; i < mozilla::gfx::VRDisplayInfo::NumEyes; i++) {
       if (!ReadParam(aMsg, aIter, &(aResult->mEyeFOV[i])) ||
           !ReadParam(aMsg, aIter, &(aResult->mEyeTranslation[i]))) {
         return false;
       }
     }
+    for (int i = 0; i < mozilla::gfx::kVRMaxLatencyFrames; i++) {
+      if (!ReadParam(aMsg, aIter, &(aResult->mLastSensorState[i]))) {
+        return false;
+      }
+    }
 
     return true;
   }
 };
 
 template <>
 struct ParamTraits<mozilla::gfx::VRHMDSensorState>
 {
--- a/gfx/vr/ovr_capi_dynamic.h
+++ b/gfx/vr/ovr_capi_dynamic.h
@@ -718,14 +718,26 @@ typedef ovrResult (OVR_PFN* pfn_ovr_Crea
 
 typedef ovrResult (OVR_PFN* pfn_ovr_GetMirrorTextureBufferGL)(ovrSession session,
 	ovrMirrorTexture mirrorTexture,
 	unsigned int* out_TexId);
 
 #define OVR_KEY_EYE_HEIGHT "EyeHeight" // float meters
 #define OVR_DEFAULT_EYE_HEIGHT 1.675f
 
+#if !defined(OVR_SUCCESS)
+#define OVR_SUCCESS(result) (result >= 0)
+#endif
+
+#if !defined(OVR_UNQUALIFIED_SUCCESS)
+#define OVR_UNQUALIFIED_SUCCESS(result) (result == ovrSuccess)
+#endif
+
+#if !defined(OVR_FAILURE)
+#define OVR_FAILURE(result) (!OVR_SUCCESS(result))
+#endif
+
 #ifdef __cplusplus
 }
 #endif
 
 #endif /* mozilla_ovr_capi_dynamic_h_ */
 #endif /* OVR_CAPI_h */
--- a/ipc/ipdl/sync-messages.ini
+++ b/ipc/ipdl/sync-messages.ini
@@ -1045,18 +1045,16 @@ description =
 [PWebRenderBridge::DPSyncEnd]
 description =
 [PWebRenderBridge::DPGetSnapshot]
 description =
 [PWebRenderBridge::SetAsyncScrollOffset]
 description = test only
 [PWebRenderBridge::SetAsyncZoom]
 description = test only
-[PVRManager::GetSensorState]
-description =
 [PHal::GetCurrentBatteryInformation]
 description =
 [PHal::GetCurrentNetworkInformation]
 description =
 [PHal::GetScreenEnabled]
 description =
 [PHal::GetKeyLightEnabled]
 description =
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -2667,29 +2667,31 @@ nsLayoutUtils::PostTranslate(Matrix4x4& 
   if (aRounded) {
     gfxOrigin.x = NS_round(gfxOrigin.x);
     gfxOrigin.y = NS_round(gfxOrigin.y);
   }
   aTransform.PostTranslate(gfxOrigin);
 }
 
 Matrix4x4
-nsLayoutUtils::GetTransformToAncestor(nsIFrame *aFrame, const nsIFrame *aAncestor)
+nsLayoutUtils::GetTransformToAncestor(nsIFrame *aFrame,
+                                      const nsIFrame *aAncestor,
+                                      bool aInCSSUnits)
 {
   nsIFrame* parent;
   Matrix4x4 ctm;
   if (aFrame == aAncestor) {
     return ctm;
   }
-  ctm = aFrame->GetTransformMatrix(aAncestor, &parent);
+  ctm = aFrame->GetTransformMatrix(aAncestor, &parent, aInCSSUnits);
   while (parent && parent != aAncestor) {
     if (!parent->Extend3DContext()) {
       ctm.ProjectTo2D();
     }
-    ctm = ctm * parent->GetTransformMatrix(aAncestor, &parent);
+    ctm = ctm * parent->GetTransformMatrix(aAncestor, &parent, aInCSSUnits);
   }
   return ctm;
 }
 
 gfxSize
 nsLayoutUtils::GetTransformToAncestorScale(nsIFrame* aFrame)
 {
   Matrix4x4 transform = GetTransformToAncestor(aFrame,
--- a/layout/base/nsLayoutUtils.h
+++ b/layout/base/nsLayoutUtils.h
@@ -844,19 +844,22 @@ public:
                                              const nsRect& aRect,
                                              const nsIFrame* aAncestor,
                                              bool* aPreservesAxisAlignedRectangles = nullptr,
                                              mozilla::Maybe<Matrix4x4>* aMatrixCache = nullptr);
 
 
   /**
    * Gets the transform for aFrame relative to aAncestor. Pass null for
-   * aAncestor to go up to the root frame.
+   * aAncestor to go up to the root frame. aInCSSUnits set to true will
+   * return CSS units, set to false (the default) will return App units.
    */
-  static Matrix4x4 GetTransformToAncestor(nsIFrame *aFrame, const nsIFrame *aAncestor);
+  static Matrix4x4 GetTransformToAncestor(nsIFrame *aFrame,
+                                          const nsIFrame *aAncestor,
+                                          bool aInCSSUnits = false);
 
   /**
    * Gets the scale factors of the transform for aFrame relative to the root
    * frame if this transform is 2D, or the identity scale factors otherwise.
    */
   static gfxSize GetTransformToAncestorScale(nsIFrame* aFrame);
 
   /**
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -6275,31 +6275,33 @@ nsIFrame::GetNearestWidget(nsPoint& aOff
   nsIWidget* widget =
     GetClosestView(&offsetToView)->GetNearestWidget(&offsetToWidget);
   aOffset = offsetToView + offsetToWidget;
   return widget;
 }
 
 Matrix4x4
 nsIFrame::GetTransformMatrix(const nsIFrame* aStopAtAncestor,
-                             nsIFrame** aOutAncestor)
+                             nsIFrame** aOutAncestor,
+                             bool aInCSSUnits)
 {
   NS_PRECONDITION(aOutAncestor, "Need a place to put the ancestor!");
 
   /* If we're transformed, we want to hand back the combination
    * transform/translate matrix that will apply our current transform, then
    * shift us to our parent.
    */
   if (IsTransformed()) {
     /* Compute the delta to the parent, which we need because we are converting
      * coordinates to our parent.
      */
     NS_ASSERTION(nsLayoutUtils::GetCrossDocParentFrame(this),
                  "Cannot transform the viewport frame!");
-    int32_t scaleFactor = PresContext()->AppUnitsPerDevPixel();
+    int32_t scaleFactor = (aInCSSUnits ? PresContext()->AppUnitsPerCSSPixel()
+                                       : PresContext()->AppUnitsPerDevPixel());
 
     Matrix4x4 result = nsDisplayTransform::GetResultingTransformMatrix(this,
                          nsPoint(0,0), scaleFactor,
                          nsDisplayTransform::INCLUDE_PERSPECTIVE|nsDisplayTransform::OFFSET_BY_ORIGIN,
                          nullptr);
     *aOutAncestor = nsLayoutUtils::GetCrossDocParentFrame(this);
     nsPoint delta = GetOffsetToCrossDoc(*aOutAncestor);
     /* Combine the raw transform with a translation to our parent. */
@@ -6374,17 +6376,18 @@ nsIFrame::GetTransformMatrix(const nsIFr
   }
 
   NS_ASSERTION(*aOutAncestor, "Somehow ended up with a null ancestor...?");
 
   /* Translate from this frame to our ancestor, if it exists.  That's the
    * entire transform, so we're done.
    */
   nsPoint delta = GetOffsetToCrossDoc(*aOutAncestor);
-  int32_t scaleFactor = PresContext()->AppUnitsPerDevPixel();
+  int32_t scaleFactor = (aInCSSUnits ? PresContext()->AppUnitsPerCSSPixel()
+                                     : PresContext()->AppUnitsPerDevPixel());
   return Matrix4x4::Translation(NSAppUnitsToFloatPixels(delta.x, scaleFactor),
                                 NSAppUnitsToFloatPixels(delta.y, scaleFactor),
                                 0.0f);
 }
 
 static void InvalidateRenderingObservers(nsIFrame* aFrame)
 {
   nsSVGEffects::InvalidateDirectRenderingObservers(aFrame);
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -2744,17 +2744,18 @@ public:
    * this frame is not a root frame, then *aOutAncestor will be in the same
    * document as this frame. If this frame IsTransformed(), then *aOutAncestor
    * will be the parent frame (if not preserve-3d) or the nearest non-transformed
    * ancestor (if preserve-3d).
    * @return A Matrix4x4 that converts points in this frame's coordinate space
    *   into points in aOutAncestor's coordinate space.
    */
   Matrix4x4 GetTransformMatrix(const nsIFrame* aStopAtAncestor,
-                               nsIFrame **aOutAncestor);
+                               nsIFrame **aOutAncestor,
+                               bool aInCSSUnits = false);
 
   /**
    * Bit-flags to pass to IsFrameOfType()
    */
   enum {
     eMathML =                           1 << 0,
     eSVG =                              1 << 1,
     eSVGForeignObject =                 1 << 2,
--- a/layout/reftests/stylesheet-cloning/reftest.list
+++ b/layout/reftests/stylesheet-cloning/reftest.list
@@ -1,6 +1,6 @@
 fuzzy-if(webrender,212,342) == counter-style-rule-clone.html glyphs-ref.html # passes trivially
 # because "Dynamic change on @counter-style not yet supported"
 == document-rule-clone.html shouldbegreen-ref.html
 == media-rule-clone.html shouldbegreen-ref.html
 == style-rule-clone.html shouldbegreen-ref.html
-skip-if(stylo) == supports-rule-clone.html shouldbegreen-ref.html # bug 1367610
+== supports-rule-clone.html shouldbegreen-ref.html
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -164,16 +164,18 @@ public class GeckoPreferences
     public static final String PREFS_ACTIVITY_STREAM = NON_PREF_PREFIX + "experiments.activitystream";
     public static final String PREFS_CATEGORY_EXPERIMENTAL_FEATURES = NON_PREF_PREFIX + "category_experimental";
     public static final String PREFS_COMPACT_TABS = NON_PREF_PREFIX + "compact_tabs";
     public static final String PREFS_SHOW_QUIT_MENU = NON_PREF_PREFIX + "distribution.show_quit_menu";
     public static final String PREFS_SEARCH_SUGGESTIONS_ENABLED = "browser.search.suggest.enabled";
     public static final String PREFS_DEFAULT_BROWSER = NON_PREF_PREFIX + "default_browser.link";
     public static final String PREFS_SYSTEM_FONT_SIZE = NON_PREF_PREFIX + "font.size.use_system_font_size";
     public static final String PREFS_SET_AS_HOMEPAGE = NON_PREF_PREFIX + "distribution.set_as_homepage";
+    public static final String PREFS_DIST_HOMEPAGE = NON_PREF_PREFIX + "distribution.homepage";
+    public static final String PREFS_DIST_HOMEPAGE_NAME = NON_PREF_PREFIX + "distribution.homepage.name";
 
     private static final String ACTION_STUMBLER_UPLOAD_PREF = "STUMBLER_PREF";
 
 
     // This isn't a Gecko pref, even if it looks like one.
     private static final String PREFS_BROWSER_LOCALE = "locale";
 
     public static final String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession3";
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/SetHomepagePreference.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/SetHomepagePreference.java
@@ -15,23 +15,25 @@ import android.preference.DialogPreferen
 import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.EditText;
 import android.widget.RadioButton;
 import android.widget.RadioGroup;
 
+
 public class SetHomepagePreference extends DialogPreference {
     private static final String DEFAULT_HOMEPAGE = AboutPages.HOME;
 
     private final SharedPreferences prefs;
 
     private RadioGroup homepageLayout;
     private RadioButton defaultRadio;
+    private RadioButton distributionRadio;
     private RadioButton userAddressRadio;
     private EditText homepageEditText;
 
     // This is the url that 1) was loaded from prefs or, 2) stored
     // when the user pressed the "default homepage" checkbox.
     private String storedUrl;
 
     public SetHomepagePreference(final Context context, final AttributeSet attrs) {
@@ -46,16 +48,17 @@ public class SetHomepagePreference exten
     }
 
     @Override
     protected void onBindDialogView(final View view) {
         super.onBindDialogView(view);
 
         homepageLayout = (RadioGroup) view.findViewById(R.id.homepage_layout);
         defaultRadio = (RadioButton) view.findViewById(R.id.radio_default);
+        distributionRadio = (RadioButton) view.findViewById(R.id.radio_distribution);
         userAddressRadio = (RadioButton) view.findViewById(R.id.radio_user_address);
         homepageEditText = (EditText) view.findViewById(R.id.edittext_user_address);
 
         storedUrl = prefs.getString(GeckoPreferences.PREFS_HOMEPAGE, DEFAULT_HOMEPAGE);
 
         homepageLayout.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {
             @Override
             public void onCheckedChanged(final RadioGroup radioGroup, final int checkedId) {
@@ -66,28 +69,42 @@ public class SetHomepagePreference exten
                     homepageEditText.setVisibility(View.GONE);
                 }
             }
         });
         setUIState(storedUrl);
     }
 
     private void setUIState(final String url) {
+        if (prefs.contains(GeckoPreferences.PREFS_DIST_HOMEPAGE_NAME) &&
+            prefs.contains(GeckoPreferences.PREFS_DIST_HOMEPAGE)) {
+            distributionRadio.setText(prefs.getString(GeckoPreferences.PREFS_DIST_HOMEPAGE_NAME, ""));
+        } else {
+            distributionRadio.setVisibility(View.GONE);
+        }
         if (isUrlDefaultHomepage(url)) {
             defaultRadio.setChecked(true);
+        } else if (distributionRadio.getVisibility() == View.VISIBLE &&
+                   isUrlDistributionHomepage(url)) {
+            distributionRadio.setChecked(true);
         } else {
             userAddressRadio.setChecked(true);
             homepageEditText.setText(url);
         }
     }
 
     private boolean isUrlDefaultHomepage(final String url) {
         return TextUtils.isEmpty(url) || DEFAULT_HOMEPAGE.equals(url);
     }
 
+    private boolean isUrlDistributionHomepage(final String url) {
+        String distributionHomepage = prefs.getString(GeckoPreferences.PREFS_DIST_HOMEPAGE, "");
+        return distributionHomepage.equals(url);
+    }
+
     private static void openKeyboardAndSelectAll(final Context context, final View viewToFocus) {
         viewToFocus.requestFocus();
         viewToFocus.post(new Runnable() {
             @Override
             public void run() {
                 InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
                 imm.showSoftInput(viewToFocus, InputMethodManager.SHOW_IMPLICIT);
                 // android:selectAllOnFocus doesn't work for the initial focus:
@@ -101,18 +118,21 @@ public class SetHomepagePreference exten
 
     @Override
     protected void onDialogClosed(final boolean positiveResult) {
         super.onDialogClosed(positiveResult);
         if (positiveResult) {
             final SharedPreferences.Editor editor = prefs.edit();
             final String homePageEditTextValue = homepageEditText.getText().toString();
             final String newPrefValue;
-            if (homepageLayout.getCheckedRadioButtonId() == R.id.radio_default ||
-                    isUrlDefaultHomepage(homePageEditTextValue)) {
+            if (homepageLayout.getCheckedRadioButtonId() == R.id.radio_distribution) {
+                newPrefValue = prefs.getString(GeckoPreferences.PREFS_DIST_HOMEPAGE, "");
+                editor.putString(GeckoPreferences.PREFS_HOMEPAGE, newPrefValue);
+            } else if (homepageLayout.getCheckedRadioButtonId() == R.id.radio_default ||
+                       isUrlDefaultHomepage(homePageEditTextValue)) {
                 newPrefValue = "";
                 editor.remove(GeckoPreferences.PREFS_HOMEPAGE);
             } else {
                 newPrefValue = homePageEditTextValue;
                 editor.putString(GeckoPreferences.PREFS_HOMEPAGE, newPrefValue);
             }
             editor.apply();
 
--- a/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabs/TabsPanel.java
@@ -183,16 +183,21 @@ public class TabsPanel extends LinearLay
             public void onClick(View view) {
                 mActivity.onBackPressed();
             }
         });
     }
 
     public void showMenu() {
         final Menu menu = mPopupMenu.getMenu();
+        // Ensure we update the anchor here to absolutely guarantee there's an anchor
+        // We do set this during prepareToShow(), however only via a UI-thread callback. There are no
+        // guarantees that that callback will complete before a user clicks on the menu button, so
+        // we need to ensure we've set an anchor here.
+        mPopupMenu.setAnchor(mMenuButton);
 
         // Each panel has a "+" shortcut button, so don't show it for that panel.
         menu.findItem(R.id.new_tab).setVisible(mCurrentPanel != Panel.NORMAL_TABS);
         menu.findItem(R.id.new_private_tab).setVisible(mCurrentPanel != Panel.PRIVATE_TABS
                 && Restrictions.isAllowed(mContext, Restrictable.PRIVATE_BROWSING));
 
         // Only show "Clear * tabs" for current panel.
         menu.findItem(R.id.close_all_tabs).setVisible(mCurrentPanel == Panel.NORMAL_TABS);
@@ -379,22 +384,26 @@ public class TabsPanel extends LinearLay
             default:
                 throw new IllegalArgumentException("Unknown panel type " + panelToShow);
         }
         mPanel.show();
 
         mAddTab.setVisibility(View.VISIBLE);
 
         mMenuButton.setEnabled(true);
-        // If mPopupMenu is visible then setAnchor redisplays the menu on its new anchor - but we
-        // may have just been inflated, so give mMenuButton a chance to get its true measurements
-        // before mPopupMenu.setAnchor reads them to determine its offset from the anchor.
-        ThreadUtils.postToUiThread(new Runnable() {
+        mMenuButton.addOnLayoutChangeListener(new OnLayoutChangeListener() {
             @Override
-            public void run() {
+            public void onLayoutChange(View v, int left, int top, int right, int bottom,
+                                       int oldLeft,
+                                       int oldTop, int oldRight, int oldBottom) {
+                // We also set the anchor in showMenu(), but we need to update it in case the menu
+                // is already showing.
+                // If mPopupMenu is visible then setAnchor redisplays the menu on its new anchor - but we
+                // may have just been inflated, so give mMenuButton a chance to get its true measurements
+                // before mPopupMenu.setAnchor reads them to determine its offset from the anchor.
                 mPopupMenu.setAnchor(mMenuButton);
             }
         });
     }
 
     public void hide() {
         mHeaderVisible = false;
 
--- a/mobile/android/base/java/org/mozilla/gecko/widget/GeckoPopupMenu.java
+++ b/mobile/android/base/java/org/mozilla/gecko/widget/GeckoPopupMenu.java
@@ -132,16 +132,20 @@ public class GeckoPopupMenu implements G
     public void setOnMenuItemLongClickListener(OnMenuItemLongClickListener listener) {
         mLongClickListener = listener;
     }
 
     /**
      * Show the inflated menu.
      */
     public void show() {
+        if (mAnchor == null) {
+            throw new IllegalStateException("GeckoPopupMenu.show() called without preceeding call to setAnchor()");
+        }
+
         if (!mMenuPopup.isShowing())
             mMenuPopup.showAsDropDown(mAnchor);
     }
 
     /**
      * Hide the inflated menu.
      */
     public void dismiss() {
--- a/mobile/android/base/resources/layout/preference_set_homepage.xml
+++ b/mobile/android/base/resources/layout/preference_set_homepage.xml
@@ -1,27 +1,35 @@
 <?xml version="1.0" encoding="utf-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/. -->
 <RadioGroup xmlns:android="http://schemas.android.com/apk/res/android"
+            xmlns:tools="http://schemas.android.com/tools"
             android:id="@+id/homepage_layout"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:paddingLeft="20dp"
             android:paddingRight="20dp"
             android:orientation="vertical">
 
     <RadioButton android:id="@+id/radio_default"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_marginTop="8dp"
                  android:text="@string/home_homepage_radio_default"
                  android:textColor="@color/text_and_tabs_tray_grey"/>
 
+    <RadioButton android:id="@+id/radio_distribution"
+                 android:layout_width="wrap_content"
+                 android:layout_height="wrap_content"
+                 android:layout_marginTop="8dp"
+                 tools:text="Distribution Homepage"
+                 android:textColor="@color/text_and_tabs_tray_grey"/>
+
     <RadioButton android:id="@+id/radio_user_address"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_marginTop="8dp"
                  android:text="@string/home_homepage_radio_user_address"
                  android:textColor="@color/text_and_tabs_tray_grey"/>
 
     <!-- RadioGroup is a LinearLayout under the hood, so including this View is fine.
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -662,23 +662,18 @@ pref("apz.allow_zooming", false);
 // 1 = STANDARD (Once locked, remain locked until scrolling ends)
 // 2 = STICKY (Allow lock to be broken, with hysteresis)
 pref("apz.axis_lock.mode", 0);
 pref("apz.axis_lock.lock_angle", "0.5235987");        // PI / 6 (30 degrees)
 pref("apz.axis_lock.breakout_threshold", "0.03125");  // 1/32 inches
 pref("apz.axis_lock.breakout_angle", "0.3926991");    // PI / 8 (22.5 degrees)
 pref("apz.axis_lock.direct_pan_angle", "1.047197");   // PI / 3 (60 degrees)
 pref("apz.content_response_timeout", 400);
-#ifdef NIGHTLY_BUILD
 pref("apz.drag.enabled", true);
 pref("apz.drag.initial.enabled", true);
-#else
-pref("apz.drag.enabled", false);
-pref("apz.drag.initial.enabled", false);
-#endif
 pref("apz.danger_zone_x", 50);
 pref("apz.danger_zone_y", 100);
 pref("apz.disable_for_scroll_linked_effects", false);
 pref("apz.displayport_expiry_ms", 15000);
 pref("apz.enlarge_displayport_when_clipped", false);
 pref("apz.fling_accel_base_mult", "1.0");
 pref("apz.fling_accel_interval_ms", 500);
 pref("apz.fling_accel_min_velocity", "1.5");
--- a/netwerk/base/nsIOService.cpp
+++ b/netwerk/base/nsIOService.cpp
@@ -54,21 +54,16 @@
 #include "mozilla/net/NeckoChild.h"
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/net/CaptivePortalService.h"
 #include "ReferrerPolicy.h"
 #include "nsContentSecurityManager.h"
 #include "nsContentUtils.h"
 #include "xpcpublic.h"
 
-#ifdef MOZ_WIDGET_GONK
-#include "nsINetworkManager.h"
-#include "nsINetworkInterface.h"
-#endif
-
 namespace mozilla {
 namespace net {
 
 #define PORT_PREF_PREFIX           "network.security.ports."
 #define PORT_PREF(x)               PORT_PREF_PREFIX x
 #define MANAGE_OFFLINE_STATUS_PREF "network.manage-offline-status"
 #define OFFLINE_MIRRORS_CONNECTIVITY "network.offline-mirrors-connectivity"
 
--- a/netwerk/base/nsIncrementalDownload.cpp
+++ b/netwerk/base/nsIncrementalDownload.cpp
@@ -44,37 +44,16 @@ using namespace mozilla;
 //-----------------------------------------------------------------------------
 
 static nsresult
 WriteToFile(nsIFile *lf, const char *data, uint32_t len, int32_t flags)
 {
   PRFileDesc *fd;
   int32_t mode = 0600;
   nsresult rv;
-#if defined(MOZ_WIDGET_GONK)
-  // The sdcard on a B2G phone looks like:
-  // d---rwx--- system   sdcard_rw          1970-01-01 01:00:00 sdcard
-  // On the emulator, xpcshell fails when using 0600 mode to open the file,
-  // and 0660 works.
-  nsCOMPtr<nsIFile> parent;
-  rv = lf->GetParent(getter_AddRefs(parent));
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-  uint32_t  parentPerm;
-  rv = parent->GetPermissions(&parentPerm);
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-  if ((parentPerm & 0700) == 0) {
-    // Parent directory has no owner-write, so try to use group permissions
-    // instead of owner permissions.
-    mode = 0660;
-  }
-#endif
   rv = lf->OpenNSPRFileDesc(flags, mode, &fd);
   if (NS_FAILED(rv))
     return rv;
 
   if (len)
     rv = PR_Write(fd, data, len) == int32_t(len) ? NS_OK : NS_ERROR_FAILURE;
 
   PR_Close(fd);
--- a/netwerk/base/nsLoadGroup.cpp
+++ b/netwerk/base/nsLoadGroup.cpp
@@ -820,17 +820,23 @@ nsLoadGroup::SetUserAgentOverrideCache(c
 }
 
 
 ////////////////////////////////////////////////////////////////////////////////
 
 void
 nsLoadGroup::TelemetryReport()
 {
-    if (mDefaultLoadIsTimed) {
+    nsresult defaultStatus = NS_ERROR_INVALID_ARG;
+    // We should only report HTTP_PAGE_* telemetry if the defaultRequest was
+    // actually successful.
+    if (mDefaultLoadRequest) {
+        mDefaultLoadRequest->GetStatus(&defaultStatus);
+    }
+    if (mDefaultLoadIsTimed && NS_SUCCEEDED(defaultStatus)) {
         Telemetry::Accumulate(Telemetry::HTTP_REQUEST_PER_PAGE, mTimedRequests);
         if (mTimedRequests) {
             Telemetry::Accumulate(Telemetry::HTTP_REQUEST_PER_PAGE_FROM_CACHE,
                                   mCachedRequests * 100 / mTimedRequests);
         }
 
         nsCOMPtr<nsITimedChannel> timedChannel =
             do_QueryInterface(mDefaultLoadRequest);
--- a/netwerk/base/nsNetUtil.cpp
+++ b/netwerk/base/nsNetUtil.cpp
@@ -66,21 +66,16 @@
 #include "mozilla/dom/nsCSPUtils.h"
 #include "mozilla/net/HttpBaseChannel.h"
 #include "nsIScriptError.h"
 #include "nsISiteSecurityService.h"
 #include "nsHttpHandler.h"
 #include "nsNSSComponent.h"
 #include "nsIRedirectHistoryEntry.h"
 
-#ifdef MOZ_WIDGET_GONK
-#include "nsINetworkManager.h"
-#include "nsThreadUtils.h" // for NS_IsMainThread
-#endif
-
 #include <limits>
 
 using namespace mozilla;
 using namespace mozilla::net;
 
 #define DEFAULT_USER_CONTROL_RP 3
 
 static uint32_t sUserControlRp = DEFAULT_USER_CONTROL_RP;
--- a/netwerk/base/nsUDPSocket.cpp
+++ b/netwerk/base/nsUDPSocket.cpp
@@ -28,20 +28,16 @@
 #include "nsIPipe.h"
 #include "prerror.h"
 #include "nsThreadUtils.h"
 #include "nsIDNSRecord.h"
 #include "nsIDNSService.h"
 #include "nsICancelable.h"
 #include "nsWrapperCacheInlines.h"
 
-#ifdef MOZ_WIDGET_GONK
-#include "NetStatistics.h"
-#endif
-
 namespace mozilla {
 namespace net {
 
 static const uint32_t UDP_PACKET_CHUNK_SIZE = 1400;
 
 //-----------------------------------------------------------------------------
 
 typedef void (nsUDPSocket:: *nsUDPSocketFunc)(void);
--- a/netwerk/base/nsUDPSocket.h
+++ b/netwerk/base/nsUDPSocket.h
@@ -7,21 +7,16 @@
 #define nsUDPSocket_h__
 
 #include "nsIUDPSocket.h"
 #include "mozilla/Mutex.h"
 #include "nsIOutputStream.h"
 #include "nsAutoPtr.h"
 #include "nsCycleCollectionParticipant.h"
 
-#ifdef MOZ_WIDGET_GONK
-#include "nsINetworkInterface.h"
-#include "nsProxyRelease.h"
-#endif
-
 //-----------------------------------------------------------------------------
 
 namespace mozilla {
 namespace net {
 
 class nsUDPSocket final : public nsASocketHandler
                         , public nsIUDPSocket
 {
--- a/netwerk/dns/mdns/libmdns/nsDNSServiceDiscovery.cpp
+++ b/netwerk/dns/mdns/libmdns/nsDNSServiceDiscovery.cpp
@@ -4,51 +4,29 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsDNSServiceDiscovery.h"
 #include "MDNSResponderOperator.h"
 #include "nsICancelable.h"
 #include "nsXULAppAPI.h"
 #include "private/pprio.h"
 
-#ifdef MOZ_WIDGET_GONK
-#include <cutils/properties.h>
-#endif // MOZ_WIDGET_GONK
-
 namespace mozilla {
 namespace net {
 
 namespace {
 
 inline void
 StartService()
 {
-#ifdef MOZ_WIDGET_GONK
-  char value[PROPERTY_VALUE_MAX] = { '\0' };
-  property_get("init.svc.mdnsd", value, "");
-
-  if (strcmp(value, "running") == 0) {
-    return;
-  }
-  property_set("ctl.start", "mdnsd");
-#endif // MOZ_WIDGET_GONK
 }
 
 inline void
 StopService()
 {
-#ifdef MOZ_WIDGET_GONK
-  char value[PROPERTY_VALUE_MAX] = { '\0' };
-  property_get("init.svc.mdnsd", value, "");
-
-  if (strcmp(value, "stopped") == 0) {
-    return;
-  }
-  property_set("ctl.stop", "mdnsd");
-#endif // MOZ_WIDGET_GONK
 }
 
 class ServiceCounter
 {
 public:
   static bool IsServiceRunning()
   {
     return !!sUseCount;
@@ -152,19 +130,16 @@ RegisterRequest::Cancel(nsresult aReason
 }
 
 } // namespace anonymous
 
 NS_IMPL_ISUPPORTS(nsDNSServiceDiscovery, nsIDNSServiceDiscovery)
 
 nsDNSServiceDiscovery::~nsDNSServiceDiscovery()
 {
-#ifdef MOZ_WIDGET_GONK
-  StopService();
-#endif
 }
 
 nsresult
 nsDNSServiceDiscovery::Init()
 {
   if (!XRE_IsParentProcess()) {
     MOZ_ASSERT(false, "nsDNSServiceDiscovery can only be used in parent process");
     return NS_ERROR_FAILURE;
--- a/netwerk/dns/mdns/libmdns/nsMulticastDNSModule.cpp
+++ b/netwerk/dns/mdns/libmdns/nsMulticastDNSModule.cpp
@@ -1,14 +1,14 @@
 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
-#if defined(MOZ_WIDGET_COCOA) || (defined(MOZ_WIDGET_GONK) && ANDROID_VERSION >= 16)
+#if defined(MOZ_WIDGET_COCOA)
 #define ENABLE_DNS_SERVICE_DISCOVERY
 #endif
 
 #include "mozilla/ModuleUtils.h"
 
 #ifdef ENABLE_DNS_SERVICE_DISCOVERY
 #include "nsDNSServiceDiscovery.h"
 #endif
--- a/netwerk/protocol/ftp/nsFtpConnectionThread.cpp
+++ b/netwerk/protocol/ftp/nsFtpConnectionThread.cpp
@@ -40,20 +40,16 @@
 #include "nsIRunnable.h"
 #include "nsISocketTransportService.h"
 #include "nsIURI.h"
 #include "nsILoadInfo.h"
 #include "NullPrincipal.h"
 #include "nsIAuthPrompt2.h"
 #include "nsIFTPChannelParentInternal.h"
 
-#ifdef MOZ_WIDGET_GONK
-#include "NetStatistics.h"
-#endif
-
 using namespace mozilla;
 using namespace mozilla::net;
 
 extern LazyLogModule gFTPLog;
 #define LOG(args)         MOZ_LOG(gFTPLog, mozilla::LogLevel::Debug, args)
 #define LOG_INFO(args)  MOZ_LOG(gFTPLog, mozilla::LogLevel::Info, args)
 
 // remove FTP parameters (starting with ";") from the path
@@ -1611,23 +1607,16 @@ nsFtpState::R_opts() {
 nsresult
 nsFtpState::Init(nsFtpChannel *channel)
 {
     // parameter validation
     NS_ASSERTION(channel, "FTP: needs a channel");
 
     mChannel = channel; // a straight ref ptr to the channel
 
-#ifdef MOZ_WIDGET_GONK
-    nsCOMPtr<nsINetworkInfo> activeNetworkInfo;
-    GetActiveNetworkInfo(activeNetworkInfo);
-    mActiveNetworkInfo =
-        new nsMainThreadPtrHolder<nsINetworkInfo>(activeNetworkInfo);
-#endif
-
     mKeepRunning = true;
     mSuppliedEntityID = channel->EntityID();
 
     if (channel->UploadStream())
         mAction = PUT;
 
     nsresult rv;
     nsCOMPtr<nsIURL> url = do_QueryInterface(mChannel->URI());
--- a/netwerk/protocol/ftp/nsFtpConnectionThread.h
+++ b/netwerk/protocol/ftp/nsFtpConnectionThread.h
@@ -12,21 +12,16 @@
 #include "nsCOMPtr.h"
 #include "nsIAsyncInputStream.h"
 #include "nsAutoPtr.h"
 #include "nsITransport.h"
 #include "mozilla/net/DNS.h"
 #include "nsFtpControlConnection.h"
 #include "nsIProtocolProxyCallback.h"
 
-#ifdef MOZ_WIDGET_GONK
-#include "nsINetworkInterface.h"
-#include "nsProxyRelease.h"
-#endif
-
<