Merge mozilla-central to mozilla-inbound
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 29 Jul 2016 12:34:10 +0200
changeset 347302 674c51af1dcc31a139b792ebef9ebcd509f87ed3
parent 347301 bff4f9be49619c6d224f716cfbb9c258424c5983 (current diff)
parent 347245 2ea3d51ba1bb9f5c3b6921c43ea63f70b4fdf5d2 (diff)
child 347303 dd66dc5f5603d0d5e885353094f71d99b17121e2
push id6389
push userraliiev@mozilla.com
push dateMon, 19 Sep 2016 13:38:22 +0000
treeherdermozilla-beta@01d67bfe6c81 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone50.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 mozilla-central to mozilla-inbound
layout/style/nsComputedDOMStyle.cpp
layout/style/nsStyleStruct.cpp
layout/style/nsStyleStruct.h
--- a/.eslintignore
+++ b/.eslintignore
@@ -107,26 +107,31 @@ devtools/client/shared/widgets/*.jsm
 devtools/client/sourceeditor/**
 devtools/client/webaudioeditor/**
 devtools/client/webconsole/**
 !devtools/client/webconsole/panel.js
 !devtools/client/webconsole/jsterm.js
 !devtools/client/webconsole/console-commands.js
 devtools/client/webide/**
 !devtools/client/webide/components/webideCli.js
-devtools/server/**
+devtools/server/*.js
+devtools/server/*.jsm
 !devtools/server/child.js
 !devtools/server/css-logic.js
 !devtools/server/main.js
+devtools/server/actors/**
 !devtools/server/actors/inspector.js
 !devtools/server/actors/highlighters/eye-dropper.js
 !devtools/server/actors/webbrowser.js
+!devtools/server/actors/webextension.js
 !devtools/server/actors/styles.js
 !devtools/server/actors/string.js
 !devtools/server/actors/csscoverage.js
+devtools/server/performance/**
+devtools/server/tests/**
 devtools/shared/*.js
 !devtools/shared/css-lexer.js
 !devtools/shared/defer.js
 !devtools/shared/event-emitter.js
 !devtools/shared/task.js
 devtools/shared/*.jsm
 !devtools/shared/Loader.jsm
 devtools/shared/apps/**
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1030,16 +1030,17 @@ pref("services.sync.prefs.sync.accessibi
 pref("services.sync.prefs.sync.accessibility.typeaheadfind", true);
 pref("services.sync.prefs.sync.accessibility.typeaheadfind.linksonly", true);
 pref("services.sync.prefs.sync.addons.ignoreUserEnabledChanges", true);
 // The addons prefs related to repository verification are intentionally
 // not synced for security reasons. If a system is compromised, a user
 // could weaken the pref locally, install an add-on from an untrusted
 // source, and this would propagate automatically to other,
 // uncompromised Sync-connected devices.
+pref("services.sync.prefs.sync.browser.ctrlTab.previews", true);
 pref("services.sync.prefs.sync.browser.download.useDownloadDir", true);
 pref("services.sync.prefs.sync.browser.formfill.enable", true);
 pref("services.sync.prefs.sync.browser.link.open_newwindow", true);
 pref("services.sync.prefs.sync.browser.newtabpage.enabled", true);
 pref("services.sync.prefs.sync.browser.newtabpage.enhanced", true);
 pref("services.sync.prefs.sync.browser.newtabpage.pinned", true);
 pref("services.sync.prefs.sync.browser.offline-apps.notify", true);
 pref("services.sync.prefs.sync.browser.safebrowsing.phishing.enabled", true);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -7283,60 +7283,49 @@ var gIdentityHandler = {
     let uri = gBrowser.currentURI;
 
     for (let permission of SitePermissions.getPermissionDetailsByURI(uri)) {
       let item = this._createPermissionItem(permission);
       this._permissionList.appendChild(item);
     }
   },
 
-  setPermission: function (aPermission, aState) {
-    if (aState == SitePermissions.getDefault(aPermission))
-      SitePermissions.remove(gBrowser.currentURI, aPermission);
-    else
-      SitePermissions.set(gBrowser.currentURI, aPermission, aState);
-  },
-
   _createPermissionItem: function (aPermission) {
-    let menulist = document.createElement("menulist");
-    let menupopup = document.createElement("menupopup");
-    for (let state of aPermission.availableStates) {
-      let menuitem = document.createElement("menuitem");
-      menuitem.setAttribute("value", state.id);
-      menuitem.setAttribute("label", state.label);
-      menupopup.appendChild(menuitem);
-    }
-    menulist.appendChild(menupopup);
-    menulist.setAttribute("value", aPermission.state);
-    menulist.setAttribute("oncommand", "gIdentityHandler.setPermission('" +
-                                       aPermission.id + "', this.value)");
-    menulist.setAttribute("id", "identity-popup-permission:" + aPermission.id);
-
-    let label = document.createElement("label");
-    label.setAttribute("flex", "1");
-    label.setAttribute("class", "identity-popup-permission-label");
-    label.setAttribute("control", menulist.getAttribute("id"));
-    label.textContent = aPermission.label;
+    let container = document.createElement("hbox");
+    container.setAttribute("class", "identity-popup-permission-item");
+    container.setAttribute("align", "center");
 
     let img = document.createElement("image");
     let isBlocked = (aPermission.state == SitePermissions.BLOCK) ? " blocked" : "";
     img.setAttribute("class",
       "identity-popup-permission-icon " + aPermission.id + "-icon" + isBlocked);
 
-    let container = document.createElement("hbox");
-    container.setAttribute("align", "center");
+    let nameLabel = document.createElement("label");
+    nameLabel.setAttribute("flex", "1");
+    nameLabel.setAttribute("class", "identity-popup-permission-label");
+    nameLabel.textContent = SitePermissions.getPermissionLabel(aPermission.id);
+
+    let stateLabel = document.createElement("label");
+    stateLabel.setAttribute("flex", "1");
+    stateLabel.setAttribute("class", "identity-popup-permission-state-label");
+    stateLabel.textContent = SitePermissions.getStateLabel(
+      aPermission.id, aPermission.state);
+
+    let button = document.createElement("button");
+    button.setAttribute("class", "identity-popup-permission-remove-button");
+    button.addEventListener("command", () => {
+      this._permissionList.removeChild(container);
+      this._identityPopupMultiView.setHeightToFit();
+      SitePermissions.remove(gBrowser.currentURI, aPermission.id);
+    });
+
     container.appendChild(img);
-    container.appendChild(label);
-    container.appendChild(menulist);
-
-    // The menuitem text can be long and we don't want the dropdown
-    // to expand to the width of unselected labels.
-    // Need to set this attribute after it's appended, otherwise it gets
-    // overridden with sizetopopup="pref".
-    menulist.setAttribute("sizetopopup", "none");
+    container.appendChild(nameLabel);
+    container.appendChild(stateLabel);
+    container.appendChild(button);
 
     return container;
   }
 };
 
 function getNotificationBox(aWindow) {
   var foundBrowser = gBrowser.getBrowserForDocument(aWindow.document);
   if (foundBrowser)
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -720,17 +720,17 @@
                    align="center"
                    aria-label="&urlbar.viewSiteInfo.label;"
                    onclick="gIdentityHandler.handleIdentityButtonEvent(event);"
                    onkeypress="gIdentityHandler.handleIdentityButtonEvent(event);"
                    ondragstart="gIdentityHandler.onDragStart(event);">
                 <image id="identity-icon"
                        consumeanchor="identity-box"
                        onclick="PageProxyClickHandler(event);"/>
-                <box id="blocked-permissions-container" align="center">
+                <box id="blocked-permissions-container" align="center" tooltiptext="">
                   <image data-permission-id="geo" class="notification-anchor-icon geo-icon blocked" role="button"
                          aria-label="&urlbar.geolocationNotificationAnchor.label;"/>
                   <image data-permission-id="desktop-notification" class="notification-anchor-icon desktop-notification-icon blocked" role="button"
                          aria-label="&urlbar.webNotsNotificationAnchor3.label;"/>
                   <image data-permission-id="camera" class="notification-anchor-icon camera-icon blocked" role="button"
                          aria-label="&urlbar.webRTCShareDevicesNotificationAnchor.label;"/>
                   <image data-permission-id="indexedDB" class="notification-anchor-icon indexedDB-icon blocked" role="button"
                          aria-label="&urlbar.indexedDBNotificationAnchor.label;"/>
--- a/browser/base/content/sanitize.js
+++ b/browser/base/content/sanitize.js
@@ -229,17 +229,16 @@ Sanitizer.prototype = {
       })
     },
 
     cookies: {
       clear: Task.async(function* (range) {
         let seenException;
         let yieldCounter = 0;
         let refObj = {};
-        TelemetryStopwatch.start("FX_SANITIZE_COOKIES", refObj);
 
         // Clear cookies.
         TelemetryStopwatch.start("FX_SANITIZE_COOKIES_2", refObj);
         try {
           let cookieMgr = Components.classes["@mozilla.org/cookiemanager;1"]
                                     .getService(Ci.nsICookieManager);
           if (range) {
             // Iterate through the cookies and delete any created after our cutoff.
@@ -282,17 +281,16 @@ Sanitizer.prototype = {
         // As evidenced in bug 1253204, clearing plugin data can sometimes be
         // very, very long, for mysterious reasons. Unfortunately, this is not
         // something actionable by Mozilla, so crashing here serves no purpose.
         //
         // For this reason, instead of waiting for sanitization to always
         // complete, we introduce a soft timeout. Once this timeout has
         // elapsed, we proceed with the shutdown of Firefox.
         let promiseClearPluginCookies;
-        TelemetryStopwatch.start("FX_SANITIZE_PLUGINS", refObj);
         try {
           // We don't want to wait for this operation to complete...
           promiseClearPluginCookies = this.promiseClearPluginCookies(range);
 
           //... at least, not for more than 10 seconds.
           yield Promise.race([
             promiseClearPluginCookies,
             new Promise(resolve => setTimeout(resolve, 10000 /* 10 seconds */))
@@ -301,20 +299,16 @@ Sanitizer.prototype = {
           seenException = ex;
         }
 
         // Detach waiting for plugin cookies to be cleared.
         promiseClearPluginCookies.catch(() => {
           // If this exception is raised before the soft timeout, it
           // will appear in `seenException`. Otherwise, it's too late
           // to do anything about it.
-        }).then(() => {
-          // Finally, update statistics.
-          TelemetryStopwatch.finish("FX_SANITIZE_PLUGINS", refObj);
-          TelemetryStopwatch.finish("FX_SANITIZE_COOKIES", refObj);
         });
 
         if (seenException) {
           throw seenException;
         }
       }),
 
       promiseClearPluginCookies: Task.async(function* (range) {
@@ -356,25 +350,19 @@ Sanitizer.prototype = {
             }
           }
         }
       })
     },
 
     offlineApps: {
       clear: Task.async(function* (range) {
-        let refObj = {};
-        TelemetryStopwatch.start("FX_SANITIZE_OFFLINEAPPS", refObj);
-        try {
-          Components.utils.import("resource:///modules/offlineAppCache.jsm");
-          // This doesn't wait for the cleanup to be complete.
-          OfflineAppCacheHelper.clear();
-        } finally {
-          TelemetryStopwatch.finish("FX_SANITIZE_OFFLINEAPPS", refObj);
-        }
+        Components.utils.import("resource:///modules/offlineAppCache.jsm");
+        // This doesn't wait for the cleanup to be complete.
+        OfflineAppCacheHelper.clear();
       })
     },
 
     history: {
       clear: Task.async(function* (range) {
         let seenException;
         let refObj = {};
         TelemetryStopwatch.start("FX_SANITIZE_HISTORY", refObj);
--- a/browser/base/content/test/general/browser_misused_characters_in_strings.js
+++ b/browser/base/content/test/general/browser_misused_characters_in_strings.js
@@ -95,20 +95,16 @@ let gWhitelist = [{
   }, {
     file: "netError.dtd",
     key: "inadequateSecurityError.longDesc",
     type: "single-quote"
   }, {
     file: "netErrorApp.dtd",
     key: "securityOverride.warningContent",
     type: "single-quote"
-  }, {
-    file: "sync.properties",
-    key: "client.name2",
-    type: "apostrophe"
   }
 ];
 
 var moduleLocation = gTestPath.replace(/\/[^\/]*$/i, "/parsingTestHelpers.jsm");
 var {generateURIsFromDirTree} = Cu.import(moduleLocation, {});
 
 /**
  * Check if an error should be ignored due to matching one of the whitelist
--- a/browser/base/content/test/general/browser_permissions.js
+++ b/browser/base/content/test/general/browser_permissions.js
@@ -2,17 +2,16 @@
  * Test the Permissions section in the Control Center.
  */
 
 var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 const PERMISSIONS_PAGE = "http://example.com/browser/browser/base/content/test/general/permissions.html";
 var {SitePermissions} = Cu.import("resource:///modules/SitePermissions.jsm", {});
 
 registerCleanupFunction(function() {
-  SitePermissions.remove(gBrowser.currentURI, "install");
   SitePermissions.remove(gBrowser.currentURI, "cookie");
   SitePermissions.remove(gBrowser.currentURI, "geo");
   SitePermissions.remove(gBrowser.currentURI, "camera");
   SitePermissions.remove(gBrowser.currentURI, "microphone");
 
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
@@ -25,88 +24,114 @@ add_task(function* testMainViewVisible()
 
   let permissionsList = document.getElementById("identity-popup-permission-list");
   let emptyLabel = permissionsList.nextSibling;
 
   gIdentityHandler._identityBox.click();
   ok(!is_hidden(emptyLabel), "List of permissions is empty");
   gIdentityHandler._identityPopup.hidden = true;
 
-  gIdentityHandler.setPermission("install", SitePermissions.ALLOW);
+  SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.ALLOW);
 
   gIdentityHandler._identityBox.click();
   ok(is_hidden(emptyLabel), "List of permissions is not empty");
 
-  let labelText = SitePermissions.getPermissionLabel("install");
+  let labelText = SitePermissions.getPermissionLabel("camera");
   let labels = permissionsList.querySelectorAll(".identity-popup-permission-label");
   is(labels.length, 1, "One permission visible in main view");
   is(labels[0].textContent, labelText, "Correct value");
 
-  let menulists = permissionsList.querySelectorAll("menulist");
-  is(menulists.length, 1, "One permission visible in main view");
-  is(menulists[0].id, "identity-popup-permission:install", "Install permission visible");
-  is(menulists[0].value, "1", "Correct value on install menulist");
-  gIdentityHandler._identityPopup.hidden = true;
+  let img = permissionsList.querySelector("image.identity-popup-permission-icon");
+  ok(img, "There is an image for the permissions");
+  ok(img.classList.contains("camera-icon"), "proper class is in image class");
 
-  let img = menulists[0].parentNode.querySelector("image");
-  ok(img, "There is an image for the permissions");
-  ok(img.classList.contains("install-icon"), "proper class is in image class");
-
-  gIdentityHandler.setPermission("install", SitePermissions.getDefault("install"));
+  SitePermissions.remove(gBrowser.currentURI, "camera");
 
   gIdentityHandler._identityBox.click();
   ok(!is_hidden(emptyLabel), "List of permissions is empty");
   gIdentityHandler._identityPopup.hidden = true;
 });
 
 add_task(function* testIdentityIcon() {
   let {gIdentityHandler} = gBrowser.ownerGlobal;
   let tab = gBrowser.selectedTab = gBrowser.addTab();
   yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
 
-  gIdentityHandler.setPermission("geo", SitePermissions.ALLOW);
+  SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.ALLOW);
 
   ok(gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
     "identity-box signals granted permissions");
 
-  gIdentityHandler.setPermission("geo", SitePermissions.getDefault("geo"));
+  SitePermissions.remove(gBrowser.currentURI, "geo");
+
+  ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
+    "identity-box doesn't signal granted permissions");
+
+  SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.BLOCK);
 
   ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
     "identity-box doesn't signal granted permissions");
 
-  gIdentityHandler.setPermission("camera", SitePermissions.BLOCK);
-
-  ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
-    "identity-box doesn't signal granted permissions");
-
-  gIdentityHandler.setPermission("cookie", SitePermissions.SESSION);
+  SitePermissions.set(gBrowser.currentURI, "cookie", SitePermissions.SESSION);
 
   ok(gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
     "identity-box signals granted permissions");
+
+  SitePermissions.remove(gBrowser.currentURI, "geo");
+  SitePermissions.remove(gBrowser.currentURI, "camera");
+  SitePermissions.remove(gBrowser.currentURI, "cookie");
+});
+
+add_task(function* testCancelPermission() {
+  let {gIdentityHandler} = gBrowser.ownerGlobal;
+  let tab = gBrowser.selectedTab = gBrowser.addTab();
+  yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
+
+  let permissionsList = document.getElementById("identity-popup-permission-list");
+  let emptyLabel = permissionsList.nextSibling;
+
+  SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.ALLOW);
+  SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.BLOCK);
+
+  gIdentityHandler._identityBox.click();
+
+  ok(is_hidden(emptyLabel), "List of permissions is not empty");
+
+  let cancelButtons = permissionsList
+    .querySelectorAll(".identity-popup-permission-remove-button");
+
+  cancelButtons[0].click();
+  let labels = permissionsList.querySelectorAll(".identity-popup-permission-label");
+  is(labels.length, 1, "One permission should be removed");
+  cancelButtons[1].click();
+  labels = permissionsList.querySelectorAll(".identity-popup-permission-label");
+  is(labels.length, 0, "One permission should be removed");
+
+  gIdentityHandler._identityPopup.hidden = true;
 });
 
 add_task(function* testPermissionIcons() {
   let {gIdentityHandler} = gBrowser.ownerGlobal;
   let tab = gBrowser.selectedTab = gBrowser.addTab();
   yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
 
-  gIdentityHandler.setPermission("camera", SitePermissions.ALLOW);
-  gIdentityHandler.setPermission("geo", SitePermissions.BLOCK);
-  gIdentityHandler.setPermission("microphone", SitePermissions.SESSION);
+  SitePermissions.set(gBrowser.currentURI, "camera", SitePermissions.ALLOW);
+  SitePermissions.set(gBrowser.currentURI, "geo", SitePermissions.BLOCK);
+  SitePermissions.set(gBrowser.currentURI, "microphone", SitePermissions.SESSION);
 
   let geoIcon = gIdentityHandler._identityBox.querySelector("[data-permission-id='geo']");
   ok(geoIcon.hasAttribute("showing"), "blocked permission icon is shown");
   ok(geoIcon.classList.contains("blocked"),
     "blocked permission icon is shown as blocked");
 
   let cameraIcon = gIdentityHandler._identityBox.querySelector("[data-permission-id='camera']");
   ok(!cameraIcon.hasAttribute("showing"),
     "allowed permission icon is not shown");
 
   let microphoneIcon  = gIdentityHandler._identityBox.querySelector("[data-permission-id='microphone']");
   ok(!microphoneIcon.hasAttribute("showing"),
     "allowed permission icon is not shown");
 
-  gIdentityHandler.setPermission("geo", SitePermissions.getDefault("geo"));
+  SitePermissions.remove(gBrowser.currentURI, "geo");
 
   ok(!geoIcon.hasAttribute("showing"),
     "blocked permission icon is not shown after reset");
 });
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -500,30 +500,26 @@ const CustomizableWidgets = [
       return item;
     },
   }, {
     id: "privatebrowsing-button",
     shortcutId: "key_privatebrowsing",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(e) {
       let win = e.target.ownerGlobal;
-      if (typeof win.OpenBrowserWindow == "function") {
-        win.OpenBrowserWindow({private: true});
-      }
+      win.OpenBrowserWindow({private: true});
     }
   }, {
     id: "save-page-button",
     shortcutId: "key_savePage",
     tooltiptext: "save-page-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target.ownerGlobal;
-      if (typeof win.saveBrowser == "function") {
-        win.saveBrowser(win.gBrowser.selectedBrowser);
-      }
+      win.saveBrowser(win.gBrowser.selectedBrowser);
     }
   }, {
     id: "find-button",
     shortcutId: "key_find",
     tooltiptext: "find-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target.ownerGlobal;
@@ -533,19 +529,17 @@ const CustomizableWidgets = [
     }
   }, {
     id: "open-file-button",
     shortcutId: "openFileKb",
     tooltiptext: "open-file-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target.ownerGlobal;
-      if (typeof win.BrowserOpenFileWindow == "function") {
-        win.BrowserOpenFileWindow();
-      }
+      win.BrowserOpenFileWindow();
     }
   }, {
     id: "sidebar-button",
     type: "view",
     viewId: "PanelUI-sidebar",
     tooltiptext: "sidebar-button.tooltiptext2",
     onViewShowing: function(aEvent) {
       // Populate the subview with whatever menuitems are in the
@@ -610,19 +604,17 @@ const CustomizableWidgets = [
     }
   }, {
     id: "add-ons-button",
     shortcutId: "key_openAddons",
     tooltiptext: "add-ons-button.tooltiptext3",
     defaultArea: CustomizableUI.AREA_PANEL,
     onCommand: function(aEvent) {
       let win = aEvent.target.ownerGlobal;
-      if (typeof win.BrowserOpenAddonsMgr == "function") {
-        win.BrowserOpenAddonsMgr();
-      }
+      win.BrowserOpenAddonsMgr();
     }
   }, {
     id: "zoom-controls",
     type: "custom",
     tooltiptext: "zoom-controls.tooltiptext2",
     defaultArea: CustomizableUI.AREA_PANEL,
     onBuild: function(aDocument) {
       const kPanelId = "PanelUI-popup";
@@ -1181,19 +1173,17 @@ const CustomizableWidgets = [
     ]),
   }];
 
 let preferencesButton = {
   id: "preferences-button",
   defaultArea: CustomizableUI.AREA_PANEL,
   onCommand: function(aEvent) {
     let win = aEvent.target.ownerGlobal;
-    if (typeof win.openPreferences == "function") {
-      win.openPreferences();
-    }
+    win.openPreferences();
   }
 };
 if (AppConstants.platform == "win") {
   preferencesButton.label = "preferences-button.labelWin";
   preferencesButton.tooltiptext = "preferences-button.tooltipWin2";
 } else if (AppConstants.platform == "macosx") {
   preferencesButton.tooltiptext = "preferences-button.tooltiptext.withshortcut";
   preferencesButton.shortcutId = "key_preferencesCmdMac";
@@ -1272,15 +1262,13 @@ if (AppConstants.E10S_TESTING_ONLY) {
       id: "e10s-button",
       defaultArea: CustomizableUI.AREA_PANEL,
       onBuild: function(aDocument) {
           node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
           node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
       },
       onCommand: function(aEvent) {
         let win = aEvent.view;
-        if (win && typeof win.OpenBrowserWindow == "function") {
-          win.OpenBrowserWindow({remote: false});
-        }
+        win.OpenBrowserWindow({remote: false});
       },
     });
   }
 }
--- a/browser/components/customizableui/content/panelUI.xml
+++ b/browser/components/customizableui/content/panelUI.xml
@@ -389,16 +389,37 @@
           }
 
           if (this._shouldSetHeight()) {
             this._adjustContainerHeight();
           }
         ]]></body>
       </method>
 
+      <!-- Call this when the height of one of your views (the main view or a
+           subview) changes and you want the heights of the multiview and panel
+           to be the same as the view's height. -->
+      <method name="setHeightToFit">
+        <body><![CDATA[
+          // Set the max-height to zero, wait until the height is actually
+          // updated, and then remove it.  If it's not removed, weird things can
+          // happen, like widgets in the panel won't respond to clicks even
+          // though they're visible.
+          let count = 5;
+          let height = getComputedStyle(this).height;
+          this.style.maxHeight = "0";
+          let interval = setInterval(() => {
+            if (height != getComputedStyle(this).height || --count == 0) {
+              clearInterval(interval);
+              this.style.removeProperty("max-height");
+            }
+          }, 0);
+        ]]></body>
+      </method>
+
       <method name="_heightOfSubview">
         <parameter name="aSubview"/>
         <parameter name="aContainerToCheck"/>
         <body><![CDATA[
           function getFullHeight(element) {
             //XXXgijs: unfortunately, scrollHeight rounds values, and there's no alternative
             // that works with overflow: auto elements. Fortunately for us,
             // we have exactly 1 (potentially) scrolling element in here (the subview body),
--- a/browser/components/downloads/content/downloads.css
+++ b/browser/components/downloads/content/downloads.css
@@ -155,23 +155,16 @@ richlistitem.download button {
 
 #downloadsPanel[hasdownloads] #emptyDownloads,
 #downloadsPanel:not([hasdownloads]) #downloadsListBox {
   display: none;
 }
 
 /*** Downloads panel multiview (main view and blocked-downloads subview) ***/
 
-#downloadsPanel,
-#downloadsPanel .panel-viewstack[viewtype=main]:not([transitioning]) #downloadsPanel-mainView {
-  /* Tiny hack to ensure the panel shrinks back to its original
-     size after closing a subview that is bigger than the main view. */
-  max-height: 0;
-}
-
 /* Hide all the usual buttons. */
 #downloadsPanel-mainView .download-state[state="8"] .downloadCancel,
 #downloadsPanel-mainView .download-state[state="8"] .downloadConfirmBlock,
 #downloadsPanel-mainView .download-state[state="8"] .downloadChooseUnblock,
 #downloadsPanel-mainView .download-state[state="8"] .downloadChooseOpen,
 #downloadsPanel-mainView .download-state[state="8"] .downloadRetry,
 #downloadsPanel-mainView .download-state[state="8"] .downloadShow {
   display: none;
--- a/browser/components/downloads/content/downloads.js
+++ b/browser/components/downloads/content/downloads.js
@@ -704,16 +704,18 @@ const DownloadsView = {
     if (count > 0) {
       DownloadsCommon.log("Setting the panel's hasdownloads attribute to true.");
       DownloadsPanel.panel.setAttribute("hasdownloads", "true");
     } else {
       DownloadsCommon.log("Removing the panel's hasdownloads attribute.");
       DownloadsPanel.panel.removeAttribute("hasdownloads");
     }
 
+    DownloadsBlockedSubview.view.setHeightToFit();
+
     // If we've got some hidden downloads, we should activate the
     // DownloadsSummary. The DownloadsSummary will determine whether or not
     // it's appropriate to actually display the summary.
     DownloadsSummary.active = hiddenCount > 0;
   },
 
   /**
    * Element corresponding to the list of downloads.
@@ -1189,17 +1191,27 @@ const DownloadsViewController = {
     // Firstly, determine if this is a command that we can handle.
     if (!DownloadsViewUI.isCommandName(aCommand)) {
       return false;
     }
     if (!(aCommand in this) &&
         !(aCommand in DownloadsViewItem.prototype)) {
       return false;
     }
-    // Secondly, determine if focus is on a control in the downloads list.
+    // The currently supported commands depend on whether the blocked subview is
+    // showing.  If it is, then take the following path.
+    if (DownloadsBlockedSubview.view.showingSubView) {
+      let blockedSubviewCmds = [
+        "downloadsCmd_chooseOpen",
+        "cmd_delete",
+      ];
+      return blockedSubviewCmds.indexOf(aCommand) >= 0;
+    }
+    // If the blocked subview is not showing, then determine if focus is on a
+    // control in the downloads list.
     let element = document.commandDispatcher.focusedElement;
     while (element && element != DownloadsView.richListBox) {
       element = element.parentNode;
     }
     // We should handle the command only if the downloads list is among the
     // ancestors of the focused element.
     return !!element;
   },
@@ -1599,16 +1611,19 @@ const DownloadsBlockedSubview = {
     }
   },
 
   /**
    * Slides out the blocked subview and shows the main view.
    */
   hide() {
     this.view.showMainView();
+    // The point of this is to focus the proper element in the panel now that
+    // the main view is showing again.  showPanel handles that.
+    DownloadsPanel.showPanel();
   },
 
   /**
    * Deletes the download and hides the entire panel.
    */
   confirmBlock() {
     goDoCommand("cmd_delete");
     DownloadsPanel.hidePanel();
--- a/browser/components/downloads/test/browser/browser_downloads_panel_block.js
+++ b/browser/components/downloads/test/browser/browser_downloads_panel_block.js
@@ -34,31 +34,33 @@ add_task(function* mainTest() {
 
     // Show the subview again.
     EventUtils.sendMouseEvent({ type: "click" }, item);
     yield promiseSubviewShown(true);
 
     // Click the Open button.  The alert blocked-download dialog should be
     // shown.
     let dialogPromise = promiseAlertDialogOpen("cancel");
-    DownloadsBlockedSubview.elements.openButton.click();
+    EventUtils.synthesizeMouse(DownloadsBlockedSubview.elements.openButton,
+                               10, 10, {}, window);
     yield dialogPromise;
 
     window.focus();
     yield SimpleTest.promiseFocus(window);
 
     // Reopen the panel and show the subview again.
     yield openPanel();
 
     EventUtils.sendMouseEvent({ type: "click" }, item);
     yield promiseSubviewShown(true);
 
     // Click the Remove button.  The panel should close and the item should be
     // removed from it.
-    DownloadsBlockedSubview.elements.deleteButton.click();
+    EventUtils.synthesizeMouse(DownloadsBlockedSubview.elements.deleteButton,
+                               10, 10, {}, window);
     yield promisePanelHidden();
     yield openPanel();
 
     Assert.ok(!item.parentNode);
     DownloadsPanel.hidePanel();
     yield promisePanelHidden();
   }
 
@@ -145,21 +147,21 @@ function makeDownload(verdict) {
       becauseBlocked: true,
       becauseBlockedByReputationCheck: true,
       reputationCheckVerdict:  verdict,
     },
   };
 }
 
 function promiseSubviewShown(shown) {
+  // More terribleness, but I'm tired of fighting intermittent timeouts on try.
+  // Just poll for the subview and wait a second before resolving the promise.
   return new Promise(resolve => {
-    if (shown == DownloadsBlockedSubview.view.showingSubView) {
-      resolve();
-      return;
-    }
-    let event = shown ? "ViewShowing" : "ViewHiding";
-    let subview = DownloadsBlockedSubview.subview;
-    subview.addEventListener(event, function showing() {
-      subview.removeEventListener(event, showing);
-      resolve();
-    });
+    let interval = setInterval(() => {
+      if (shown == DownloadsBlockedSubview.view.showingSubView &&
+          !DownloadsBlockedSubview.view._transitioning) {
+        clearInterval(interval);
+        setTimeout(resolve, 1000);
+        return;
+      }
+    }, 0);
   });
 }
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -70,16 +70,21 @@ class BasePopup {
       Services.scriptSecurityManager.DISALLOW_SCRIPT);
 
     this.extension = extension;
     this.popupURI = popupURI;
     this.viewNode = viewNode;
     this.browserStyle = browserStyle;
     this.window = viewNode.ownerGlobal;
 
+    this.panel = this.viewNode;
+    while (this.panel.localName != "panel") {
+      this.panel = this.panel.parentNode;
+    }
+
     this.contentReady = new Promise(resolve => {
       this._resolveContentReady = resolve;
     });
 
     this.viewNode.addEventListener(this.DESTROY_EVENT, this);
 
     this.browser = null;
     this.browserReady = this.createBrowser(viewNode, popupURI);
@@ -88,29 +93,34 @@ class BasePopup {
   destroy() {
     this.browserReady.then(() => {
       this.browser.removeEventListener("DOMWindowCreated", this, true);
       this.browser.removeEventListener("load", this, true);
       this.browser.removeEventListener("DOMTitleChanged", this, true);
       this.browser.removeEventListener("DOMWindowClose", this, true);
       this.browser.removeEventListener("MozScrolledAreaChanged", this, true);
       this.viewNode.removeEventListener(this.DESTROY_EVENT, this);
+      this.viewNode.style.maxHeight = "";
       this.browser.remove();
 
       this.browser = null;
       this.viewNode = null;
     });
   }
 
   // Returns the name of the event fired on `viewNode` when the popup is being
   // destroyed. This must be implemented by every subclass.
   get DESTROY_EVENT() {
     throw new Error("Not implemented");
   }
 
+  get fixedWidth() {
+    return false;
+  }
+
   handleEvent(event) {
     switch (event.type) {
       case this.DESTROY_EVENT:
         this.destroy();
         break;
 
       case "DOMWindowCreated":
         if (this.browserStyle && event.target === this.browser.contentDocument) {
@@ -161,25 +171,26 @@ class BasePopup {
 
   createBrowser(viewNode, popupURI) {
     let document = viewNode.ownerDocument;
     this.browser = document.createElementNS(XUL_NS, "browser");
     this.browser.setAttribute("type", "content");
     this.browser.setAttribute("disableglobalhistory", "true");
     this.browser.setAttribute("webextension-view-type", "popup");
 
+    // We only need flex sizing for the sake of the slide-in sub-views of the
+    // main menu panel, so that the browser occupies the full width of the view,
+    // and also takes up any extra height that's available to it.
+    this.browser.setAttribute("flex", "1");
+
     // Note: When using noautohide panels, the popup manager will add width and
     // height attributes to the panel, breaking our resize code, if the browser
     // starts out smaller than 30px by 10px. This isn't an issue now, but it
     // will be if and when we popup debugging.
 
-    // This overrides the content's preferred size when displayed in a
-    // fixed-size, slide-in panel.
-    this.browser.setAttribute("flex", "1");
-
     viewNode.appendChild(this.browser);
 
     return new Promise(resolve => {
       // The first load event is for about:blank.
       // We can't finish setting up the browser until the binding has fully
       // initialized. Waiting for the first load event guarantees that it has.
       let loadListener = event => {
         this.browser.removeEventListener("load", loadListener, true);
@@ -212,38 +223,86 @@ class BasePopup {
 
   _resizeBrowser() {
     this.resizeTimeout = null;
 
     if (!this.browser) {
       return;
     }
 
-    let width, height;
-    try {
-      let w = {}, h = {};
-      this.browser.docShell.contentViewer.getContentSize(w, h);
+    if (this.fixedWidth) {
+      // If we're in a fixed-width area (namely a slide-in subview of the main
+      // menu panel), we need to calculate the view height based on the
+      // preferred height of the content document's root scrollable element at the
+      // current width, rather than the complete preferred dimensions of the
+      // content window.
+
+      let doc = this.browser.contentDocument;
+      if (!doc || !doc.documentElement) {
+        return;
+      }
 
-      width = w.value / this.window.devicePixelRatio;
-      height = h.value / this.window.devicePixelRatio;
+      let root = doc.documentElement;
+      let body = doc.body;
+      if (!body || doc.compatMode == "BackCompat") {
+        // In quirks mode, the root element is used as the scroll frame, and the
+        // body lies about its scroll geometry, and returns the values for the
+        // root instead.
+        body = root;
+      }
+
+      // Compensate for any offsets (margin, padding, ...) between the scroll
+      // area of the body and the outer height of the document.
+      let getHeight = elem => elem.getBoundingClientRect(elem).height;
+      let bodyPadding = getHeight(root) - getHeight(body);
+
+      let height = Math.ceil(body.scrollHeight + bodyPadding);
+
+      // Figure out how much extra space we have on the side of the panel
+      // opposite the arrow.
+      let side = this.panel.getAttribute("side") == "top" ? "bottom" : "top";
+      let maxHeight = this.viewHeight + this.extraHeight[side];
 
-      // The width calculation is imperfect, and is often a fraction of a pixel
-      // too narrow, even after taking the ceiling, which causes lines of text
-      // to wrap.
-      width += 1;
-    } catch (e) {
-      // getContentSize can throw
-      [width, height] = [400, 400];
+      height = Math.min(height, maxHeight);
+      this.browser.style.height = `${height}px`;
+
+      // Set a maximum height on the <panelview> element to our preferred
+      // maximum height, so that the PanelUI resizing code can make an accurate
+      // calculation. If we don't do this, the flex sizing logic will prevent us
+      // from ever reporting a preferred size smaller than the height currently
+      // available to us in the panel.
+      height = Math.max(height, this.viewHeight);
+      this.viewNode.style.maxHeight = `${height}px`;
+    } else {
+      let width, height;
+      try {
+        let w = {}, h = {};
+        this.browser.docShell.contentViewer.getContentSize(w, h);
+
+        width = w.value / this.window.devicePixelRatio;
+        height = h.value / this.window.devicePixelRatio;
+
+        // The width calculation is imperfect, and is often a fraction of a pixel
+        // too narrow, even after taking the ceiling, which causes lines of text
+        // to wrap.
+        width += 1;
+      } catch (e) {
+        // getContentSize can throw
+        [width, height] = [400, 400];
+      }
+
+      width = Math.ceil(Math.min(width, 800));
+      height = Math.ceil(Math.min(height, 600));
+
+      this.browser.style.width = `${width}px`;
+      this.browser.style.height = `${height}px`;
     }
 
-    width = Math.ceil(Math.min(width, 800));
-    height = Math.ceil(Math.min(height, 600));
-
-    this.browser.style.width = `${width}px`;
-    this.browser.style.height = `${height}px`;
+    let event = new this.window.CustomEvent("WebExtPopupResized");
+    this.browser.dispatchEvent(event);
 
     this._resolveContentReady();
   }
 }
 
 global.PanelPopup = class PanelPopup extends BasePopup {
   constructor(extension, imageNode, popupURL, browserStyle) {
     let document = imageNode.ownerDocument;
@@ -278,32 +337,59 @@ global.PanelPopup = class PanelPopup ext
       if (this.viewNode) {
         this.viewNode.hidePopup();
       }
     });
   }
 };
 
 global.ViewPopup = class ViewPopup extends BasePopup {
+  constructor(...args) {
+    super(...args);
+
+    // Store the initial height of the view, so that we never resize menu panel
+    // sub-views smaller than the initial height of the menu.
+    this.viewHeight = this.viewNode.boxObject.height;
+
+    // Calculate the extra height available on the screen above and below the
+    // menu panel. Use that to calculate the how much the sub-view may grow.
+    let popupRect = this.panel.getBoundingClientRect();
+
+    let win = this.window;
+    let popupBottom = win.mozInnerScreenY + popupRect.bottom;
+    let popupTop = win.mozInnerScreenY + popupRect.top;
+
+    let screenBottom = win.screen.availTop + win.screen.availHeight;
+    this.extraHeight = {
+      bottom: Math.max(0, screenBottom - popupBottom),
+      top:  Math.max(0, popupTop - win.screen.availTop),
+    };
+  }
+
   get DESTROY_EVENT() {
     return "ViewHiding";
   }
 
+  get fixedWidth() {
+    return !this.viewNode.classList.contains("cui-widget-panelview");
+  }
+
   closePopup() {
     CustomizableUI.hidePanelForNode(this.viewNode);
   }
 };
 
 // Manages tab-specific context data, and dispatching tab select events
 // across all windows.
 global.TabContext = function TabContext(getDefaults, extension) {
   this.extension = extension;
   this.getDefaults = getDefaults;
 
   this.tabData = new WeakMap();
+  this.lastLocation = new WeakMap();
 
   AllWindowEvents.addListener("progress", this);
   AllWindowEvents.addListener("TabSelect", this);
 
   EventEmitter.decorate(this);
 };
 
 TabContext.prototype = {
@@ -322,22 +408,34 @@ TabContext.prototype = {
   handleEvent(event) {
     if (event.type == "TabSelect") {
       let tab = event.target;
       this.emit("tab-select", tab);
       this.emit("location-change", tab);
     }
   },
 
+  onStateChange(browser, webProgress, request, stateFlags, statusCode) {
+    let flags = Ci.nsIWebProgressListener;
+
+    if (!(~stateFlags & (flags.STATE_IS_WINDOW | flags.STATE_START) ||
+          this.lastLocation.has(browser))) {
+      this.lastLocation.set(browser, request.URI);
+    }
+  },
+
   onLocationChange(browser, webProgress, request, locationURI, flags) {
     let gBrowser = browser.ownerGlobal.gBrowser;
-    if (browser === gBrowser.selectedBrowser) {
+    let lastLocation = this.lastLocation.get(browser);
+    if (browser === gBrowser.selectedBrowser &&
+        !(lastLocation && lastLocation.equalsExceptRef(browser.currentURI))) {
       let tab = gBrowser.getTabForBrowser(browser);
       this.emit("location-change", tab, true);
     }
+    this.lastLocation.set(browser, browser.currentURI);
   },
 
   shutdown() {
     AllWindowEvents.removeListener("progress", this);
     AllWindowEvents.removeListener("TabSelect", this);
   },
 };
 
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_popup_resize.js
@@ -1,37 +1,57 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-add_task(function* testPageActionPopupResize() {
+function* openPanel(extension, win = window) {
+  clickBrowserAction(extension, win);
+
+  let {target} = yield BrowserTestUtils.waitForEvent(win.document, "load", true, (event) => {
+    return event.target.location && event.target.location.href.endsWith("popup.html");
+  });
+
+  return target.defaultView
+               .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDocShell)
+               .chromeEventHandler;
+}
+
+function* awaitResize(browser) {
+  // Debouncing code makes this a bit racy.
+  // Try to skip the first, early resize, and catch the resize event we're
+  // looking for, but don't wait longer than a few seconds.
+
+  return Promise.race([
+    BrowserTestUtils.waitForEvent(browser, "WebExtPopupResized")
+      .then(() => BrowserTestUtils.waitForEvent(browser, "WebExtPopupResized")),
+    new Promise(resolve => setTimeout(resolve, 5000)),
+  ]);
+}
+
+add_task(function* testBrowserActionPopupResize() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "browser_action": {
         "default_popup": "popup.html",
         "browser_style": true,
       },
     },
 
     files: {
-      "popup.html": "<html><head><meta charset=\"utf-8\"></head></html>",
+      "popup.html": '<html><head><meta charset="utf-8"></head></html>',
     },
   });
 
   yield extension.startup();
 
   clickBrowserAction(extension, window);
 
-  let {target: panelDocument} = yield BrowserTestUtils.waitForEvent(document, "load", true, (event) => {
-    info(`Loaded ${event.target.location}`);
-    return event.target.location && event.target.location.href.endsWith("popup.html");
-  });
-
-  let panelWindow = panelDocument.defaultView;
-  let panelBody = panelDocument.body;
+  let browser = yield openPanel(extension);
+  let panelWindow = browser.contentWindow;
+  let panelBody = panelWindow.document.body;
 
   function checkSize(expected) {
     is(panelWindow.innerHeight, expected, `Panel window should be ${expected}px tall`);
     is(panelBody.clientHeight, panelBody.scrollHeight,
       "Panel body should be tall enough to fit its contents");
 
     // Tolerate if it is 1px too wide, as that may happen with the current resizing method.
     ok(Math.abs(panelWindow.innerWidth - expected) <= 1, `Panel window should be ${expected}px wide`);
@@ -47,15 +67,245 @@ add_task(function* testPageActionPopupRe
   let sizes = [
     200,
     400,
     300,
   ];
 
   for (let size of sizes) {
     setSize(size);
-    yield BrowserTestUtils.waitForEvent(panelWindow, "resize");
+    yield awaitResize(browser);
     checkSize(size);
   }
 
   yield closeBrowserAction(extension);
   yield extension.unload();
 });
+
+function* testPopupSize(standardsMode, browserWin = window, arrowSide = "top") {
+  let docType = standardsMode ? "<!DOCTYPE html>" : "";
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "browser_action": {
+        "default_popup": "popup.html",
+        "browser_style": false,
+      },
+    },
+
+    files: {
+      "popup.html": `${docType}
+        <html>
+          <head>
+            <meta charset="utf-8">
+            <style type="text/css">
+              body > span {
+                display: inline-block;
+                width: 10px;
+                height: 150px;
+                border: 2px solid black;
+              }
+              .big > span {
+                width: 300px;
+                height: 100px;
+              }
+              .bigger > span {
+                width: 150px;
+                height: 150px;
+              }
+              .huge > span {
+                height: ${2 * screen.height}px;
+              }
+            </style>
+          </head>
+          <body>
+            <span></span>
+            <span></span>
+            <span></span>
+            <span></span>
+          </body>
+        </html>`,
+    },
+  });
+
+  yield extension.startup();
+
+  if (arrowSide == "top") {
+    // Test the standalone panel for a toolbar button.
+    let browser = yield openPanel(extension, browserWin);
+    let win = browser.contentWindow;
+    let body = win.document.body;
+
+    let isStandards = win.document.compatMode != "BackCompat";
+    is(isStandards, standardsMode, "Document has the expected compat mode");
+
+    let {innerWidth, innerHeight} = win;
+
+    body.classList.add("bigger");
+    yield awaitResize(browser);
+
+    is(win.innerHeight, innerHeight, "Window height should not change");
+    ok(win.innerWidth > innerWidth, `Window width should increase (${win.innerWidth} > ${innerWidth})`);
+
+
+    body.classList.remove("bigger");
+    yield awaitResize(browser);
+
+    is(win.innerHeight, innerHeight, "Window height should not change");
+
+    // The getContentSize calculation is not always reliable to single-pixel
+    // precision.
+    ok(Math.abs(win.innerWidth - innerWidth) <= 1,
+       `Window width should return to approximately its original value (${win.innerWidth} ~= ${innerWidth})`);
+
+    yield closeBrowserAction(extension, browserWin);
+  }
+
+
+  // Test the PanelUI panel for a menu panel button.
+  let widget = getBrowserActionWidget(extension);
+  CustomizableUI.addWidgetToArea(widget.id, CustomizableUI.AREA_PANEL);
+
+  let browser = yield openPanel(extension, browserWin);
+  let win = browser.contentWindow;
+  let body = win.document.body;
+
+  let {panel} = browserWin.PanelUI;
+  let origPanelRect = panel.getBoundingClientRect();
+
+  // Check that the panel is still positioned as expected.
+  let checkPanelPosition = () => {
+    is(panel.getAttribute("side"), arrowSide, "Panel arrow is positioned as expected");
+
+    let panelRect = panel.getBoundingClientRect();
+    if (arrowSide == "top") {
+      ok(panelRect.top, origPanelRect.top, "Panel has not moved downwards");
+      ok(panelRect.bottom >= origPanelRect.bottom, `Panel has not shrunk from original size (${panelRect.bottom} >= ${origPanelRect.bottom})`);
+
+      let screenBottom = browserWin.screen.availTop + win.screen.availHeight;
+      let panelBottom = browserWin.mozInnerScreenY + panelRect.bottom;
+      ok(panelBottom <= screenBottom, `Bottom of popup should be on-screen. (${panelBottom} <= ${screenBottom})`);
+    } else {
+      ok(panelRect.bottom, origPanelRect.bottom, "Panel has not moved upwards");
+      ok(panelRect.top <= origPanelRect.top, `Panel has not shrunk from original size (${panelRect.top} <= ${origPanelRect.top})`);
+
+      let panelTop = browserWin.mozInnerScreenY + panelRect.top;
+      ok(panelTop >= browserWin.screen.availTop, `Top of popup should be on-screen. (${panelTop} >= ${browserWin.screen.availTop})`);
+    }
+  };
+
+
+  let isStandards = win.document.compatMode != "BackCompat";
+  is(isStandards, standardsMode, "Document has the expected compat mode");
+
+  // Wait long enough to make sure the initial resize debouncing timer has
+  // expired.
+  yield new Promise(resolve => setTimeout(resolve, 100));
+
+  // If the browser's preferred height is smaller than the initial height of the
+  // panel, then it will still take up the full available vertical space. Even
+  // so, we need to check that we've gotten the preferred height calculation
+  // correct, so check that explicitly.
+  let getHeight = () => parseFloat(browser.style.height);
+
+  let {innerWidth, innerHeight} = win;
+  let height = getHeight();
+
+
+  info("Increase body children's width. " +
+       "Expect them to wrap, and the frame to grow vertically rather than widen.");
+  body.className = "big";
+  yield awaitResize(browser);
+
+  ok(getHeight() > height, `Browser height should increase (${getHeight()} > ${height})`);
+
+  is(win.innerWidth, innerWidth, "Window width should not change");
+  ok(win.innerHeight >= innerHeight, `Window height should increase (${win.innerHeight} >= ${innerHeight})`);
+  is(win.scrollMaxY, 0, "Document should not be vertically scrollable");
+
+  checkPanelPosition();
+
+
+  info("Increase body children's width and height. " +
+       "Expect them to wrap, and the frame to grow vertically rather than widen.");
+  body.className = "bigger";
+  yield awaitResize(browser);
+
+  ok(getHeight() > height, `Browser height should increase (${getHeight()} > ${height})`);
+
+  is(win.innerWidth, innerWidth, "Window width should not change");
+  ok(win.innerHeight >= innerHeight, `Window height should increase (${win.innerHeight} >= ${innerHeight})`);
+  is(win.scrollMaxY, 0, "Document should not be vertically scrollable");
+
+  checkPanelPosition();
+
+
+  info("Increase body height beyond the height of the screen. " +
+       "Expect the panel to grow to accommodate, but not larger than the height of the screen.");
+  body.className = "huge";
+  yield awaitResize(browser);
+
+  ok(getHeight() > height, `Browser height should increase (${getHeight()} > ${height})`);
+
+  is(win.innerWidth, innerWidth, "Window width should not change");
+  ok(win.innerHeight > innerHeight, `Window height should increase (${win.innerHeight} > ${innerHeight})`);
+  ok(win.innerHeight < screen.height, `Window height be less than the screen height (${win.innerHeight} < ${screen.height})`);
+  ok(win.scrollMaxY > 0, `Document should be vertically scrollable (${win.scrollMaxY} > 0)`);
+
+  checkPanelPosition();
+
+
+  info("Restore original styling. Expect original dimensions.");
+  body.className = "";
+  yield awaitResize(browser);
+
+  is(getHeight(), height, "Browser height should return to its original value");
+
+  is(win.innerWidth, innerWidth, "Window width should not change");
+  is(win.innerHeight, innerHeight, "Window height should return to its original value");
+  is(win.scrollMaxY, 0, "Document should not be vertically scrollable");
+
+  checkPanelPosition();
+
+  yield closeBrowserAction(extension, browserWin);
+
+  yield extension.unload();
+}
+
+add_task(function* testBrowserActionMenuResizeStandards() {
+  yield testPopupSize(true);
+});
+
+add_task(function* testBrowserActionMenuResizeQuirks() {
+  yield testPopupSize(false);
+});
+
+// Test that we still make reasonable maximum size calculations when the window
+// is close enough to the bottom of the screen that the menu panel opens above,
+// rather than below, its button.
+add_task(function* testBrowserActionMenuResizeBottomArrow() {
+  const WIDTH = 800;
+  const HEIGHT = 300;
+
+  let left = screen.availLeft + screen.availWidth - WIDTH;
+  let top = screen.availTop + screen.availHeight - HEIGHT;
+
+  let win = yield BrowserTestUtils.openNewBrowserWindow();
+
+  win.resizeTo(WIDTH, HEIGHT);
+
+  // Sometimes we run into problems on Linux with resizing being asynchronous
+  // and window managers not allowing us to move the window so that any part of
+  // it is off-screen, so we need to try more than once.
+  for (let i = 0; i < 20; i++) {
+    win.moveTo(left, top);
+
+    if (win.screenX == left && win.screenY == top) {
+      break;
+    }
+
+    yield new Promise(resolve => setTimeout(resolve, 100));
+  }
+
+  yield testPopupSize(true, win, "bottom");
+
+  yield BrowserTestUtils.closeWindow(win);
+});
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -243,16 +243,25 @@ add_task(function* testTabSwitchContext(
             browser.pageAction.setIcon({tabId, path: "2.png"});
             browser.pageAction.setPopup({tabId, popup: "2.html"});
             browser.pageAction.setTitle({tabId, title: "Title 2"});
 
             expect(details[2]);
           });
         },
         expect => {
+          browser.test.log("Change the hash. Expect same properties.");
+
+          promiseTabLoad({id: tabs[1], url: "about:blank?0#ref"}).then(() => {
+            expect(details[2]);
+          });
+
+          browser.tabs.update(tabs[1], {url: "about:blank?0#ref"});
+        },
+        expect => {
           browser.test.log("Clear the title. Expect default title.");
           browser.pageAction.setTitle({tabId: tabs[1], title: ""});
 
           expect(details[3]);
         },
         expect => {
           browser.test.log("Navigate to a new page. Expect icon hidden.");
 
--- a/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_executeScript.js
@@ -81,16 +81,46 @@ add_task(function* testExecuteScript() {
         }).then(result => {
           browser.test.fail("Expected error when returning non-structured-clonable object");
         }, error => {
           browser.test.assertEq("Script returned non-structured-clonable data",
                                 error.message, "Got expected error");
         }),
 
         browser.tabs.executeScript({
+          frameId: Number.MAX_SAFE_INTEGER,
+          code: "42",
+        }).then(result => {
+          browser.test.fail("Expected error when specifying invalid frame ID");
+        }, error => {
+          let details = {
+            frame_id: Number.MAX_SAFE_INTEGER,
+            matchesHost: ["http://mochi.test/", "http://example.com/"],
+          };
+          browser.test.assertEq(`No window matching ${JSON.stringify(details)}`,
+                                error.message, "Got expected error");
+        }),
+
+        browser.tabs.create({url: "http://example.net/", active: false}).then(tab => {
+          return browser.tabs.executeScript(tab.id, {
+            code: "42",
+          }).then(result => {
+            browser.test.fail("Expected error when trying to execute on invalid domain");
+          }, error => {
+            let details = {
+              matchesHost: ["http://mochi.test/", "http://example.com/"],
+            };
+            browser.test.assertEq(`No window matching ${JSON.stringify(details)}`,
+                                  error.message, "Got expected error");
+          }).then(() => {
+            return browser.tabs.remove(tab.id);
+          });
+        }),
+
+        browser.tabs.executeScript({
           code: "Promise.resolve(42)",
         }).then(result => {
           browser.test.assertEq(42, result, "Got expected promise resolution value as result");
         }),
 
         browser.tabs.executeScript({
           code: "location.href;",
           runAt: "document_end",
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -100,16 +100,19 @@
     <preference id="browser.sessionstore.restore_on_demand"
                 name="browser.sessionstore.restore_on_demand"
                 type="bool"/>
 #ifdef XP_WIN
     <preference id="browser.taskbar.previews.enable"
                 name="browser.taskbar.previews.enable"
                 type="bool"/>
 #endif
+    <preference id="browser.ctrlTab.previews"
+                name="browser.ctrlTab.previews"
+                type="bool"/>
 </preferences>
 
 <hbox id="header-general"
       class="header"
       hidden="true"
       data-category="paneGeneral">
   <label class="header-name" flex="1">&paneGeneral.title;</label>
   <button class="help-button"
@@ -263,16 +266,21 @@
     </hbox>
   </radiogroup>
 </groupbox>
 
 <!-- Tab preferences -->
 <groupbox data-category="paneGeneral"
           hidden="true" align="start">
     <caption><label>&tabsGroup.label;</label></caption>
+
+    <checkbox id="ctrlTabRecentlyUsedOrder" label="&ctrlTabRecentlyUsedOrder.label;"
+              accesskey="&ctrlTabRecentlyUsedOrder.accesskey;"
+              preference="browser.ctrlTab.previews"/>
+
     <checkbox id="linkTargeting" label="&newWindowsAsTabs.label;"
               accesskey="&newWindowsAsTabs.accesskey;"
               preference="browser.link.open_newwindow"
               onsyncfrompreference="return gMainPane.readLinkTarget();"
               onsynctopreference="return gMainPane.writeLinkTarget();"/>
 
     <checkbox id="warnCloseMultiple" label="&warnCloseMultipleTabs.label;"
               accesskey="&warnCloseMultipleTabs.accesskey;"
--- a/browser/components/preferences/in-content/search.js
+++ b/browser/components/preferences/in-content/search.js
@@ -101,16 +101,28 @@ var gSearchPane = {
       if (e.name == currentEngine)
         list.selectedItem = item;
     });
   },
 
   handleEvent: function(aEvent) {
     switch (aEvent.type) {
       case "click":
+        if (aEvent.target.id != "engineChildren" && aEvent.target.id != "removeEngineButton") {
+          let engineList = document.getElementById("engineList");
+          // We don't want to toggle off selection while editing keyword
+          // so proceed only when the input field is hidden
+          if (engineList.inputField.hidden) {
+            let selection = engineList.view.selection;
+            if (selection.count > 0) {
+              selection.toggleSelect(selection.currentIndex);
+            }
+            engineList.blur();
+          }
+        }
         if (aEvent.target.id == "addEngines" && aEvent.button == 0) {
           Services.wm.getMostRecentWindow('navigator:browser')
                      .BrowserSearch.loadAddEngines();
         }
         break;
       case "command":
         switch (aEvent.target.id) {
           case "":
--- a/browser/extensions/pdfjs/README.mozilla
+++ b/browser/extensions/pdfjs/README.mozilla
@@ -1,3 +1,3 @@
 This is the pdf.js project output, https://github.com/mozilla/pdf.js
 
-Current extension version is: 1.5.345
+Current extension version is: 1.5.365
--- a/browser/extensions/pdfjs/content/build/pdf.js
+++ b/browser/extensions/pdfjs/content/build/pdf.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf', ['exports
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdf = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.345';
-var pdfjsBuild = '10f9f11';
+var pdfjsVersion = '1.5.365';
+var pdfjsBuild = '19105f0';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -6766,16 +6766,18 @@ var PDFDocumentProxy = (function PDFDocu
 })();
 
 /**
  * Page getTextContent parameters.
  *
  * @typedef {Object} getTextContentParameters
  * @param {boolean} normalizeWhitespace - replaces all occurrences of
  *   whitespace with standard spaces (0x20). The default value is `false`.
+ * @param {boolean} disableCombineTextItems - do not attempt to combine
+ *   same line {@link TextItem}'s. The default value is `false`.
  */
 
 /**
  * Page text content.
  *
  * @typedef {Object} TextContent
  * @property {array} items - array of {@link TextItem}
  * @property {Object} styles - {@link TextStyles} objects, indexed by font
@@ -7057,21 +7059,22 @@ var PDFPageProxy = (function PDFPageProx
     },
 
     /**
      * @param {getTextContentParameters} params - getTextContent parameters.
      * @return {Promise} That is resolved a {@link TextContent}
      * object that represent the page text content.
      */
     getTextContent: function PDFPageProxy_getTextContent(params) {
-      var normalizeWhitespace = (params && params.normalizeWhitespace) || false;
-
       return this.transport.messageHandler.sendWithPromise('GetTextContent', {
         pageIndex: this.pageNumber - 1,
-        normalizeWhitespace: normalizeWhitespace,
+        normalizeWhitespace: (params && params.normalizeWhitespace === true ?
+                              true : /* Default */ false),
+        combineTextItems: (params && params.disableCombineTextItems === true ?
+                           false : /* Default */ true),
       });
     },
 
     /**
      * Destroys page object.
      */
     _destroy: function PDFPageProxy_destroy() {
       this.destroyed = true;
--- a/browser/extensions/pdfjs/content/build/pdf.worker.js
+++ b/browser/extensions/pdfjs/content/build/pdf.worker.js
@@ -23,18 +23,18 @@ define('pdfjs-dist/build/pdf.worker', ['
     factory(exports);
   } else {
 factory((root.pdfjsDistBuildPdfWorker = {}));
   }
 }(this, function (exports) {
   // Use strict in our context only - users might not want it
   'use strict';
 
-var pdfjsVersion = '1.5.345';
-var pdfjsBuild = '10f9f11';
+var pdfjsVersion = '1.5.365';
+var pdfjsBuild = '19105f0';
 
   var pdfjsFilePath =
     typeof document !== 'undefined' && document.currentScript ?
       document.currentScript.src : null;
 
   var pdfjsLibs = {};
 
   (function pdfjsWrapper() {
@@ -36501,66 +36501,88 @@ var PartialEvaluator = (function Partial
         return this.fontCache.get(fontRef);
       }
 
       font = xref.fetchIfRef(fontRef);
       if (!isDict(font)) {
         return errorFont();
       }
 
-      // We are holding font.translated references just for fontRef that are not
-      // dictionaries (Dict). See explanation below.
+      // We are holding `font.translated` references just for `fontRef`s that
+      // are not actually `Ref`s, but rather `Dict`s. See explanation below.
       if (font.translated) {
         return font.translated;
       }
 
       var fontCapability = createPromiseCapability();
 
       var preEvaluatedFont = this.preEvaluateFont(font, xref);
       var descriptor = preEvaluatedFont.descriptor;
-      var fontID = fontRef.num + '_' + fontRef.gen;
+
+      var fontRefIsRef = isRef(fontRef), fontID;
+      if (fontRefIsRef) {
+        fontID = fontRef.toString();
+      }
+
       if (isDict(descriptor)) {
         if (!descriptor.fontAliases) {
           descriptor.fontAliases = Object.create(null);
         }
 
         var fontAliases = descriptor.fontAliases;
         var hash = preEvaluatedFont.hash;
         if (fontAliases[hash]) {
           var aliasFontRef = fontAliases[hash].aliasRef;
-          if (aliasFontRef && this.fontCache.has(aliasFontRef)) {
+          if (fontRefIsRef && aliasFontRef &&
+              this.fontCache.has(aliasFontRef)) {
             this.fontCache.putAlias(fontRef, aliasFontRef);
             return this.fontCache.get(fontRef);
           }
-        }
-
-        if (!fontAliases[hash]) {
+        } else {
           fontAliases[hash] = {
             fontID: Font.getFontID()
           };
         }
 
-        fontAliases[hash].aliasRef = fontRef;
+        if (fontRefIsRef) {
+          fontAliases[hash].aliasRef = fontRef;
+        }
         fontID = fontAliases[hash].fontID;
       }
 
-      // Workaround for bad PDF generators that don't reference fonts
-      // properly, i.e. by not using an object identifier.
-      // Check if the fontRef is a Dict (as opposed to a standard object),
-      // in which case we don't cache the font and instead reference it by
-      // fontName in font.loadedName below.
-      var fontRefIsDict = isDict(fontRef);
-      if (!fontRefIsDict) {
+      // Workaround for bad PDF generators that reference fonts incorrectly,
+      // where `fontRef` is a `Dict` rather than a `Ref` (fixes bug946506.pdf).
+      // In this case we should not put the font into `this.fontCache` (which is
+      // a `RefSetCache`), since it's not meaningful to use a `Dict` as a key.
+      //
+      // However, if we don't cache the font it's not possible to remove it
+      // when `cleanup` is triggered from the API, which causes issues on
+      // subsequent rendering operations (see issue7403.pdf).
+      // A simple workaround would be to just not hold `font.translated`
+      // references in this case, but this would force us to unnecessarily load
+      // the same fonts over and over.
+      //
+      // Instead, we cheat a bit by attempting to use a modified `fontID` as a
+      // key in `this.fontCache`, to allow the font to be cached.
+      // NOTE: This works because `RefSetCache` calls `toString()` on provided
+      //       keys. Also, since `fontRef` is used when getting cached fonts,
+      //       we'll not accidentally match fonts cached with the `fontID`.
+      if (fontRefIsRef) {
         this.fontCache.put(fontRef, fontCapability.promise);
-      }
+      } else {
+        if (!fontID) {
+          fontID = (this.uniquePrefix || 'F_') + (++this.idCounters.obj);
+        }
+        this.fontCache.put('id_' + fontID, fontCapability.promise);
+      }
+      assert(fontID, 'The "fontID" must be defined.');
 
       // Keep track of each font we translated so the caller can
       // load them asynchronously before calling display on a page.
-      font.loadedName = 'g_' + this.pdfManager.docId + '_f' + (fontRefIsDict ?
-        fontName.replace(/\W/g, '') : fontID);
+      font.loadedName = 'g_' + this.pdfManager.docId + '_f' + fontID;
 
       font.translated = fontCapability.promise;
 
       // TODO move promises into translate font
       var translatedPromise;
       try {
         translatedPromise = this.translateFont(preEvaluatedFont, xref);
       } catch (e) {
@@ -36949,17 +36971,18 @@ var PartialEvaluator = (function Partial
         }
         resolve();
       });
     },
 
     getTextContent:
         function PartialEvaluator_getTextContent(stream, task, resources,
                                                  stateManager,
-                                                 normalizeWhitespace) {
+                                                 normalizeWhitespace,
+                                                 combineTextItems) {
 
       stateManager = (stateManager || new StateManager(new TextState()));
 
       var WhitespaceRegexp = /\s/g;
 
       var textContent = {
         items: [],
         styles: Object.create(null)
@@ -37260,17 +37283,18 @@ var PartialEvaluator = (function Partial
               flushTextContentItem();
               textState.leading = args[0];
               break;
             case OPS.moveText:
               // Optimization to treat same line movement as advance
               var isSameTextLine = !textState.font ? false :
                 ((textState.font.vertical ? args[0] : args[1]) === 0);
               advance = args[0] - args[1];
-              if (isSameTextLine && textContentItem.initialized &&
+              if (combineTextItems &&
+                  isSameTextLine && textContentItem.initialized &&
                   advance > 0 &&
                   advance <= textContentItem.fakeMultiSpaceMax) {
                 textState.translateTextLineMatrix(args[0], args[1]);
                 textContentItem.width +=
                   (args[0] - textContentItem.lastAdvanceWidth);
                 textContentItem.height +=
                   (args[1] - textContentItem.lastAdvanceHeight);
                 diff = (args[0] - textContentItem.lastAdvanceWidth) -
@@ -37292,17 +37316,18 @@ var PartialEvaluator = (function Partial
             case OPS.nextLine:
               flushTextContentItem();
               textState.carriageReturn();
               break;
             case OPS.setTextMatrix:
               // Optimization to treat same line movement as advance.
               advance = textState.calcTextLineMatrixAdvance(
                 args[0], args[1], args[2], args[3], args[4], args[5]);
-              if (advance !== null && textContentItem.initialized &&
+              if (combineTextItems &&
+                  advance !== null && textContentItem.initialized &&
                   advance.value > 0 &&
                   advance.value <= textContentItem.fakeMultiSpaceMax) {
                 textState.translateTextLineMatrix(advance.width,
                                                   advance.height);
                 textContentItem.width +=
                   (advance.width - textContentItem.lastAdvanceWidth);
                 textContentItem.height +=
                   (advance.height - textContentItem.lastAdvanceHeight);
@@ -37433,17 +37458,18 @@ var PartialEvaluator = (function Partial
               stateManager.save();
               var matrix = xobj.dict.getArray('Matrix');
               if (isArray(matrix) && matrix.length === 6) {
                 stateManager.transform(matrix);
               }
 
               next(self.getTextContent(xobj, task,
                    xobj.dict.get('Resources') || resources, stateManager,
-                   normalizeWhitespace).then(function (formTextContent) {
+                   normalizeWhitespace, combineTextItems).then(
+                function (formTextContent) {
                   Util.appendToArray(textContent.items, formTextContent.items);
                   Util.extendObj(textContent.styles, formTextContent.styles);
                   stateManager.restore();
 
                   xobjsCache.key = name;
                   xobjsCache.texts = formTextContent;
                 }));
               return;
@@ -37589,30 +37615,36 @@ var PartialEvaluator = (function Partial
       if (properties.toUnicode && properties.toUnicode.length !== 0) {
         return Promise.resolve(properties.toUnicode);
       }
       // According to the spec if the font is a simple font we should only map
       // to unicode if the base encoding is MacRoman, MacExpert, or WinAnsi or
       // the differences array only contains adobe standard or symbol set names,
       // in pratice it seems better to always try to create a toUnicode
       // map based of the default encoding.
-      var toUnicode, charcode;
+      var toUnicode, charcode, glyphName;
       if (!properties.composite /* is simple font */) {
         toUnicode = [];
         var encoding = properties.defaultEncoding.slice();
         var baseEncodingName = properties.baseEncodingName;
         // Merge in the differences array.
         var differences = properties.differences;
         for (charcode in differences) {
-          encoding[charcode] = differences[charcode];
+          glyphName = differences[charcode];
+          if (glyphName === '.notdef') {
+            // Skip .notdef to prevent rendering errors, e.g. boxes appearing
+            // where there should be spaces (fixes issue5256.pdf).
+            continue;
+          }
+          encoding[charcode] = glyphName;
         }
         var glyphsUnicodeMap = getGlyphsUnicode();
         for (charcode in encoding) {
           // a) Map the character code to a character name.
-          var glyphName = encoding[charcode];
+          glyphName = encoding[charcode];
           // b) Look up the character name in the Adobe Glyph List (see the
           //    Bibliography) to obtain the corresponding Unicode value.
           if (glyphName === '') {
             continue;
           } else if (glyphsUnicodeMap[glyphName] === undefined) {
             // (undocumented) c) Few heuristics to recognize unknown glyphs
             // NOTE: Adobe Reader does not do this step, but OSX Preview does
             var code = 0;
@@ -37954,25 +37986,25 @@ var PartialEvaluator = (function Partial
 
       var descriptor = dict.get('FontDescriptor');
       if (descriptor) {
         var hash = new MurmurHash3_64();
         var encoding = baseDict.getRaw('Encoding');
         if (isName(encoding)) {
           hash.update(encoding.name);
         } else if (isRef(encoding)) {
-          hash.update(encoding.num + '_' + encoding.gen);
+          hash.update(encoding.toString());
         } else if (isDict(encoding)) {
           var keys = encoding.getKeys();
           for (var i = 0, ii = keys.length; i < ii; i++) {
             var entry = encoding.getRaw(keys[i]);
             if (isName(entry)) {
               hash.update(entry.name);
             } else if (isRef(entry)) {
-              hash.update(entry.num + '_' + entry.gen);
+              hash.update(entry.toString());
             } else if (isArray(entry)) { // 'Differences' entry.
               // Ideally we should check the contents of the array, but to avoid
               // parsing it here and then again in |extractDataStructures|,
               // we only use the array length for now (fixes bug1157493.pdf).
               hash.update(entry.length.toString());
             }
           }
         }
@@ -39160,23 +39192,24 @@ AnnotationFactory.prototype = /** @lends
   create: function AnnotationFactory_create(xref, ref) {
     var dict = xref.fetchIfRef(ref);
     if (!isDict(dict)) {
       return;
     }
 
     // Determine the annotation's subtype.
     var subtype = dict.get('Subtype');
-    subtype = isName(subtype) ? subtype.name : '';
+    subtype = isName(subtype) ? subtype.name : null;
 
     // Return the right annotation object based on the subtype and field type.
     var parameters = {
       xref: xref,
       dict: dict,
-      ref: ref
+      ref: ref,
+      subtype: subtype,
     };
 
     switch (subtype) {
       case 'Link':
         return new LinkAnnotation(parameters);
 
       case 'Text':
         return new TextAnnotation(parameters);
@@ -39202,18 +39235,22 @@ AnnotationFactory.prototype = /** @lends
 
       case 'StrikeOut':
         return new StrikeOutAnnotation(parameters);
 
       case 'FileAttachment':
         return new FileAttachmentAnnotation(parameters);
 
       default:
-        warn('Unimplemented annotation type "' + subtype + '", ' +
-             'falling back to base annotation');
+        if (!subtype) {
+          warn('Annotation is missing the required /Subtype.');
+        } else {
+          warn('Unimplemented annotation type "' + subtype + '", ' +
+               'falling back to base annotation.');
+        }
         return new Annotation(parameters);
     }
   }
 };
 
 var Annotation = (function AnnotationClosure() {
   // 12.5.5: Algorithm: Appearance streams
   function getTransformMatrix(rect, bbox, matrix) {
@@ -39267,17 +39304,17 @@ var Annotation = (function AnnotationClo
     this.setRectangle(dict.getArray('Rect'));
     this.setColor(dict.getArray('C'));
     this.setBorderStyle(dict);
     this.appearance = getDefaultAppearance(dict);
 
     // Expose public properties using a data object.
     this.data = {};
     this.data.id = params.ref.toString();
-    this.data.subtype = dict.get('Subtype').name;
+    this.data.subtype = params.subtype;
     this.data.annotationFlags = this.flags;
     this.data.rect = this.rectangle;
     this.data.color = this.color;
     this.data.borderStyle = this.borderStyle;
     this.data.hasAppearance = !!this.appearance;
   }
 
   Annotation.prototype = {
@@ -40275,17 +40312,18 @@ var Page = (function PageClosure() {
         return annotationsReadyPromise.then(function () {
           pageOpList.flush(true);
           return pageOpList;
         });
       });
     },
 
     extractTextContent: function Page_extractTextContent(task,
-                                                         normalizeWhitespace) {
+                                                         normalizeWhitespace,
+                                                         combineTextItems) {
       var handler = {
         on: function nullHandlerOn() {},
         send: function nullHandlerSend() {}
       };
 
       var self = this;
 
       var pdfManager = this.pdfManager;
@@ -40308,17 +40346,18 @@ var Page = (function PageClosure() {
                                                     self.idCounters,
                                                     self.fontCache,
                                                     self.evaluatorOptions);
 
         return partialEvaluator.getTextContent(contentStream,
                                                task,
                                                self.resources,
                                                /* stateManager = */ null,
-                                               normalizeWhitespace);
+                                               normalizeWhitespace,
+                                               combineTextItems);
       });
     },
 
     getAnnotationsData: function Page_getAnnotationsData(intent) {
       var annotations = this.annotations;
       var annotationsData = [];
       for (var i = 0, n = annotations.length; i < n; ++i) {
         if (intent) {
@@ -41572,22 +41611,24 @@ var WorkerMessageHandler = {
           });
         });
       });
     }, this);
 
     handler.on('GetTextContent', function wphExtractText(data) {
       var pageIndex = data.pageIndex;
       var normalizeWhitespace = data.normalizeWhitespace;
+      var combineTextItems = data.combineTextItems;
       return pdfManager.getPage(pageIndex).then(function(page) {
         var task = new WorkerTask('GetTextContent: page ' + pageIndex);
         startWorkerTask(task);
         var pageNum = pageIndex + 1;
         var start = Date.now();
-        return page.extractTextContent(task, normalizeWhitespace).then(
+        return page.extractTextContent(task, normalizeWhitespace,
+                                       combineTextItems).then(
             function(textContent) {
           finishWorkerTask(task);
           info('text indexing: page=' + pageNum + ' - time=' +
                (Date.now() - start) + 'ms');
           return textContent;
         }, function (reason) {
           finishWorkerTask(task);
           if (task.terminated) {
--- a/browser/extensions/pdfjs/content/web/viewer.js
+++ b/browser/extensions/pdfjs/content/web/viewer.js
@@ -2521,17 +2521,16 @@ exports.binarySearchFirstItem = binarySe
         pageNumber: e.pageNumber
       });
       e.source.textLayerDiv.dispatchEvent(event);
     });
     eventBus.on('pagechange', function (e) {
       var event = document.createEvent('UIEvents');
       event.initUIEvent('pagechange', true, true, window, 0);
       event.pageNumber = e.pageNumber;
-      event.previousPageNumber = e.previousPageNumber;
       e.source.container.dispatchEvent(event);
     });
     eventBus.on('pagesinit', function (e) {
       var event = document.createEvent('CustomEvent');
       event.initCustomEvent('pagesinit', true, true, null);
       e.source.container.dispatchEvent(event);
     });
     eventBus.on('pagesloaded', function (e) {
@@ -5469,22 +5468,22 @@ var PDFPageView = (function PDFPageViewC
       };
       var renderTask = this.renderTask = this.pdfPage.render(renderContext);
       renderTask.onContinue = renderContinueCallback;
 
       this.renderTask.promise.then(
         function pdfPageRenderCallback() {
           pageViewDrawCallback(null);
           if (textLayer) {
-            self.pdfPage.getTextContent({ normalizeWhitespace: true }).then(
-              function textContentResolved(textContent) {
-                textLayer.setTextContent(textContent);
-                textLayer.render(TEXT_LAYER_RENDER_DELAY);
-              }
-            );
+            self.pdfPage.getTextContent({
+              normalizeWhitespace: true,
+            }).then(function textContentResolved(textContent) {
+              textLayer.setTextContent(textContent);
+              textLayer.render(TEXT_LAYER_RENDER_DELAY);
+            });
           }
         },
         function pdfPageRenderError(error) {
           pageViewDrawCallback(error);
         }
       );
 
       if (this.annotationLayerFactory) {
@@ -6352,16 +6351,19 @@ var PDFViewer = (function pdfViewer() {
     get currentPageNumber() {
       return this._currentPageNumber;
     },
 
     /**
      * @param {number} val - The page number.
      */
     set currentPageNumber(val) {
+      if ((val | 0) !== val) { // Ensure that `val` is an integer.
+        throw new Error('Invalid page number.');
+      }
       if (!this.pdfDocument) {
         this._currentPageNumber = val;
         return;
       }
       // The intent can be to just reset a scroll position and/or scale.
       this._setCurrentPageNumber(val, /* resetCurrentPageView = */ true);
     },
 
@@ -6371,32 +6373,24 @@ var PDFViewer = (function pdfViewer() {
     _setCurrentPageNumber:
         function pdfViewer_setCurrentPageNumber(val, resetCurrentPageView) {
       if (this._currentPageNumber === val) {
         if (resetCurrentPageView) {
           this._resetCurrentPageView();
         }
         return;
       }
-      var arg;
+
       if (!(0 < val && val <= this.pagesCount)) {
-        arg = {
-          source: this,
-          pageNumber: this._currentPageNumber,
-          previousPageNumber: val
-        };
-        this.eventBus.dispatch('pagechanging', arg);
-        this.eventBus.dispatch('pagechange', arg);
         return;
       }
 
-      arg = {
+      var arg = {
         source: this,
         pageNumber: val,
-        previousPageNumber: this._currentPageNumber
       };
       this._currentPageNumber = val;
       this.eventBus.dispatch('pagechanging', arg);
       this.eventBus.dispatch('pagechange', arg);
 
       if (resetCurrentPageView) {
         this._resetCurrentPageView();
       }
@@ -6409,17 +6403,17 @@ var PDFViewer = (function pdfViewer() {
       return this._currentScale !== UNKNOWN_SCALE ? this._currentScale :
                                                     DEFAULT_SCALE;
     },
 
     /**
      * @param {number} val - Scale of the pages in percents.
      */
     set currentScale(val) {
-      if (isNaN(val))  {
+      if (isNaN(val)) {
         throw new Error('Invalid numeric scale');
       }
       if (!this.pdfDocument) {
         this._currentScale = val;
         this._currentScaleValue = val !== UNKNOWN_SCALE ? val.toString() : null;
         return;
       }
       this._setScale(val, false);
@@ -6970,17 +6964,19 @@ var PDFViewer = (function pdfViewer() {
         }.bind(this));
         return true;
       }
       return false;
     },
 
     getPageTextContent: function (pageIndex) {
       return this.pdfDocument.getPage(pageIndex + 1).then(function (page) {
-        return page.getTextContent({ normalizeWhitespace: true });
+        return page.getTextContent({
+          normalizeWhitespace: true,
+        });
       });
     },
 
     /**
      * @param {HTMLDivElement} textLayerDiv
      * @param {number} pageIndex
      * @param {PageViewport} viewport
      * @returns {TextLayerBuilder}
@@ -8328,21 +8324,22 @@ function webViewerInitialized() {
       PDFViewerApplication.zoomOut();
     });
 
   appConfig.toolbar.pageNumber.addEventListener('click', function() {
     this.select();
   });
 
   appConfig.toolbar.pageNumber.addEventListener('change', function() {
-    // Handle the user inputting a floating point number.
     PDFViewerApplication.page = (this.value | 0);
 
-    if (this.value !== (this.value | 0).toString()) {
-      this.value = PDFViewerApplication.page;
+    // Ensure that the page number input displays the correct value, even if the
+    // value entered by the user was invalid (e.g. a floating point number).
+    if (this.value !== PDFViewerApplication.page.toString()) {
+      PDFViewerApplication._updateUIToolbar({});
     }
   });
 
   appConfig.toolbar.scaleSelect.addEventListener('change', function() {
     if (this.value === 'custom') {
       return;
     }
     PDFViewerApplication.pdfViewer.currentScaleValue = this.value;
@@ -8688,18 +8685,18 @@ function webViewerScaleChanging(e) {
 }
 
 function webViewerPageChanging(e) {
   var page = e.pageNumber;
 
   PDFViewerApplication._updateUIToolbar({
     pageNumber: page,
   });
-  if (e.previousPageNumber !== page &&
-      PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
+
+  if (PDFViewerApplication.pdfSidebar.isThumbnailViewVisible) {
     PDFViewerApplication.pdfThumbnailViewer.scrollThumbnailIntoView(page);
   }
 
   // we need to update stats
   if (pdfjsLib.PDFJS.pdfBug && Stats.enabled) {
     var pageView = PDFViewerApplication.pdfViewer.getPageView(page - 1);
     if (pageView.stats) {
       Stats.add(page, pageView.stats);
--- a/browser/locales/en-US/chrome/browser/preferences/main.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/main.dtd
@@ -27,17 +27,17 @@
 <!ENTITY chooseFolderWin.label        "Browse…">
 <!ENTITY chooseFolderWin.accesskey    "o">
 <!ENTITY chooseFolderMac.label        "Choose…">
 <!ENTITY chooseFolderMac.accesskey    "e">
 <!ENTITY alwaysAsk.label "Always ask me where to save files">
 <!ENTITY alwaysAsk.accesskey "A">
 
 <!ENTITY alwaysCheckDefault2.label        "Always check if &brandShortName; is your default browser">
-<!ENTITY alwaysCheckDefault2.accesskey    "w">
+<!ENTITY alwaysCheckDefault2.accesskey    "y">
 <!ENTITY setAsMyDefaultBrowser2.label     "Make Default">
 <!ENTITY setAsMyDefaultBrowser2.accesskey "D">
 <!ENTITY isDefault.label                  "&brandShortName; is currently your default browser">
 <!ENTITY isNotDefault.label               "&brandShortName; is not your default browser">
 
 <!ENTITY separateProfileMode.label        "Allow &brandShortName; and Firefox to run at the same time">
 <!ENTITY useFirefoxSync.label             "Tip: This uses separate profiles. Use Sync to share data between them.">
 <!ENTITY getStarted.label                 "Start using Sync…">
--- a/browser/locales/en-US/chrome/browser/preferences/tabs.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/tabs.dtd
@@ -1,14 +1,17 @@
 <!-- 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/. -->
 
+<!ENTITY ctrlTabRecentlyUsedOrder.label       "Ctrl+Tab cycles through tabs in recently used order">
+<!ENTITY ctrlTabRecentlyUsedOrder.accesskey   "T">
+
 <!ENTITY newWindowsAsTabs.label       "Open new windows in a new tab instead">
-<!ENTITY newWindowsAsTabs.accesskey   "t">
+<!ENTITY newWindowsAsTabs.accesskey   "w">
 
 <!ENTITY warnCloseMultipleTabs.label  "Warn me when closing multiple tabs">
 <!ENTITY warnCloseMultipleTabs.accesskey  "m">
 
 <!ENTITY warnOpenManyTabs.label       "Warn me when opening multiple tabs might slow down &brandShortName;">
 <!ENTITY warnOpenManyTabs.accesskey   "d">
 
 <!ENTITY switchToNewTabs.label        "When I open a link in a new tab, switch to it immediately">
--- a/browser/themes/osx/devedition.css
+++ b/browser/themes/osx/devedition.css
@@ -86,20 +86,16 @@
 }
 
 #back-button:hover:active:not([disabled="true"]) {
   -moz-image-region: rect(18px, 54px, 36px, 36px);
 }
 
 /* Use smaller back button icon */
 @media (min-resolution: 2dppx) {
-  #back-button {
-    -moz-image-region: rect(0, 108px, 36px, 72px);
-  }
-
   #back-button:hover:active:not([disabled="true"]) {
     -moz-image-region: rect(36px, 108px, 72px, 72px);
   }
 }
 
 #forward-button:hover:active:not(:-moz-lwtheme) {
   background-image: none;
   box-shadow: none;
--- a/browser/themes/shared/controlcenter/panel.inc.css
+++ b/browser/themes/shared/controlcenter/panel.inc.css
@@ -357,18 +357,18 @@ description#identity-popup-content-verif
 
 #identity-popup-permission-list {
   /* Offset the padding set on #identity-popup-permissions-content so that it
      shows up just below the section. The permission icons are 16px wide and
      should be right aligned with the section icon. */
   margin-inline-start: calc(-1em - 16px);
 }
 
-#identity-popup-permission-list menulist {
-  min-width: 60px;
+.identity-popup-permission-item {
+  min-height: 24px;
 }
 
 #identity-popup-permission-list:not(:empty) {
   margin-top: 5px;
 }
 
 #identity-popup-permission-list:not(:empty) + description {
   display: none;
@@ -376,10 +376,53 @@ description#identity-popup-content-verif
 
 .identity-popup-permission-icon {
   width: 16px;
   height: 16px;
 }
 
 .identity-popup-permission-label {
   margin-inline-start: 1em;
-  word-wrap: break-word;
+}
+
+.identity-popup-permission-state-label {
+  text-align: end;
+  opacity: 0.6;
+}
+
+.identity-popup-permission-remove-button {
+  -moz-appearance: none;
+  margin: 0;
+  border-width: 0;
+  border-radius: 50%;
+  min-width: 0;
+  padding: 2px;
+}
+
+.identity-popup-permission-remove-button > .button-box {
+  border-width: 0;
+  padding: 0;
 }
+
+.identity-popup-permission-remove-button > .button-box > .button-icon {
+  margin: 0;
+  width: 16px;
+  height: 16px;
+  list-style-image: url(chrome://browser/skin/panel-icons.svg#cancel);
+  filter: url(chrome://browser/skin/filters.svg#fill);
+  fill: #999;
+}
+
+.identity-popup-permission-remove-button > .button-box > .button-text {
+  display: none;
+}
+
+.identity-popup-permission-remove-button:hover {
+  background-color: #999;
+}
+
+.identity-popup-permission-remove-button:hover > .button-box > .button-icon {
+  fill: #fff;
+}
+
+.identity-popup-permission-remove-button:hover:active {
+  background-color: #808080;
+}
--- a/browser/themes/shared/customizableui/panelUI.inc.css
+++ b/browser/themes/shared/customizableui/panelUI.inc.css
@@ -272,16 +272,20 @@ panelmultiview[nosubviews=true] > .panel
 .cui-widget-panel[viewId^=PanelUI-webext-] > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
 }
 
 .cui-widget-panelview[id^=PanelUI-webext-] {
   border-radius: 3.5px;
 }
 
+panelview[id^=PanelUI-webext-] {
+  overflow: hidden;
+}
+
 panelview:not([mainview]) .toolbarbutton-text,
 .cui-widget-panel toolbarbutton > .toolbarbutton-text {
   text-align: start;
   display: -moz-box;
 }
 
 .cui-widget-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 4px 0;
--- a/browser/themes/shared/devedition.inc.css
+++ b/browser/themes/shared/devedition.inc.css
@@ -223,16 +223,22 @@ window:not([chromehidden~="toolbar"]) #u
   padding-bottom: 0;
 }
 
 /* Use smaller back button icon */
 #back-button {
   -moz-image-region: rect(0, 54px, 18px, 36px);
 }
 
+@media (min-resolution: 1.1dppx) {
+  #back-button {
+    -moz-image-region: rect(0, 108px, 36px, 72px);
+  }
+}
+
 .tab-background {
   visibility: hidden;
 }
 
 /* Tab separators */
 .tabbrowser-tab::after,
 .tabbrowser-tab::before {
   background: currentColor;
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -67,16 +67,17 @@
   skin/classic/browser/identity-mixed-active-loaded.svg        (../shared/identity-block/identity-mixed-active-loaded.svg)
   skin/classic/browser/info.svg                                (../shared/info.svg)
   skin/classic/browser/notification-icons.svg                  (../shared/notification-icons.svg)
   skin/classic/browser/tracking-protection-16.svg              (../shared/identity-block/tracking-protection-16.svg)
   skin/classic/browser/tracking-protection-disabled-16.svg     (../shared/identity-block/tracking-protection-disabled-16.svg)
   skin/classic/browser/newtab/close.png                        (../shared/newtab/close.png)
   skin/classic/browser/newtab/controls.svg                     (../shared/newtab/controls.svg)
   skin/classic/browser/newtab/whimsycorn.png                   (../shared/newtab/whimsycorn.png)
+  skin/classic/browser/panel-icons.svg                         (../shared/panel-icons.svg)
   skin/classic/browser/preferences/in-content/favicon.ico      (../shared/incontentprefs/favicon.ico)
   skin/classic/browser/preferences/in-content/icons.svg        (../shared/incontentprefs/icons.svg)
   skin/classic/browser/preferences/in-content/search.css       (../shared/incontentprefs/search.css)
   skin/classic/browser/fxa/default-avatar.svg                  (../shared/fxa/default-avatar.svg)
   skin/classic/browser/fxa/logo.png                            (../shared/fxa/logo.png)
   skin/classic/browser/fxa/logo@2x.png                         (../shared/fxa/logo@2x.png)
   skin/classic/browser/fxa/sync-illustration.png               (../shared/fxa/sync-illustration.png)
   skin/classic/browser/fxa/sync-illustration@2x.png            (../shared/fxa/sync-illustration@2x.png)
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -36,17 +36,17 @@
 }
 
 .popup-notification-icon {
   width: 64px;
   height: 64px;
   margin-inline-end: 10px;
 }
 
-#notification-popup-box > .notification-anchor-icon:hover {
+#notification-popup-box > .notification-anchor-icon:not(.in-use):hover {
   fill: #606060;
 }
 
 /* INDIVIDUAL NOTIFICATIONS */
 
 /* For the moment we apply the color filter only on the icons listed here.
    The first two selectors are used by socialchat.xml (bug 1275558). */
 .webRTC-sharingDevices-notification-icon,
@@ -269,17 +269,17 @@
 /* PLUGINS */
 
 .plugin-icon {
   list-style-image: url(chrome://browser/skin/notification-icons.svg#plugin);
 }
 
 .plugin-icon.plugin-blocked {
   list-style-image: url(chrome://browser/skin/notification-icons.svg#plugin-blocked);
-  fill: #d92215;
+  fill: #d92215 !important; /* important! to override the default hover color */
 }
 
 #notification-popup-box[hidden] {
   /* Override display:none to make the pluginBlockedNotification animation work
      when showing the notification repeatedly. */
   display: -moz-box;
   visibility: collapse;
 }
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/panel-icons.svg
@@ -0,0 +1,8 @@
+<?xml version="1.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/. -->
+<svg xmlns="http://www.w3.org/2000/svg"
+     width="32" height="32" viewBox="0 0 32 32">
+  <path id="cancel" d="m 6,9.5 6.5,6.5 -6.5,6.5 3.5,3.5 6.5,-6.5 6.5,6.5 3.5,-3.5 -6.5,-6.5 6.5,-6.5 -3.5,-3.5 -6.5,6.5 -6.5,-6.5 z" />
+</svg>
--- a/browser/themes/windows/devedition.css
+++ b/browser/themes/windows/devedition.css
@@ -38,48 +38,38 @@
 #TabsToolbar::after {
   display: none;
 }
 
 #back-button > .toolbarbutton-icon,
 #forward-button > .toolbarbutton-icon {
   background: var(--chrome-nav-buttons-background) !important;
   border-radius: 0 !important;
-  width: auto !important;
   height: auto !important;
   padding: var(--toolbarbutton-vertical-inner-padding) 5px !important;
   margin: 0 !important;
   border: 1px solid var(--chrome-nav-bar-controls-border-color) !important;
   box-shadow: none !important;
 }
 
+#back-button > .toolbarbutton-icon {
+  /* 18px icon + 2 * 5px padding + 2 * 1px border */
+  width: 30px !important;
+}
+
+#forward-button > .toolbarbutton-icon {
+  /* 18px icon + 2 * 5px padding + 1 * 1px border */
+  width: 29px !important;
+}
+
 /* the normal theme adds box-shadow: <stuff> !important when the back-button is [open]. Fix: */
 #back-button[open="true"] > .toolbarbutton-icon {
   box-shadow: none !important;
 }
 
-/* Force 1x image for back/forward button for now, otherwise it breaks the
-   layout - Bug 1165360. */
-@media (min-resolution: 1.1dppx) {
-  #back-button,
-  #forward-button {
-    list-style-image: url("chrome://browser/skin/Toolbar.png");
-  }
-
-  toolbar[brighttext] #back-button,
-  toolbar[brighttext] #forward-button {
-    list-style-image: url("chrome://browser/skin/Toolbar-inverted.png");
-  }
-
-  /* The back button region is already set in devedition.inc.css */
-  #forward-button {
-    -moz-image-region: rect(0px, 72px, 18px, 54px);
-  }
-}
-
 #forward-button > .toolbarbutton-icon {
   border-inline-start: none !important;
 }
 
 /* Override a box shadow for disabled back button */
 #main-window:not([customizing]) #back-button[disabled] > .toolbarbutton-icon {
   box-shadow: none !important;
 }
--- a/build/moz.configure/windows.configure
+++ b/build/moz.configure/windows.configure
@@ -180,39 +180,30 @@ def sdk_bin_path(valid_windows_sdk_dir):
 mt = check_prog('_MT', depends_win()(lambda: ('mt.exe',)), what='mt',
                 input='MT', paths=sdk_bin_path)
 
 
 # Check that MT is not something unexpected like "magnetic tape manipulation
 # utility".
 @depends_win(mt)
 @checking('whether MT is really Microsoft Manifest Tool', lambda x: bool(x))
-@imports('re')
 @imports('subprocess')
 def valid_mt(path):
     try:
         out = subprocess.check_output([path]).splitlines()
         out = '\n'.join(l for l in out
                         if 'Microsoft (R) Manifest Tool' in l)
         if out:
-              m = re.search(r'(?<=[^!-~])[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+',
-                            out)
-              if not m:
-                  raise FatalCheckError(
-                      'Unknown version of the Microsoft Manifest Tool')
-              return namespace(
-                  path=path,
-                  version=Version(m.group(0)),
-              )
+              return path
     except subprocess.CalledProcessError:
         pass
     raise FatalCheckError('%s is not Microsoft Manifest Tool')
 
 
-set_config('MT', depends_if(valid_mt)(lambda x: os.path.basename(x.path)))
+set_config('MT', depends_if(valid_mt)(lambda x: os.path.basename(x)))
 set_config('MSMANIFEST_TOOL', depends(valid_mt)(lambda x: bool(x)))
 
 
 # Normally, we'd just have CC, etc. set to absolute paths, but the build system
 # doesn't currently handle properly the case where the paths contain spaces.
 # Additionally, there's the issue described in toolchain.configure, in
 # valid_compiler().
 @depends_win(sdk_bin_path)
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension-nobg/manifest.json
@@ -0,0 +1,10 @@
+{
+  "manifest_version": 2,
+  "name": "test-devtools-webextension-nobg",
+  "version": "1.0",
+  "applications": {
+    "gecko": {
+      "id": "test-devtools-webextension-nobg@mozilla.org"
+    }
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/bg.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* global browser */
+
+"use strict";
+
+document.body.innerText = "Background Page Body Test Content";
+
+// This function are called from the webconsole test:
+// browser_addons_debug_webextension.js
+
+function myWebExtensionAddonFunction() {  // eslint-disable-line no-unused-vars
+  console.log("Background page function called", browser.runtime.getManifest());
+}
+
+function myWebExtensionShowPopup() {  // eslint-disable-line no-unused-vars
+  console.log("readyForOpenPopup");
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/manifest.json
@@ -0,0 +1,17 @@
+{
+  "manifest_version": 2,
+  "name": "test-devtools-webextension",
+  "version": "1.0",
+  "applications": {
+    "gecko": {
+      "id": "test-devtools-webextension@mozilla.org"
+    }
+  },
+  "background": {
+    "scripts": ["bg.js"]
+  },
+  "browser_action": {
+    "default_title": "WebExtension Popup Debugging",
+    "default_popup": "popup.html"
+  }
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8">
+    <script src="popup.js"></script>
+  </head>
+  <body>
+    Background Page Body Test Content
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/test-devtools-webextension/popup.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* eslint-env browser */
+/* global browser */
+
+"use strict";
+
+// This function is called from the webconsole test:
+// browser_addons_debug_webextension.js
+function myWebExtensionPopupAddonFunction() {  // eslint-disable-line no-unused-vars
+  console.log("Popup page function called", browser.runtime.getManifest());
+}
--- a/devtools/client/aboutdebugging/test/browser.ini
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -2,23 +2,29 @@
 tags = devtools
 subsuite = devtools
 support-files =
   head.js
   addons/unpacked/bootstrap.js
   addons/unpacked/install.rdf
   addons/bad/manifest.json
   addons/bug1273184.xpi
+  addons/test-devtools-webextension/*
+  addons/test-devtools-webextension-nobg/*
   service-workers/empty-sw.html
   service-workers/empty-sw.js
   service-workers/push-sw.html
   service-workers/push-sw.js
   !/devtools/client/framework/test/shared-head.js
 
 [browser_addons_debug_bootstrapped.js]
+[browser_addons_debug_webextension.js]
+[browser_addons_debug_webextension_inspector.js]
+[browser_addons_debug_webextension_nobg.js]
+[browser_addons_debug_webextension_popup.js]
 [browser_addons_debugging_initial_state.js]
 [browser_addons_install.js]
 [browser_addons_reload.js]
 [browser_addons_toggle_debug.js]
 [browser_page_not_found.js]
 [browser_service_workers.js]
 [browser_service_workers_not_compatible.js]
 [browser_service_workers_push.js]
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
@@ -21,18 +21,21 @@ add_task(function* () {
       // Enable Browser toolbox test script execution via env variable
       ["devtools.browser-toolbox.allow-unsafe-script", true],
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
-  yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
-                     "test-devtools");
+  yield installAddon({
+    document,
+    path: "addons/unpacked/install.rdf",
+    name: ADDON_NAME,
+  });
 
   // Retrieve the DEBUG button for the addon
   let names = [...document.querySelectorAll("#addons .target-name")];
   let name = names.filter(element => element.textContent === ADDON_NAME)[0];
   ok(name, "Found the addon in the list");
   let targetElement = name.parentNode.parentNode;
   let debugBtn = targetElement.querySelector(".debug-button");
   ok(debugBtn, "Found its debug button");
@@ -70,11 +73,11 @@ add_task(function* () {
   debugBtn.click();
 
   yield onCustomMessage;
   ok(true, "Received the notification message from the bootstrap.js function");
 
   yield onToolboxClose;
   ok(true, "Addon toolbox closed");
 
-  yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
   yield closeAboutDebugging(tab);
 });
copy from devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
copy to devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js
--- a/devtools/client/aboutdebugging/test/browser_addons_debug_bootstrapped.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension.js
@@ -1,80 +1,74 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
 requestLongerTimeout(2);
 
-const ADDON_ID = "test-devtools@mozilla.org";
-const ADDON_NAME = "test-devtools";
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+const ADDON_MANIFEST_PATH = "addons/test-devtools-webextension/manifest.json";
 
-const { BrowserToolboxProcess } = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+const {
+  BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
 
-add_task(function* () {
-  yield new Promise(resolve => {
-    let options = {"set": [
-      // Force enabling of addons debugging
-      ["devtools.chrome.enabled", true],
-      ["devtools.debugger.remote-enabled", true],
-      // Disable security prompt
-      ["devtools.debugger.prompt-connection", false],
-      // Enable Browser toolbox test script execution via env variable
-      ["devtools.browser-toolbox.allow-unsafe-script", true],
-    ]};
-    SpecialPowers.pushPrefEnv(options, resolve);
-  });
-
-  let { tab, document } = yield openAboutDebugging("addons");
-  yield waitForInitialAddonList(document);
-  yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
-                     "test-devtools");
-
-  // Retrieve the DEBUG button for the addon
-  let names = [...document.querySelectorAll("#addons .target-name")];
-  let name = names.filter(element => element.textContent === ADDON_NAME)[0];
-  ok(name, "Found the addon in the list");
-  let targetElement = name.parentNode.parentNode;
-  let debugBtn = targetElement.querySelector(".debug-button");
-  ok(debugBtn, "Found its debug button");
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - when the debug button is clicked on a webextension, the opened toolbox
+ *   has a working webconsole with the background page as default target;
+ */
+add_task(function* testWebExtensionsToolboxWebConsole() {
+  let {
+    tab, document, debugBtn,
+  } = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_MANIFEST_PATH);
 
   // Wait for a notification sent by a script evaluated the test addon via
   // the web console.
   let onCustomMessage = new Promise(done => {
-    Services.obs.addObserver(function listener() {
-      Services.obs.removeObserver(listener, "addon-console-works");
-      done();
-    }, "addon-console-works", false);
+    Services.obs.addObserver(function listener(message, topic) {
+      let apiMessage = message.wrappedJSObject;
+      if (!apiMessage.originAttributes ||
+          apiMessage.originAttributes.addonId != ADDON_ID) {
+        return;
+      }
+      Services.obs.removeObserver(listener, "console-api-log-event");
+      done(apiMessage.arguments);
+    }, "console-api-log-event", false);
   });
 
   // Be careful, this JS function is going to be executed in the addon toolbox,
   // which lives in another process. So do not try to use any scope variable!
   let env = Cc["@mozilla.org/process/environment;1"]
               .getService(Ci.nsIEnvironment);
   let testScript = function () {
     /* eslint-disable no-undef */
     toolbox.selectTool("webconsole")
       .then(console => {
         let { jsterm } = console.hud;
-        return jsterm.execute("myBootstrapAddonFunction()");
+        return jsterm.execute("myWebExtensionAddonFunction()");
       })
       .then(() => toolbox.destroy());
     /* eslint-enable no-undef */
   };
   env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
   registerCleanupFunction(() => {
     env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
   });
 
   let onToolboxClose = BrowserToolboxProcess.once("close");
 
   debugBtn.click();
 
-  yield onCustomMessage;
-  ok(true, "Received the notification message from the bootstrap.js function");
+  let args = yield onCustomMessage;
+  ok(true, "Received console message from the background page function as expected");
+  is(args[0], "Background page function called", "Got the expected console message");
+  is(args[1] && args[1].name, ADDON_NAME,
+     "Got the expected manifest from WebExtension API");
 
   yield onToolboxClose;
   ok(true, "Addon toolbox closed");
 
-  yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
   yield closeAboutDebugging(tab);
 });
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_inspector.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+const ADDON_PATH = "addons/test-devtools-webextension/manifest.json";
+
+const {
+  BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - the webextension developer toolbox has a working Inspector panel, with the
+ *   background page as default target;
+ */
+add_task(function* testWebExtensionsToolboxInspector() {
+  let {
+    tab, document, debugBtn,
+  } = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_PATH);
+
+  // Be careful, this JS function is going to be executed in the addon toolbox,
+  // which lives in another process. So do not try to use any scope variable!
+  let env = Cc["@mozilla.org/process/environment;1"]
+        .getService(Ci.nsIEnvironment);
+  let testScript = function () {
+    /* eslint-disable no-undef */
+    toolbox.selectTool("inspector")
+      .then(inspector => {
+        return inspector.walker.querySelector(inspector.walker.rootNode, "body");
+      })
+      .then((nodeActor) => {
+        if (!nodeActor) {
+          throw new Error("nodeActor not found");
+        }
+
+        dump("Got a nodeActor\n");
+
+        if (!(nodeActor.inlineTextChild)) {
+          throw new Error("inlineTextChild not found");
+        }
+
+        dump("Got a nodeActor with an inline text child\n");
+
+        let expectedValue = "Background Page Body Test Content";
+        let actualValue = nodeActor.inlineTextChild._form.nodeValue;
+
+        if (String(actualValue).trim() !== String(expectedValue).trim()) {
+          throw new Error(
+            `mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
+          );
+        }
+
+        dump("Got the expected inline text content in the selected node\n");
+        return Promise.resolve();
+      })
+      .then(() => toolbox.destroy())
+      .catch((error) => {
+        dump("Error while running code in the browser toolbox process:\n");
+        dump(error + "\n");
+        dump("stack:\n" + error.stack + "\n");
+      });
+    /* eslint-enable no-undef */
+  };
+  env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+  registerCleanupFunction(() => {
+    env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+  });
+
+  let onToolboxClose = BrowserToolboxProcess.once("close");
+  debugBtn.click();
+  yield onToolboxClose;
+
+  ok(true, "Addon toolbox closed");
+
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+  yield closeAboutDebugging(tab);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_nobg.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_NOBG_ID = "test-devtools-webextension-nobg@mozilla.org";
+const ADDON_NOBG_NAME = "test-devtools-webextension-nobg";
+const ADDON_NOBG_PATH = "addons/test-devtools-webextension-nobg/manifest.json";
+
+const {
+  BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - the webextension developer toolbox is connected to a fallback page when the
+ *   background page is not available (and in the fallback page document body contains
+ *   the expected message, which warns the user that the current page is not a real
+ *   webextension context);
+ */
+add_task(function* testWebExtensionsToolboxNoBackgroundPage() {
+  let {
+    tab, document, debugBtn,
+  } = yield setupTestAboutDebuggingWebExtension(ADDON_NOBG_NAME, ADDON_NOBG_PATH);
+
+  // Be careful, this JS function is going to be executed in the addon toolbox,
+  // which lives in another process. So do not try to use any scope variable!
+  let env = Cc["@mozilla.org/process/environment;1"]
+        .getService(Ci.nsIEnvironment);
+  let testScript = function () {
+    /* eslint-disable no-undef */
+    toolbox.selectTool("inspector")
+      .then(inspector => {
+        return inspector.walker.querySelector(inspector.walker.rootNode, "body");
+      })
+      .then((nodeActor) => {
+        if (!nodeActor) {
+          throw new Error("nodeActor not found");
+        }
+
+        dump("Got a nodeActor\n");
+
+        if (!(nodeActor.inlineTextChild)) {
+          throw new Error("inlineTextChild not found");
+        }
+
+        dump("Got a nodeActor with an inline text child\n");
+
+        let expectedValue = "Your addon does not have any document opened yet.";
+        let actualValue = nodeActor.inlineTextChild._form.nodeValue;
+
+        if (actualValue !== expectedValue) {
+          throw new Error(
+            `mismatched inlineTextchild value: "${actualValue}" !== "${expectedValue}"`
+          );
+        }
+
+        dump("Got the expected inline text content in the selected node\n");
+        return Promise.resolve();
+      })
+      .then(() => toolbox.destroy())
+      .catch((error) => {
+        dump("Error while running code in the browser toolbox process:\n");
+        dump(error + "\n");
+        dump("stack:\n" + error.stack + "\n");
+      });
+    /* eslint-enable no-undef */
+  };
+  env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+  registerCleanupFunction(() => {
+    env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+  });
+
+  let onToolboxClose = BrowserToolboxProcess.once("close");
+  debugBtn.click();
+  yield onToolboxClose;
+
+  ok(true, "Addon toolbox closed");
+
+  yield uninstallAddon({document, id: ADDON_NOBG_ID, name: ADDON_NOBG_NAME});
+  yield closeAboutDebugging(tab);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_debug_webextension_popup.js
@@ -0,0 +1,180 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Avoid test timeouts that can occur while waiting for the "addon-console-works" message.
+requestLongerTimeout(2);
+
+const ADDON_ID = "test-devtools-webextension@mozilla.org";
+const ADDON_NAME = "test-devtools-webextension";
+const ADDON_MANIFEST_PATH = "addons/test-devtools-webextension/manifest.json";
+
+const {
+  BrowserToolboxProcess
+} = Cu.import("resource://devtools/client/framework/ToolboxProcess.jsm", {});
+
+/**
+ * This test file ensures that the webextension addon developer toolbox:
+ * - when the debug button is clicked on a webextension, the opened toolbox
+ *   has a working webconsole with the background page as default target;
+ * - the webextension developer toolbox has a working Inspector panel, with the
+ *   background page as default target;
+ * - the webextension developer toolbox is connected to a fallback page when the
+ *   background page is not available (and in the fallback page document body contains
+ *   the expected message, which warns the user that the current page is not a real
+ *   webextension context);
+ * - the webextension developer toolbox has a frame list menu and the noautohide toolbar
+ *   toggle button, and they can be used to switch the current target to the extension
+ *   popup page.
+ */
+
+/**
+ * Returns the widget id for an extension with the passed id.
+ */
+function makeWidgetId(id) {
+  id = id.toLowerCase();
+  return id.replace(/[^a-z0-9_-]/g, "_");
+}
+
+add_task(function* testWebExtensionsToolboxSwitchToPopup() {
+  let {
+    tab, document, debugBtn,
+  } = yield setupTestAboutDebuggingWebExtension(ADDON_NAME, ADDON_MANIFEST_PATH);
+
+  let onReadyForOpenPopup = new Promise(done => {
+    Services.obs.addObserver(function listener(message, topic) {
+      let apiMessage = message.wrappedJSObject;
+      if (!apiMessage.originAttributes ||
+          apiMessage.originAttributes.addonId != ADDON_ID) {
+        return;
+      }
+
+      if (apiMessage.arguments[0] == "readyForOpenPopup") {
+        Services.obs.removeObserver(listener, "console-api-log-event");
+        done();
+      }
+    }, "console-api-log-event", false);
+  });
+
+  // Be careful, this JS function is going to be executed in the addon toolbox,
+  // which lives in another process. So do not try to use any scope variable!
+  let env = Cc["@mozilla.org/process/environment;1"]
+        .getService(Ci.nsIEnvironment);
+  let testScript = function () {
+    /* eslint-disable no-undef */
+
+    let jsterm;
+
+    toolbox.selectTool("webconsole")
+      .then(console => {
+        dump(`Clicking the noautohide button\n`);
+        toolbox.doc.getElementById("command-button-noautohide").click();
+        dump(`Clicked the noautohide button\n`);
+
+        let waitForFrameListUpdate = new Promise((done) => {
+          toolbox.target.once("frame-update", () => {
+            done(console);
+          });
+        });
+
+        jsterm = console.hud.jsterm;
+        jsterm.execute("myWebExtensionShowPopup()");
+
+        // Wait the initial frame update (which list the background page).
+        return waitForFrameListUpdate;
+      })
+      .then((console) => {
+        // Wait the new frame update (once the extension popup has been opened).
+        return new Promise((done) => {
+          toolbox.target.once("frame-update", done);
+        });
+      })
+      .then(() => {
+        dump(`Clicking the frame list button\n`);
+        let btn = toolbox.doc.getElementById("command-button-frames");
+        let menu = toolbox.showFramesMenu({target: btn});
+        dump(`Clicked the frame list button\n`);
+        return menu.once("open").then(() => {
+          return menu;
+        });
+      })
+      .then(frameMenu => {
+        let frames = frameMenu.items;
+
+        if (frames.length != 2) {
+          throw Error(`Number of frames found is wrong: ${frames.length} != 2`);
+        }
+
+        let popupFrameBtn = frames.filter((frame) => {
+          return frame.label.endsWith("popup.html");
+        }).pop();
+
+        if (!popupFrameBtn) {
+          throw Error("Extension Popup frame not found in the listed frames");
+        }
+
+        let waitForNavigated = toolbox.target.once("navigate");
+
+        popupFrameBtn.click();
+
+        return waitForNavigated;
+      })
+      .then(() => {
+        return jsterm.execute("myWebExtensionPopupAddonFunction()");
+      })
+      .then(() => toolbox.destroy())
+      .catch((error) => {
+        dump("Error while running code in the browser toolbox process:\n");
+        dump(error + "\n");
+        dump("stack:\n" + error.stack + "\n");
+      });
+    /* eslint-enable no-undef */
+  };
+  env.set("MOZ_TOOLBOX_TEST_SCRIPT", "new " + testScript);
+  registerCleanupFunction(() => {
+    env.set("MOZ_TOOLBOX_TEST_SCRIPT", "");
+  });
+
+  // Wait for a notification sent by a script evaluated the test addon via
+  // the web console.
+  let onPopupCustomMessage = new Promise(done => {
+    Services.obs.addObserver(function listener(message, topic) {
+      let apiMessage = message.wrappedJSObject;
+      if (!apiMessage.originAttributes ||
+          apiMessage.originAttributes.addonId != ADDON_ID) {
+        return;
+      }
+
+      if (apiMessage.arguments[0] == "Popup page function called") {
+        Services.obs.removeObserver(listener, "console-api-log-event");
+        done(apiMessage.arguments);
+      }
+    }, "console-api-log-event", false);
+  });
+
+  let onToolboxClose = BrowserToolboxProcess.once("close");
+
+  debugBtn.click();
+
+  yield onReadyForOpenPopup;
+
+  let browserActionId = makeWidgetId(ADDON_ID) + "-browser-action";
+  let browserActionEl = window.document.getElementById(browserActionId);
+
+  ok(browserActionEl, "Got the browserAction button from the browser UI");
+  browserActionEl.click();
+  info("Clicked on the browserAction button");
+
+  let args = yield onPopupCustomMessage;
+  ok(true, "Received console message from the popup page function as expected");
+  is(args[0], "Popup page function called", "Got the expected console message");
+  is(args[1] && args[1].name, ADDON_NAME,
+     "Got the expected manifest from WebExtension API");
+
+  yield onToolboxClose;
+
+  ok(true, "Addon toolbox closed");
+
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
+  yield closeAboutDebugging(tab);
+});
--- a/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_debugging_initial_state.js
@@ -45,26 +45,29 @@ function* testCheckboxState(testData) {
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
 
   info("Install a test addon.");
-  yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
-                     "test-devtools");
+  yield installAddon({
+    document,
+    path: "addons/unpacked/install.rdf",
+    name: ADDON_NAME,
+  });
 
   info("Test checkbox checked state.");
   let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
   is(addonDebugCheckbox.checked, testData.expected,
     "Addons debugging checkbox should be in expected state.");
 
   info("Test debug buttons disabled state.");
   let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
   ok(debugButtons.every(b => b.disabled != testData.expected),
     "Debug buttons should be in the expected state");
 
   info("Uninstall test addon installed earlier.");
-  yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
 
   yield closeAboutDebugging(tab);
 }
--- a/devtools/client/aboutdebugging/test/browser_addons_install.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_install.js
@@ -5,21 +5,24 @@
 const ADDON_ID = "test-devtools@mozilla.org";
 const ADDON_NAME = "test-devtools";
 
 add_task(function* () {
   let { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
 
   // Install this add-on, and verify that it appears in the about:debugging UI
-  yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
-                     "test-devtools");
+  yield installAddon({
+    document,
+    path: "addons/unpacked/install.rdf",
+    name: ADDON_NAME,
+  });
 
   // Install the add-on, and verify that it disappears in the about:debugging UI
-  yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
 
   yield closeAboutDebugging(tab);
 });
 
 add_task(function* () {
   let { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
 
--- a/devtools/client/aboutdebugging/test/browser_addons_reload.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_reload.js
@@ -104,18 +104,21 @@ class TempWebExt {
   remove() {
     return this.tmpDir.remove(true);
   }
 }
 
 add_task(function* reloadButtonReloadsAddon() {
   const { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
-  yield installAddon(document, "addons/unpacked/install.rdf",
-                     ADDON_NAME, ADDON_NAME);
+  yield installAddon({
+    document,
+    path: "addons/unpacked/install.rdf",
+    name: ADDON_NAME,
+  });
 
   const reloadButton = getReloadButton(document, ADDON_NAME);
   is(reloadButton.disabled, false, "Reload button should not be disabled");
   is(reloadButton.title, "", "Reload button should not have a tooltip");
   const onInstalled = promiseAddonEvent("onInstalled");
 
   const onBootstrapInstallCalled = new Promise(done => {
     Services.obs.addObserver(function listener() {
--- a/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
+++ b/devtools/client/aboutdebugging/test/browser_addons_toggle_debug.js
@@ -18,18 +18,21 @@ add_task(function* () {
     ]};
     SpecialPowers.pushPrefEnv(options, resolve);
   });
 
   let { tab, document } = yield openAboutDebugging("addons");
   yield waitForInitialAddonList(document);
 
   info("Install a test addon.");
-  yield installAddon(document, "addons/unpacked/install.rdf", ADDON_NAME,
-                     "test-devtools");
+  yield installAddon({
+    document,
+    path: "addons/unpacked/install.rdf",
+    name: ADDON_NAME,
+  });
 
   let addonDebugCheckbox = document.querySelector("#enable-addon-debugging");
   ok(!addonDebugCheckbox.checked, "Addons debugging should be disabled.");
 
   info("Check all debug buttons are disabled.");
   let debugButtons = [...document.querySelectorAll("#addons .debug-button")];
   ok(debugButtons.every(b => b.disabled), "Debug buttons should be disabled");
 
@@ -51,12 +54,12 @@ add_task(function* () {
   addonDebugCheckbox.click();
   yield onAddonsMutation;
 
   info("Check all debug buttons are disabled again.");
   debugButtons = [...document.querySelectorAll("#addons .debug-button")];
   ok(debugButtons.every(b => b.disabled), "Debug buttons should be disabled");
 
   info("Uninstall addon installed earlier.");
-  yield uninstallAddon(document, ADDON_ID, ADDON_NAME);
+  yield uninstallAddon({document, id: ADDON_ID, name: ADDON_NAME});
 
   yield closeAboutDebugging(tab);
 });
--- a/devtools/client/aboutdebugging/test/head.js
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -1,27 +1,29 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /* eslint-env browser */
 /* exported openAboutDebugging, changeAboutDebuggingHash, closeAboutDebugging,
    installAddon, uninstallAddon, waitForMutation, assertHasTarget,
    getServiceWorkerList, getTabList, openPanel, waitForInitialAddonList,
    waitForServiceWorkerRegistered, unregisterServiceWorker,
-   waitForDelayedStartupFinished */
+   waitForDelayedStartupFinished, setupTestAboutDebuggingWebExtension */
 /* import-globals-from ../../framework/test/shared-head.js */
 
 "use strict";
 
 // Load the shared-head file first.
 Services.scriptloader.loadSubScript(
   "chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js",
   this);
 
 const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+const { Management } = Cu.import("resource://gre/modules/Extension.jsm", {});
+
 DevToolsUtils.testing = true;
 
 registerCleanupFunction(() => {
   DevToolsUtils.testing = false;
 });
 
 function* openAboutDebugging(page, win) {
   info("opening about:debugging");
@@ -143,55 +145,70 @@ function getServiceWorkerList(document) 
  * @param  {DOMDocument}  document   #tabs section container document
  * @return {DOMNode}                 target list or container element
  */
 function getTabList(document) {
   return document.querySelector("#tabs .target-list") ||
     document.querySelector("#tabs.targets");
 }
 
-function* installAddon(document, path, name, evt) {
+function* installAddon({document, path, name, isWebExtension}) {
   // Mock the file picker to select a test addon
   let MockFilePicker = SpecialPowers.MockFilePicker;
   MockFilePicker.init(null);
   let file = getSupportsFile(path);
   MockFilePicker.returnFiles = [file.file];
 
   let addonList = getAddonList(document);
   let addonListMutation = waitForMutation(addonList, { childList: true });
 
-  // Wait for a message sent by the addon's bootstrap.js file
-  let onAddonInstalled = new Promise(done => {
-    Services.obs.addObserver(function listener() {
-      Services.obs.removeObserver(listener, evt);
+  let onAddonInstalled;
+
+  if (isWebExtension) {
+    onAddonInstalled = new Promise(done => {
+      Management.on("startup", function listener(event, extension) {
+        if (extension.name != name) {
+          return;
+        }
 
-      done();
-    }, evt, false);
-  });
+        Management.off("startup", listener);
+        done();
+      });
+    });
+  } else {
+    // Wait for a "test-devtools" message sent by the addon's bootstrap.js file
+    onAddonInstalled = new Promise(done => {
+      Services.obs.addObserver(function listener() {
+        Services.obs.removeObserver(listener, "test-devtools");
+
+        done();
+      }, "test-devtools", false);
+    });
+  }
   // Trigger the file picker by clicking on the button
   document.getElementById("load-addon-from-file").click();
 
   yield onAddonInstalled;
   ok(true, "Addon installed and running its bootstrap.js file");
 
   // Check that the addon appears in the UI
   yield addonListMutation;
   let names = [...addonList.querySelectorAll(".target-name")];
   names = names.map(element => element.textContent);
   ok(names.includes(name),
     "The addon name appears in the list of addons: " + names);
 }
 
-function* uninstallAddon(document, addonId, addonName) {
+function* uninstallAddon({document, id, name}) {
   let addonList = getAddonList(document);
   let addonListMutation = waitForMutation(addonList, { childList: true });
 
   // Now uninstall this addon
   yield new Promise(done => {
-    AddonManager.getAddonByID(addonId, addon => {
+    AddonManager.getAddonByID(id, addon => {
       let listener = {
         onUninstalled: function (uninstalledAddon) {
           if (uninstalledAddon != addon) {
             return;
           }
           AddonManager.removeAddonListener(listener);
 
           done();
@@ -201,17 +218,17 @@ function* uninstallAddon(document, addon
       addon.uninstall();
     });
   });
 
   // Ensure that the UI removes the addon from the list
   yield addonListMutation;
   let names = [...addonList.querySelectorAll(".target-name")];
   names = names.map(element => element.textContent);
-  ok(!names.includes(addonName),
+  ok(!names.includes(name),
     "After uninstall, the addon name disappears from the list of addons: "
     + names);
 }
 
 /**
  * Returns a promise that will resolve when the add-on list has been updated.
  *
  * @param {Node} document
@@ -307,8 +324,46 @@ function waitForDelayedStartupFinished(w
     Services.obs.addObserver(function observer(subject, topic) {
       if (win == subject) {
         Services.obs.removeObserver(observer, topic);
         resolve();
       }
     }, "browser-delayed-startup-finished", false);
   });
 }
+
+/**
+ * open the about:debugging page and install an addon
+ */
+function* setupTestAboutDebuggingWebExtension(name, path) {
+  yield new Promise(resolve => {
+    let options = {"set": [
+      // Force enabling of addons debugging
+      ["devtools.chrome.enabled", true],
+      ["devtools.debugger.remote-enabled", true],
+      // Disable security prompt
+      ["devtools.debugger.prompt-connection", false],
+      // Enable Browser toolbox test script execution via env variable
+      ["devtools.browser-toolbox.allow-unsafe-script", true],
+    ]};
+    SpecialPowers.pushPrefEnv(options, resolve);
+  });
+
+  let { tab, document } = yield openAboutDebugging("addons");
+  yield waitForInitialAddonList(document);
+
+  yield installAddon({
+    document,
+    path,
+    name,
+    isWebExtension: true,
+  });
+
+  // Retrieve the DEBUG button for the addon
+  let names = [...document.querySelectorAll("#addons .target-name")];
+  let nameEl = names.filter(element => element.textContent === name)[0];
+  ok(name, "Found the addon in the list");
+  let targetElement = nameEl.parentNode.parentNode;
+  let debugBtn = targetElement.querySelector(".debug-button");
+  ok(debugBtn, "Found its debug button");
+
+  return { tab, document, debugBtn };
+}
--- a/devtools/client/debugger/test/mochitest/browser.ini
+++ b/devtools/client/debugger/test/mochitest/browser.ini
@@ -474,17 +474,17 @@ skip-if = e10s && debug
 skip-if = e10s && debug
 [browser_dbg_sources-large.js]
 [browser_dbg_sources-sorting.js]
 skip-if = e10s && debug
 [browser_dbg_sources-bookmarklet.js]
 skip-if = e10s && debug
 [browser_dbg_sources-webext-contentscript.js]
 [browser_dbg_split-console-paused-reload.js]
-skip-if = e10s && debug
+skip-if = true # Bug 1288348 - previously e10s && debug
 [browser_dbg_stack-01.js]
 skip-if = e10s && debug
 [browser_dbg_stack-02.js]
 skip-if = e10s && debug
 [browser_dbg_stack-03.js]
 skip-if = e10s # TODO
 [browser_dbg_stack-04.js]
 skip-if = e10s && debug
--- a/devtools/client/debugger/test/mochitest/browser_dbg_split-console-paused-reload.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_split-console-paused-reload.js
@@ -32,17 +32,36 @@ function* runTests() {
   yield ensureThreadClientState(panel, "paused");
   info("Breakpoint was hit.");
   EventUtils.sendMouseEvent({ type: "mousedown" },
     frames.selectedItem.target,
     dbgWin);
   info("The breadcrumb received focus.");
 
   // This is the meat of the test.
-  let result = toolbox.once("webconsole-ready", () => {
-    ok(toolbox.splitConsole, "Split console is shown.");
-    is(dbgWin.gThreadClient.state, "paused", "Execution is still paused.");
-    Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
-  });
-  EventUtils.synthesizeKey("VK_ESCAPE", {}, dbgWin);
-  yield result;
-  yield resumeDebuggerThenCloseAndFinish(panel);
+  let jsterm = yield getSplitConsole(toolbox);
+
+  is(dbgWin.gThreadClient.state, "paused", "Execution is still paused.");
+
+  let dbgFrameConsoleEvalResult = yield jsterm.execute("privateVar");
+
+  is(
+    dbgFrameConsoleEvalResult.querySelector(".console-string").textContent,
+    '"privateVarValue"',
+    "Got the expected split console result on paused debugger"
+  );
+
+  yield dbgWin.gThreadClient.resume();
+
+  is(dbgWin.gThreadClient.state, "attached", "Execution is resumed.");
+
+  // Get the last evaluation result adopted by the new debugger.
+  let mainTargetConsoleEvalResult = yield jsterm.execute("$_");
+
+  is(
+    mainTargetConsoleEvalResult.querySelector(".console-string").textContent,
+    '"privateVarValue"',
+    "Got the expected split console log on $_ executed on resumed debugger"
+  );
+
+  Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+  yield closeDebuggerAndFinish(panel);
 }
--- a/devtools/client/debugger/test/mochitest/doc_split-console-paused-reload.html
+++ b/devtools/client/debugger/test/mochitest/doc_split-console-paused-reload.html
@@ -5,16 +5,18 @@
 <html>
   <head>
     <meta charset="utf-8"/>
     <title>Test page for opening a split-console when execution is paused</title>
   </head>
 
   <body>
     <script type="text/javascript">
-      function runDebuggerStatement() {
-        debugger;
+      function executeFunction() {
+        let privateVar = { propKey: "privateVarValue" };
+
+        window.foobar = "foobar";
       }
-      window.foobar = 1;
+      executeFunction();
     </script>
   </body>
 
 </html>
--- a/devtools/client/framework/attach-thread.js
+++ b/devtools/client/framework/attach-thread.js
@@ -84,26 +84,26 @@ function attachThread(toolbox) {
           box.PRIORITY_WARNING_HIGH
         );
       }
 
       deferred.resolve(threadClient);
     });
   };
 
-  if (target.isAddon) {
-    // Attaching an addon
+  if (target.isTabActor) {
+    // Attaching a tab, a browser process, or a WebExtensions add-on.
+    target.activeTab.attachThread(threadOptions, handleResponse);
+  } else if (target.isAddon) {
+    // Attaching a legacy addon.
     target.client.attachAddon(actor, res => {
       target.client.attachThread(res.threadActor, handleResponse);
     });
-  } else if (target.isTabActor) {
-    // Attaching a normal thread
-    target.activeTab.attachThread(threadOptions, handleResponse);
-  } else {
-    // Attaching the browser debugger
+  }  else {
+    // Attaching an old browser debugger or a content process.
     target.client.attachThread(chromeDebugger, handleResponse);
   }
 
   return deferred.promise;
 }
 
 function detachThread(threadClient) {
   threadClient.removeListener("paused");
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -343,18 +343,25 @@ TabTarget.prototype = {
     return this._url;
   },
 
   get isRemote() {
     return !this.isLocalTab;
   },
 
   get isAddon() {
+    return !!(this._form && this._form.actor && (
+      this._form.actor.match(/conn\d+\.addon\d+/) ||
+      this._form.actor.match(/conn\d+\.webExtension\d+/)
+    ));
+  },
+
+  get isWebExtension() {
     return !!(this._form && this._form.actor &&
-              this._form.actor.match(/conn\d+\.addon\d+/));
+              this._form.actor.match(/conn\d+\.webExtension\d+/));
   },
 
   get isLocalTab() {
     return !!this._tab;
   },
 
   get isMultiProcess() {
     return !this.window;
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -39,17 +39,21 @@ var connect = Task.async(function*() {
   });
   gClient = new DebuggerClient(transport);
   gClient.connect().then(() => {
     let addonID = getParameterByName("addonID");
 
     if (addonID) {
       gClient.listAddons(({addons}) => {
         let addonActor = addons.filter(addon => addon.id === addonID).pop();
-        openToolbox({ form: addonActor, chrome: true, isTabActor: false });
+        openToolbox({
+          form: addonActor,
+          chrome: true,
+          isTabActor: addonActor.isWebExtension ? true : false
+        });
       });
     } else {
       gClient.getProcess().then(aResponse => {
         openToolbox({ form: aResponse.form, chrome: true });
       });
     }
   });
 });
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -923,17 +923,17 @@ Toolbox.prototype = {
       Services.focus.moveFocus(win, elm, type, 0);
     });
   },
 
   /**
    * Add buttons to the UI as specified in the devtools.toolbox.toolbarSpec pref
    */
   _buildButtons: function () {
-    if (!this.target.isAddon) {
+    if (!this.target.isAddon || this.target.isWebExtension) {
       this._buildPickerButton();
     }
 
     this.setToolboxButtonsVisibility();
 
     // Old servers don't have a GCLI Actor, so just return
     if (!this.target.hasActor("gcli")) {
       return promise.resolve();
--- a/devtools/client/shared/components/reps/grip.js
+++ b/devtools/client/shared/components/reps/grip.js
@@ -17,17 +17,17 @@ define(function (require, exports, modul
   // Shortcuts
   const { span } = React.DOM;
 
   /**
    * Renders generic grip. Grip is client representation
    * of remote JS object and is used as an input object
    * for this rep component.
    */
-  const Grip = React.createClass({
+  const GripRep = React.createClass({
     displayName: "Grip",
 
     propTypes: {
       object: React.PropTypes.object.isRequired,
       mode: React.PropTypes.string,
     },
 
     getTitle: function (object) {
@@ -114,16 +114,17 @@ define(function (require, exports, modul
         let value = ownProperties[name].value;
         props.push(PropRep(Object.assign({}, this.props, {
           key: name,
           mode: "tiny",
           name: name,
           object: value,
           equal: ": ",
           delim: ", ",
+          defaultRep: Grip
         })));
       });
 
       return props;
     },
 
     /**
      * Get the indexes of props in the object.
@@ -204,14 +205,16 @@ define(function (require, exports, modul
   // Registration
   function supportsObject(object, type) {
     if (!isGrip(object)) {
       return false;
     }
     return (object.preview && object.preview.ownProperties);
   }
 
-  // Exports from this module
-  exports.Grip = {
-    rep: Grip,
+  let Grip = {
+    rep: GripRep,
     supportsObject: supportsObject
   };
+
+  // Exports from this module
+  exports.Grip = Grip;
 });
--- a/devtools/client/shared/components/sidebar-toggle.css
+++ b/devtools/client/shared/components/sidebar-toggle.css
@@ -2,23 +2,31 @@
 /* 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/. */
 
 .sidebar-toggle {
   display: block;
 }
 
-.sidebar-toggle::before {
+.sidebar-toggle:-moz-locale-dir(ltr)::before,
+.sidebar-toggle.pane-collapsed:-moz-locale-dir(rtl)::before {
   background-image: var(--theme-pane-collapse-image);
 }
 
-.sidebar-toggle.pane-collapsed::before {
+.sidebar-toggle.pane-collapsed:-moz-locale-dir(ltr)::before,
+.sidebar-toggle:-moz-locale-dir(rtl)::before {
   background-image: var(--theme-pane-expand-image);
 }
 
 /* Rotate button icon 90deg if the toolbox container is
   in vertical mode (sidebar displayed under the main panel) */
 @media (max-width: 700px) {
-  .sidebar-toggle::before {
+  .sidebar-toggle:-moz-locale-dir(ltr)::before {
     transform: rotate(90deg);
   }
+
+  /* Since RTL swaps the used images, we need to flip them
+     the other way round */
+  .sidebar-toggle:-moz-locale-dir(rtl)::before {
+    transform: rotate(-90deg);
+  }
 }
--- a/devtools/client/shared/components/tabs/tabs.css
+++ b/devtools/client/shared/components/tabs/tabs.css
@@ -11,20 +11,16 @@
 
 .tabs .tabs-menu {
   display: table;
   list-style: none;
   padding: 0;
   margin: 0;
 }
 
-.tabs .tabs-menu-item {
-  float: inline-start;
-}
-
 .tabs .tabs-menu-item a {
   display: block;
   color: #A9A9A9;
   padding: 4px 8px;
   border: 1px solid transparent;
   text-decoration: none;
   white-space: nowrap;
 }
@@ -45,17 +41,17 @@
 .theme-dark .tabs,
 .theme-light .tabs {
   background: var(--theme-tab-toolbar-background);
 }
 
 .theme-dark .tabs .tabs-navigation,
 .theme-light .tabs .tabs-navigation {
   border-bottom: 1px solid var(--theme-splitter-color);
-  font-size: 12px;
+  font-size: 11px;
 }
 
 .theme-firebug .tabs .tabs-navigation {
   font-size: 11px;
 }
 
 .theme-dark .tabs .tabs-menu-item,
 .theme-light .tabs .tabs-menu-item {
--- a/devtools/client/shared/widgets/Tooltip.js
+++ b/devtools/client/shared/widgets/Tooltip.js
@@ -845,31 +845,36 @@ Heritage.extend(SwatchBasedEditorTooltip
   _openEyeDropper: function () {
     let {inspector, toolbox, telemetry} = this.inspector;
     telemetry.toolOpened("pickereyedropper");
     inspector.pickColorFromPage({copyOnSelect: false}).catch(e => console.error(e));
 
     inspector.once("color-picked", color => {
       toolbox.win.focus();
       this._selectColor(color);
+      this._onEyeDropperDone();
     });
 
     inspector.once("color-pick-canceled", () => {
-      this.eyedropperOpen = false;
-      this.activeSwatch = null;
+      this._onEyeDropperDone();
     });
 
     this.eyedropperOpen = true;
 
     // close the colorpicker tooltip so that only the eyedropper is open.
     this.hide();
 
     this.tooltip.emit("eyedropper-opened");
   },
 
+  _onEyeDropperDone: function () {
+    this.eyedropperOpen = false;
+    this.activeSwatch = null;
+  },
+
   _colorToRgba: function (color) {
     color = new colorUtils.CssColor(color);
     let rgba = color._getRGBATuple();
     return [rgba.r, rgba.g, rgba.b, rgba.a];
   },
 
   _toDefaultType: function (color) {
     let colorObj = new colorUtils.CssColor(color);
--- a/devtools/server/actors/addon.js
+++ b/devtools/server/actors/addon.js
@@ -129,16 +129,19 @@ BrowserAddonActor.prototype = {
 
   onUninstalled: function BAA_onUninstalled(aAddon) {
     if (aAddon != this._addon) {
       return;
     }
 
     if (this.attached) {
       this.onDetach();
+
+      // The BrowserAddonActor is not a TabActor and it has to send
+      // "tabDetached" directly to close the devtools toolbox window.
       this.conn.send({ from: this.actorID, type: "tabDetached" });
     }
 
     this.disconnect();
   },
 
   onAttach: function BAA_onAttach() {
     if (this.exited) {
--- a/devtools/server/actors/moz.build
+++ b/devtools/server/actors/moz.build
@@ -57,11 +57,12 @@ DevToolsModules(
     'styleeditor.js',
     'styles.js',
     'stylesheets.js',
     'timeline.js',
     'webapps.js',
     'webaudio.js',
     'webbrowser.js',
     'webconsole.js',
+    'webextension.js',
     'webgl.js',
     'worker.js',
 )
--- a/devtools/server/actors/webbrowser.js
+++ b/devtools/server/actors/webbrowser.js
@@ -1,16 +1,18 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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";
 
+/* global XPCNativeWrapper */
+
 var { Ci, Cu } = require("chrome");
 var Services = require("Services");
 var { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
 var promise = require("promise");
 var {
   ActorPool, createExtraActors, appendExtraActors, GeneratedLocation
 } = require("devtools/server/actors/common");
 var { DebuggerServer } = require("devtools/server/main");
@@ -18,16 +20,17 @@ var DevToolsUtils = require("devtools/sh
 var { assert } = DevToolsUtils;
 var { TabSources } = require("./utils/TabSources");
 var makeDebugger = require("./utils/make-debugger");
 
 loader.lazyRequireGetter(this, "RootActor", "devtools/server/actors/root", true);
 loader.lazyRequireGetter(this, "ThreadActor", "devtools/server/actors/script", true);
 loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
 loader.lazyRequireGetter(this, "BrowserAddonActor", "devtools/server/actors/addon", true);
+loader.lazyRequireGetter(this, "WebExtensionActor", "devtools/server/actors/webextension", true);
 loader.lazyRequireGetter(this, "WorkerActorList", "devtools/server/actors/worker", true);
 loader.lazyRequireGetter(this, "ServiceWorkerRegistrationActorList", "devtools/server/actors/worker", true);
 loader.lazyRequireGetter(this, "ProcessActorList", "devtools/server/actors/process", true);
 loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
 loader.lazyImporter(this, "ExtensionContent", "resource://gre/modules/ExtensionContent.jsm");
 
 // Assumptions on events module:
 // events needs to be dispatched synchronously,
@@ -553,44 +556,46 @@ BrowserTabList.prototype._listenForEvent
  * @param aShouldListen boolean
  *    True if we should add message listeners; false if we should remove them.
  * @param aGuard string
  *    The name of a guard property of 'this', indicating whether we're
  *    already listening for those messages.
  * @param aMessageNames array of strings
  *    An array of message names.
  */
-BrowserTabList.prototype._listenForMessagesIf = function (aShouldListen, aGuard, aMessageNames) {
-  if (!aShouldListen !== !this[aGuard]) {
-    let op = aShouldListen ? "addMessageListener" : "removeMessageListener";
-    for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
-      for (let name of aMessageNames) {
-        win.messageManager[op](name, this);
+BrowserTabList.prototype._listenForMessagesIf =
+  function (shouldListen, guard, messageNames) {
+    if (!shouldListen !== !this[guard]) {
+      let op = shouldListen ? "addMessageListener" : "removeMessageListener";
+      for (let win of allAppShellDOMWindows(DebuggerServer.chromeWindowType)) {
+        for (let name of messageNames) {
+          win.messageManager[op](name, this);
+        }
       }
+      this[guard] = shouldListen;
     }
-    this[aGuard] = aShouldListen;
-  }
-};
+  };
 
 /**
  * Implement nsIMessageListener.
  */
-BrowserTabList.prototype.receiveMessage = DevToolsUtils.makeInfallible(function (message) {
-  let browser = message.target;
-  switch (message.name) {
-    case "DOMTitleChanged": {
-      let actor = this._actorByBrowser.get(browser);
-      if (actor) {
-        this._notifyListChanged();
-        this._checkListening();
+BrowserTabList.prototype.receiveMessage = DevToolsUtils.makeInfallible(
+  function (message) {
+    let browser = message.target;
+    switch (message.name) {
+      case "DOMTitleChanged": {
+        let actor = this._actorByBrowser.get(browser);
+        if (actor) {
+          this._notifyListChanged();
+          this._checkListening();
+        }
+        break;
       }
-      break;
     }
-  }
-});
+  });
 
 /**
  * Implement nsIDOMEventListener.
  */
 BrowserTabList.prototype.handleEvent =
 DevToolsUtils.makeInfallible(function (event) {
   let browser = event.target.linkedBrowser;
   switch (event.type) {
@@ -884,16 +889,26 @@ function TabActor(connection) {
 }
 
 // XXX (bug 710213): TabActor attach/detach/exit/disconnect is a
 // *complete* mess, needs to be rethought asap.
 
 TabActor.prototype = {
   traits: null,
 
+  // Optional console API listener options (e.g. used by the WebExtensionActor to
+  // filter console messages by addonID), set to an empty (no options) object by default.
+  consoleAPIListenerOptions: {},
+
+  // Optional TabSources filter function (e.g. used by the WebExtensionActor to filter
+  // sources by addonID), allow all sources by default.
+  _allowSource() {
+    return true;
+  },
+
   get exited() {
     return this._exited;
   },
 
   get attached() {
     return !!this._attached;
   },
 
@@ -1054,17 +1069,17 @@ TabActor.prototype = {
     }
     // Abrupt closing of the browser window may leave callbacks without a
     // currentURI.
     return null;
   },
 
   get sources() {
     if (!this._sources) {
-      this._sources = new TabSources(this.threadActor);
+      this._sources = new TabSources(this.threadActor, this._allowSource);
     }
     return this._sources;
   },
 
   /**
    * This is called by BrowserTabList.getList for existing tab actors prior to
    * calling |form| below.  It can be used to do any async work that may be
    * needed to assemble the form.
@@ -1359,27 +1374,38 @@ TabActor.prototype = {
       // Ignore the parent of the original document on non-e10s firefox,
       // as we get the xul window as parent and don't care about it.
       if (window.parent && window != this._originalWindow) {
         parentID = window.parent
                          .QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIDOMWindowUtils)
                          .outerWindowID;
       }
+
+      // Collect the addonID from the document origin attributes.
+      let addonID = window.document.nodePrincipal.originAttributes.addonId;
+
       return {
-        id: id,
+        id,
+        parentID,
+        addonID,
         url: window.location.href,
         title: window.document.title,
-        parentID: parentID
       };
     });
   },
 
   _notifyDocShellsUpdate(docshells) {
     let windows = this._docShellsToWindows(docshells);
+
+    // Do not send the `frameUpdate` event if the windows array is empty.
+    if (windows.length == 0) {
+      return;
+    }
+
     this.conn.send({ from: this.actorID,
                      type: "frameUpdate",
                      frames: windows
                    });
   },
 
   _updateChildDocShells() {
     this._notifyDocShellsUpdate(this.docShells);
@@ -2022,17 +2048,17 @@ TabActor.prototype = {
    *         True if the window.console object is native, or false otherwise.
    */
   hasNativeConsoleAPI(window) {
     let isNative = false;
     try {
       // We are very explicitly examining the "console" property of
       // the non-Xrayed object here.
       let console = window.wrappedJSObject.console;
-      isNative = new XPCNativeWrapper(console).IS_NATIVE_CONSOLE
+      isNative = new XPCNativeWrapper(console).IS_NATIVE_CONSOLE;
     } catch (ex) {
       // ignore
     }
     return isNative;
   },
 
   /**
    * Create or return the StyleSheetActor for a style sheet. This method
@@ -2262,17 +2288,22 @@ function BrowserAddonList(connection) {
 }
 
 BrowserAddonList.prototype.getList = function () {
   let deferred = promise.defer();
   AddonManager.getAllAddons((addons) => {
     for (let addon of addons) {
       let actor = this._actorByAddonId.get(addon.id);
       if (!actor) {
-        actor = new BrowserAddonActor(this._connection, addon);
+        if (addon.isWebExtension) {
+          actor = new WebExtensionActor(this._connection, addon);
+        } else {
+          actor = new BrowserAddonActor(this._connection, addon);
+        }
+
         this._actorByAddonId.set(addon.id, actor);
       }
     }
     deferred.resolve([...this._actorByAddonId].map(([_, actor]) => actor));
   });
   return deferred.promise;
 };
 
@@ -2309,22 +2340,20 @@ BrowserAddonList.prototype._notifyListCh
   }
 };
 
 BrowserAddonList.prototype._adjustListener = function () {
   if (this._onListChanged) {
     // As long as the callback exists, we need to listen for changes
     // so we can notify about add-on changes.
     AddonManager.addAddonListener(this);
-  } else {
+  } else if (this._actorByAddonId.size === 0) {
     // When the callback does not exist, we only need to keep listening
     // if the actor cache will need adjusting when add-ons change.
-    if (this._actorByAddonId.size === 0) {
-      AddonManager.removeAddonListener(this);
-    }
+    AddonManager.removeAddonListener(this);
   }
 };
 
 exports.BrowserAddonList = BrowserAddonList;
 
 /**
  * The DebuggerProgressListener object is an nsIWebProgressListener which
  * handles onStateChange events for the inspected browser. If the user tries to
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -589,18 +589,21 @@ WebConsoleActor.prototype =
             this.consoleServiceListener =
               new ConsoleServiceListener(window, this);
             this.consoleServiceListener.init();
           }
           startedListeners.push(listener);
           break;
         case "ConsoleAPI":
           if (!this.consoleAPIListener) {
+            // Create the consoleAPIListener (and apply the filtering options defined
+            // in the parent actor).
             this.consoleAPIListener =
-              new ConsoleAPIListener(window, this);
+              new ConsoleAPIListener(window, this,
+                                     this.parentActor.consoleAPIListenerOptions);
             this.consoleAPIListener.init();
           }
           startedListeners.push(listener);
           break;
         case "NetworkActivity":
           if (!this.networkMonitor) {
             // Create a StackTraceCollector that's going to be shared both by the
             // NetworkMonitorChild (getting messages about requests from parent) and
@@ -1291,17 +1294,29 @@ WebConsoleActor.prototype =
     // Ready to evaluate the string.
     helpers.evalInput = aString;
 
     let evalOptions;
     if (typeof aOptions.url == "string") {
       evalOptions = { url: aOptions.url };
     }
 
+    // If the debugger object is changed from the last evaluation,
+    // adopt this._lastConsoleInputEvaluation value in the new debugger,
+    // to prevents "Debugger.Object belongs to a different Debugger" exceptions
+    // related to the $_ bindings.
+    if (this._lastConsoleInputEvaluation &&
+        this._lastConsoleInputEvaluation.global !== dbgWindow) {
+      this._lastConsoleInputEvaluation = dbg.adoptDebuggeeValue(
+        this._lastConsoleInputEvaluation
+      );
+    }
+
     let result;
+
     if (frame) {
       result = frame.evalWithBindings(aString, bindings, evalOptions);
     }
     else {
       result = dbgWindow.executeInGlobalWithBindings(aString, bindings, evalOptions);
       // Attempt to initialize any declarations found in the evaluated string
       // since they may now be stuck in an "initializing" state due to the
       // error. Already-initialized bindings will be ignored.
copy from devtools/server/actors/chrome.js
copy to devtools/server/actors/webextension.js
--- a/devtools/server/actors/chrome.js
+++ b/devtools/server/actors/webextension.js
@@ -1,185 +1,333 @@
 /* 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 { Ci } = require("chrome");
+const { Ci, Cu } = require("chrome");
 const Services = require("Services");
-const { DebuggerServer } = require("../main");
-const { getChildDocShells, TabActor } = require("./webbrowser");
+const { ChromeActor } = require("./chrome");
 const makeDebugger = require("./utils/make-debugger");
 
+var DevToolsUtils = require("devtools/shared/DevToolsUtils");
+var { assert } = DevToolsUtils;
+
+loader.lazyRequireGetter(this, "mapURIToAddonID", "devtools/server/actors/utils/map-uri-to-addon-id");
+loader.lazyRequireGetter(this, "unwrapDebuggerObjectGlobal", "devtools/server/actors/script", true);
+
+loader.lazyImporter(this, "AddonManager", "resource://gre/modules/AddonManager.jsm");
+loader.lazyImporter(this, "XPIProvider", "resource://gre/modules/addons/XPIProvider.jsm");
+
+const FALLBACK_DOC_MESSAGE = "Your addon does not have any document opened yet.";
+
 /**
- * Creates a TabActor for debugging all the chrome content in the
- * current process. Most of the implementation is inherited from TabActor.
- * ChromeActor is a child of RootActor, it can be instanciated via
- * RootActor.getProcess request.
- * ChromeActor exposes all tab actors via its form() request, like TabActor.
+ * Creates a TabActor for debugging all the contexts associated to a target WebExtensions
+ * add-on.
+ * Most of the implementation is inherited from ChromeActor (which inherits most of its
+ * implementation from TabActor).
+ * WebExtensionActor is a child of RootActor, it can be retrieved via
+ * RootActor.listAddons request.
+ * WebExtensionActor exposes all tab actors via its form() request, like TabActor.
  *
  * History lecture:
- * All tab actors used to also be registered as global actors,
- * so that the root actor was also exposing tab actors for the main process.
- * Tab actors ended up having RootActor as parent actor,
- * but more and more features of the tab actors were relying on TabActor.
- * So we are now exposing a process actor that offers the same API as TabActor
- * by inheriting its functionality.
- * Global actors are now only the actors that are meant to be global,
- * and are no longer related to any specific scope/document.
+ * The add-on actors used to not inherit TabActor because of the different way the
+ * add-on APIs where exposed to the add-on itself, and for this reason the Addon Debugger
+ * has only a sub-set of the feature available in the Tab or in the Browser Toolbox.
+ * In a WebExtensions add-on all the provided contexts (background and popup pages etc.),
+ * besides the Content Scripts which run in the content process, hooked to an existent
+ * tab, by creating a new WebExtensionActor which inherits from ChromeActor, we can
+ * provide a full features Addon Toolbox (which is basically like a BrowserToolbox which
+ * filters the visible sources and frames to the one that are related to the target
+ * add-on).
  *
- * @param aConnection DebuggerServerConnection
+ * @param conn DebuggerServerConnection
  *        The connection to the client.
+ * @param addon AddonWrapper
+ *        The target addon.
  */
-function ChromeActor(aConnection) {
-  TabActor.call(this, aConnection);
+function WebExtensionActor(conn, addon) {
+  ChromeActor.call(this, conn);
+
+  this.id = addon.id;
+  this.addon = addon;
+
+  // Bind the _allowSource helper to this, it is used in the
+  // TabActor to lazily create the TabSources instance.
+  this._allowSource = this._allowSource.bind(this);
 
-  // This creates a Debugger instance for chrome debugging all globals.
+  // Set the consoleAPIListener filtering options
+  // (retrieved and used in the related webconsole child actor).
+  this.consoleAPIListenerOptions = {
+    addonId: addon.id,
+  };
+
+  // This creates a Debugger instance for debugging all the add-on globals.
   this.makeDebugger = makeDebugger.bind(null, {
-    findDebuggees: dbg => dbg.findAllGlobals(),
-    shouldAddNewGlobalAsDebuggee: () => true
+    findDebuggees: dbg => {
+      return dbg.findAllGlobals().filter(this._shouldAddNewGlobalAsDebuggee);
+    },
+    shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee.bind(this),
   });
 
-  // Ensure catching the creation of any new content docshell
-  this.listenForNewDocShells = true;
+  // Discover the preferred debug global for the target addon
+  this.preferredTargetWindow = null;
+  this._findAddonPreferredTargetWindow();
+
+  AddonManager.addAddonListener(this);
+}
+exports.WebExtensionActor = WebExtensionActor;
+
+WebExtensionActor.prototype = Object.create(ChromeActor.prototype);
+
+WebExtensionActor.prototype.actorPrefix = "webExtension";
+WebExtensionActor.prototype.constructor = WebExtensionActor;
+
+// NOTE: This is needed to catch in the webextension webconsole all the
+// errors raised by the WebExtension internals that are not currently
+// associated with any window.
+WebExtensionActor.prototype.isRootActor = true;
+
+WebExtensionActor.prototype.form = function () {
+  assert(this.actorID, "addon should have an actorID.");
+
+  let baseForm = ChromeActor.prototype.form.call(this);
 
-  // Defines the default docshell selected for the tab actor
-  let window = Services.wm.getMostRecentWindow(DebuggerServer.chromeWindowType);
+  return Object.assign(baseForm, {
+    actor: this.actorID,
+    id: this.id,
+    name: this.addon.name,
+    url: this.addon.sourceURI ? this.addon.sourceURI.spec : undefined,
+    iconURL: this.addon.iconURL,
+    debuggable: this.addon.isDebuggable,
+    temporarilyInstalled: this.addon.temporarilyInstalled,
+    isWebExtension: this.addon.isWebExtension,
+  });
+};
 
-  // Default to any available top level window if there is no expected window
-  // (for example when we open firefox with -webide argument)
-  if (!window) {
-    window = Services.wm.getMostRecentWindow(null);
+WebExtensionActor.prototype._attach = function () {
+  // NOTE: we need to be sure that `this.window` can return a
+  // window before calling the ChromeActor.onAttach, or the TabActor
+  // will not be subscribed to the child doc shell updates.
+
+  // If a preferredTargetWindow exists, set it as the target for this actor
+  // when the client request to attach this actor.
+  if (this.preferredTargetWindow) {
+    this._setWindow(this.preferredTargetWindow);
+  } else {
+    this._createFallbackWindow();
   }
-  // On xpcshell, there is no window/docshell
-  let docShell = window ? window.QueryInterface(Ci.nsIInterfaceRequestor)
-                                .getInterface(Ci.nsIDocShell)
-                        : null;
-  Object.defineProperty(this, "docShell", {
-    value: docShell,
-    configurable: true
-  });
-}
-exports.ChromeActor = ChromeActor;
+
+  // Call ChromeActor's _attach to listen for any new/destroyed chrome docshell
+  ChromeActor.prototype._attach.apply(this);
+};
 
-ChromeActor.prototype = Object.create(TabActor.prototype);
+WebExtensionActor.prototype._detach = function () {
+  this._destroyFallbackWindow();
 
-ChromeActor.prototype.constructor = ChromeActor;
-
-ChromeActor.prototype.isRootActor = true;
+  // Call ChromeActor's _detach to unsubscribe new/destroyed chrome docshell listeners.
+  ChromeActor.prototype._detach.apply(this);
+};
 
 /**
- * Getter for the list of all docshells in this tabActor
- * @return {Array}
+ * Called when the actor is removed from the connection.
+ */
+WebExtensionActor.prototype.exit = function () {
+  AddonManager.removeAddonListener(this);
+
+  this.preferredTargetWindow = null;
+  this.addon = null;
+  this.id = null;
+
+  return ChromeActor.prototype.exit.apply(this);
+};
+
+// Addon Specific Remote Debugging requestTypes and methods.
+
+/**
+ * Reloads the addon.
  */
-Object.defineProperty(ChromeActor.prototype, "docShells", {
-  get: function () {
-    // Iterate over all top-level windows and all their docshells.
-    let docShells = [];
-    let e = Services.ww.getWindowEnumerator();
-    while (e.hasMoreElements()) {
-      let window = e.getNext();
-      let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                           .getInterface(Ci.nsIWebNavigation)
-                           .QueryInterface(Ci.nsIDocShell);
-      docShells = docShells.concat(getChildDocShells(docShell));
-    }
+WebExtensionActor.prototype.onReload = function () {
+  return this.addon.reload()
+    .then(() => {
+      // send an empty response
+      return {};
+    });
+};
 
-    return docShells;
-  }
-});
-
-ChromeActor.prototype.observe = function (aSubject, aTopic, aData) {
-  TabActor.prototype.observe.call(this, aSubject, aTopic, aData);
-  if (!this.attached) {
-    return;
-  }
-  if (aTopic == "chrome-webnavigation-create") {
-    aSubject.QueryInterface(Ci.nsIDocShell);
-    this._onDocShellCreated(aSubject);
-  } else if (aTopic == "chrome-webnavigation-destroy") {
-    this._onDocShellDestroy(aSubject);
+/**
+ * Set the preferred global for the add-on (called from the AddonManager).
+ */
+WebExtensionActor.prototype.setOptions = function (addonOptions) {
+  if ("global" in addonOptions) {
+    // Set the proposed debug global as the preferred target window
+    // (the actor will eventually set it as the target once it is attached)
+    this.preferredTargetWindow = addonOptions.global;
   }
 };
 
-ChromeActor.prototype._attach = function () {
-  if (this.attached) {
-    return false;
+// AddonManagerListener callbacks.
+
+WebExtensionActor.prototype.onInstalled = function (addon) {
+  if (addon.id != this.id) {
+    return;
   }
 
-  TabActor.prototype._attach.call(this);
+  // Update the AddonManager's addon object on reload/update.
+  this.addon = addon;
+};
 
-  // Listen for any new/destroyed chrome docshell
-  Services.obs.addObserver(this, "chrome-webnavigation-create", false);
-  Services.obs.addObserver(this, "chrome-webnavigation-destroy", false);
+WebExtensionActor.prototype.onUninstalled = function (addon) {
+  if (addon != this.addon) {
+    return;
+  }
 
-  // Iterate over all top-level windows.
-  let docShells = [];
-  let e = Services.ww.getWindowEnumerator();
-  while (e.hasMoreElements()) {
-    let window = e.getNext();
-    let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIWebNavigation)
-                         .QueryInterface(Ci.nsIDocShell);
-    if (docShell == this.docShell) {
-      continue;
-    }
-    this._progressListener.watch(docShell);
+  this.exit();
+};
+
+WebExtensionActor.prototype.onPropertyChanged = function (addon, changedPropNames) {
+  if (addon != this.addon) {
+    return;
+  }
+
+  // Refresh the preferred debug global on disabled/reloaded/upgraded addon.
+  if (changedPropNames.includes("debugGlobal")) {
+    this._findAddonPreferredTargetWindow();
   }
 };
 
-ChromeActor.prototype._detach = function () {
-  if (!this.attached) {
-    return false;
+// Private helpers
+
+WebExtensionActor.prototype._createFallbackWindow = function () {
+  if (this.fallbackWindow) {
+    // Skip if there is already an existent fallback window.
+    return;
   }
 
-  Services.obs.removeObserver(this, "chrome-webnavigation-create");
-  Services.obs.removeObserver(this, "chrome-webnavigation-destroy");
+  // Create an empty hidden window as a fallback (e.g. the background page could be
+  // not defined for the target add-on or not yet when the actor instance has been
+  // created).
+  this.fallbackWebNav = Services.appShell.createWindowlessBrowser(true);
+  this.fallbackWebNav.loadURI(
+    `data:text/html;charset=utf-8,${FALLBACK_DOC_MESSAGE}`,
+    0, null, null, null
+  );
 
-  // Iterate over all top-level windows.
-  let docShells = [];
-  let e = Services.ww.getWindowEnumerator();
-  while (e.hasMoreElements()) {
-    let window = e.getNext();
-    let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIWebNavigation)
-                         .QueryInterface(Ci.nsIDocShell);
-    if (docShell == this.docShell) {
-      continue;
-    }
-    this._progressListener.unwatch(docShell);
-  }
+  this.fallbackDocShell = this.fallbackWebNav
+    .QueryInterface(Ci.nsIInterfaceRequestor)
+    .getInterface(Ci.nsIDocShell);
 
-  TabActor.prototype._detach.call(this);
+  Object.defineProperty(this, "docShell", {
+    value: this.fallbackDocShell,
+    configurable: true
+  });
+
+  // Save the reference to the fallback DOMWindow
+  this.fallbackWindow = this.fallbackDocShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                                             .getInterface(Ci.nsIDOMWindow);
 };
 
-/* ThreadActor hooks. */
+WebExtensionActor.prototype._destroyFallbackWindow = function () {
+  if (this.fallbackWebNav) {
+    // Explicitly close the fallback windowless browser to prevent it to leak
+    // (and to prevent it to freeze devtools xpcshell tests).
+    this.fallbackWebNav.loadURI("about:blank", 0, null, null, null);
+    this.fallbackWebNav.close();
 
-/**
- * Prepare to enter a nested event loop by disabling debuggee events.
- */
-ChromeActor.prototype.preNest = function () {
-  // Disable events in all open windows.
-  let e = Services.wm.getEnumerator(null);
-  while (e.hasMoreElements()) {
-    let win = e.getNext();
-    let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindowUtils);
-    windowUtils.suppressEventHandling(true);
-    windowUtils.suspendTimeouts();
+    this.fallbackWebNav = null;
+    this.fallbackWindow = null;
   }
 };
 
 /**
- * Prepare to exit a nested event loop by enabling debuggee events.
+ * Discover the preferred debug global and switch to it if the addon has been attached.
  */
-ChromeActor.prototype.postNest = function (aNestData) {
-  // Enable events in all open windows.
-  let e = Services.wm.getEnumerator(null);
-  while (e.hasMoreElements()) {
-    let win = e.getNext();
-    let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
-                         .getInterface(Ci.nsIDOMWindowUtils);
-    windowUtils.resumeTimeouts();
-    windowUtils.suppressEventHandling(false);
+WebExtensionActor.prototype._findAddonPreferredTargetWindow = function () {
+  return new Promise(resolve => {
+    let activeAddon = XPIProvider.activeAddons.get(this.id);
+
+    if (!activeAddon) {
+      // The addon is not active, the background page is going to be destroyed,
+      // navigate to the fallback window (if it already exists).
+      resolve(null);
+    } else {
+      AddonManager.getAddonByInstanceID(activeAddon.instanceID)
+        .then(privateWrapper => {
+          let targetWindow = privateWrapper.getDebugGlobal();
+
+          // Do not use the preferred global if it is not a DOMWindow as expected.
+          if (!(targetWindow instanceof Ci.nsIDOMWindow)) {
+            targetWindow = null;
+          }
+
+          resolve(targetWindow);
+        });
+    }
+  }).then(preferredTargetWindow => {
+    this.preferredTargetWindow = preferredTargetWindow;
+
+    if (!preferredTargetWindow) {
+      // Create a fallback window if no preferred target window has been found.
+      this._createFallbackWindow();
+    } else if (this.attached) {
+      // Change the top level document if the actor is already attached.
+      this._changeTopLevelDocument(preferredTargetWindow);
+    }
+  });
+};
+
+/**
+ * Return an array of the json details related to an array/iterator of docShells.
+ */
+WebExtensionActor.prototype._docShellsToWindows = function (docshells) {
+  return ChromeActor.prototype._docShellsToWindows.call(this, docshells)
+                    .filter(windowDetails => {
+                      // filter the docShells based on the addon id
+                      return windowDetails.addonID == this.id;
+                    });
+};
+
+/**
+ * Return true if the given source is associated with this addon and should be
+ * added to the visible sources (retrieved and used by the webbrowser actor module).
+ */
+WebExtensionActor.prototype._allowSource = function (source) {
+  try {
+    let uri = Services.io.newURI(source.url, null, null);
+    let addonID = mapURIToAddonID(uri);
+
+    return addonID == this.id;
+  } catch (e) {
+    return false;
   }
 };
+
+/**
+ * Return true if the given global is associated with this addon and should be
+ * added as a debuggee, false otherwise.
+ */
+WebExtensionActor.prototype._shouldAddNewGlobalAsDebuggee = function (newGlobal) {
+  const global = unwrapDebuggerObjectGlobal(newGlobal);
+
+  if (global instanceof Ci.nsIDOMWindow) {
+    return global.document.nodePrincipal.originAttributes.addonId == this.id;
+  }
+
+  try {
+    // This will fail for non-Sandbox objects, hence the try-catch block.
+    let metadata = Cu.getSandboxMetadata(global);
+    if (metadata) {
+      return metadata.addonID === this.id;
+    }
+  } catch (e) {
+    // Unable to retrieve the sandbox metadata.
+  }
+
+  return false;
+};
+
+/**
+ * Override WebExtensionActor requestTypes:
+ * - redefined `reload`, which should reload the target addon
+ *   (instead of the entire browser as the regular ChromeActor does).
+ */
+WebExtensionActor.prototype.requestTypes.reload = WebExtensionActor.prototype.onReload;
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -3078,76 +3078,110 @@ HTMLMediaElement::ReportTelemetry()
     if (stalled) {
       state = STALLED;
     }
   }
 
   Telemetry::Accumulate(Telemetry::VIDEO_UNLOAD_STATE, state);
   LOG(LogLevel::Debug, ("%p VIDEO_UNLOAD_STATE = %d", this, state));
 
+  FrameStatisticsData data;
+
   if (HTMLVideoElement* vid = HTMLVideoElement::FromContentOrNull(this)) {
-    RefPtr<VideoPlaybackQuality> quality = vid->GetVideoPlaybackQuality();
-    uint32_t totalFrames = quality->TotalVideoFrames();
-    if (totalFrames) {
-      uint32_t droppedFrames = quality->DroppedVideoFrames();
-      MOZ_ASSERT(droppedFrames <= totalFrames);
-      // Dropped frames <= total frames, so 'percentage' cannot be higher than
-      // 100 and therefore can fit in a uint32_t (that Telemetry takes).
-      uint32_t percentage = 100 * droppedFrames / totalFrames;
-      LOG(LogLevel::Debug,
-          ("Reporting telemetry DROPPED_FRAMES_IN_VIDEO_PLAYBACK"));
-      Telemetry::Accumulate(Telemetry::VIDEO_DROPPED_FRAMES_PROPORTION,
-                            percentage);
-    }
-  }
-
-  double playTime = mPlayTime.Total();
-  double hiddenPlayTime = mHiddenPlayTime.Total();
-
-  Telemetry::Accumulate(Telemetry::VIDEO_PLAY_TIME_MS, SECONDS_TO_MS(playTime));
-  LOG(LogLevel::Debug, ("%p VIDEO_PLAY_TIME_MS = %f", this, playTime));
-
-  Telemetry::Accumulate(Telemetry::VIDEO_HIDDEN_PLAY_TIME_MS, SECONDS_TO_MS(hiddenPlayTime));
-  LOG(LogLevel::Debug, ("%p VIDEO_HIDDEN_PLAY_TIME_MS = %f", this, hiddenPlayTime));
-
-  if (playTime > 0.0 &&
-      mMediaInfo.HasVideo() &&
-      mMediaInfo.mVideo.mImage.height > 0) {
-    // We have actually played some valid video -> Report hidden/total ratio.
-    uint32_t hiddenPercentage = uint32_t(hiddenPlayTime / playTime * 100.0 + 0.5);
-
-    // Keyed by audio+video or video alone, and by a resolution range.
-    nsCString key(mMediaInfo.HasAudio() ? "AV," : "V,");
-    static const struct { int32_t mH; const char* mRes; } sResolutions[] = {
-      {  240, "0<h<=240" },
-      {  480, "240<h<=480" },
-      {  576, "480<h<=576" },
-      {  720, "576<h<=720" },
-      { 1080, "720<h<=1080" },
-      { 2160, "1080<h<=2160" }
-    };
-    const char* resolution = "h>2160";
-    int32_t height = mMediaInfo.mVideo.mImage.height;
-    for (const auto& res : sResolutions) {
-      if (height <= res.mH) {
-        resolution = res.mRes;
-        break;
+    FrameStatistics* stats = vid->GetFrameStatistics();
+    if (stats) {
+      data = stats->GetFrameStatisticsData();
+      if (data.mParsedFrames) {
+        MOZ_ASSERT(data.mDroppedFrames <= data.mParsedFrames);
+        // Dropped frames <= total frames, so 'percentage' cannot be higher than
+        // 100 and therefore can fit in a uint32_t (that Telemetry takes).
+        uint32_t percentage = 100 * data.mDroppedFrames / data.mParsedFrames;
+        LOG(LogLevel::Debug,
+            ("Reporting telemetry DROPPED_FRAMES_IN_VIDEO_PLAYBACK"));
+        Telemetry::Accumulate(Telemetry::VIDEO_DROPPED_FRAMES_PROPORTION,
+                              percentage);
       }
     }
-    key.AppendASCII(resolution);
-
-    Telemetry::Accumulate(Telemetry::VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE,
-                          key,
-                          hiddenPercentage);
-    // Also accumulate all percentages in an "All" key.
-    Telemetry::Accumulate(Telemetry::VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE,
-                          NS_LITERAL_CSTRING("All"),
-                          hiddenPercentage);
-    LOG(LogLevel::Debug, ("%p VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE = %u, keys: '%s' and 'All'",
-                          this, hiddenPercentage, key.get()));
+  }
+
+  if (mMediaInfo.HasVideo() &&
+      mMediaInfo.mVideo.mImage.height > 0) {
+    // We have a valid video.
+    double playTime = mPlayTime.Total();
+    double hiddenPlayTime = mHiddenPlayTime.Total();
+
+    Telemetry::Accumulate(Telemetry::VIDEO_PLAY_TIME_MS, SECONDS_TO_MS(playTime));
+    LOG(LogLevel::Debug, ("%p VIDEO_PLAY_TIME_MS = %f", this, playTime));
+
+    Telemetry::Accumulate(Telemetry::VIDEO_HIDDEN_PLAY_TIME_MS, SECONDS_TO_MS(hiddenPlayTime));
+    LOG(LogLevel::Debug, ("%p VIDEO_HIDDEN_PLAY_TIME_MS = %f", this, hiddenPlayTime));
+
+    if (playTime > 0.0) {
+      // We have actually played something -> Report hidden/total ratio.
+      uint32_t hiddenPercentage = uint32_t(hiddenPlayTime / playTime * 100.0 + 0.5);
+
+      // Keyed by audio+video or video alone, and by a resolution range.
+      nsCString key(mMediaInfo.HasAudio() ? "AV," : "V,");
+      static const struct { int32_t mH; const char* mRes; } sResolutions[] = {
+        {  240, "0<h<=240" },
+        {  480, "240<h<=480" },
+        {  576, "480<h<=576" },
+        {  720, "576<h<=720" },
+        { 1080, "720<h<=1080" },
+        { 2160, "1080<h<=2160" }
+      };
+      const char* resolution = "h>2160";
+      int32_t height = mMediaInfo.mVideo.mImage.height;
+      for (const auto& res : sResolutions) {
+        if (height <= res.mH) {
+          resolution = res.mRes;
+          break;
+        }
+      }
+      key.AppendASCII(resolution);
+
+      Telemetry::Accumulate(Telemetry::VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE,
+                            key,
+                            hiddenPercentage);
+      // Also accumulate all percentages in an "All" key.
+      Telemetry::Accumulate(Telemetry::VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE,
+                            NS_LITERAL_CSTRING("All"),
+                            hiddenPercentage);
+      LOG(LogLevel::Debug, ("%p VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE = %u, keys: '%s' and 'All'",
+                            this, hiddenPercentage, key.get()));
+
+      if (data.mInterKeyframeCount != 0) {
+        uint32_t average_ms =
+          uint32_t(std::min<uint64_t>(double(data.mInterKeyframeSum_us)
+                                      / double(data.mInterKeyframeCount)
+                                      / 1000.0
+                                      + 0.5,
+                                      UINT32_MAX));
+        Telemetry::Accumulate(Telemetry::VIDEO_INTER_KEYFRAME_AVERAGE_MS,
+                              key,
+                              average_ms);
+        Telemetry::Accumulate(Telemetry::VIDEO_INTER_KEYFRAME_AVERAGE_MS,
+                              NS_LITERAL_CSTRING("All"),
+                              average_ms);
+        LOG(LogLevel::Debug, ("%p VIDEO_INTER_KEYFRAME_AVERAGE_MS = %u, keys: '%s' and 'All'",
+                              this, average_ms, key.get()));
+
+        uint32_t max_ms =
+          uint32_t(std::min<uint64_t>((data.mInterKeyFrameMax_us + 500) / 1000,
+                                      UINT32_MAX));
+        Telemetry::Accumulate(Telemetry::VIDEO_INTER_KEYFRAME_MAX_MS,
+                              key,
+                              max_ms);
+        Telemetry::Accumulate(Telemetry::VIDEO_INTER_KEYFRAME_MAX_MS,
+                              NS_LITERAL_CSTRING("All"),
+                              max_ms);
+        LOG(LogLevel::Debug, ("%p VIDEO_INTER_KEYFRAME_MAX_MS = %u, keys: '%s' and 'All'",
+                              this, max_ms, key.get()));
+      }
+    }
   }
 }
 
 void HTMLMediaElement::UnbindFromTree(bool aDeep,
                                       bool aNullParent)
 {
   if (!mPaused && mNetworkState != nsIDOMHTMLMediaElement::NETWORK_EMPTY) {
     Pause();
--- a/dom/html/HTMLVideoElement.cpp
+++ b/dom/html/HTMLVideoElement.cpp
@@ -26,16 +26,19 @@
 #include "MediaError.h"
 #include "MediaDecoder.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/dom/WakeLock.h"
 #include "mozilla/dom/power/PowerManagerService.h"
 #include "mozilla/dom/Performance.h"
 #include "mozilla/dom/VideoPlaybackQuality.h"
 
+#include <algorithm>
+#include <limits>
+
 NS_IMPL_NS_NEW_HTML_ELEMENT(Video)
 
 namespace mozilla {
 namespace dom {
 
 static bool sVideoStatsEnabled;
 
 NS_IMPL_ELEMENT_CLONE(HTMLVideoElement)
@@ -220,16 +223,22 @@ HTMLVideoElement::WrapNode(JSContext* aC
 bool
 HTMLVideoElement::NotifyOwnerDocumentActivityChangedInternal()
 {
   bool pauseElement = HTMLMediaElement::NotifyOwnerDocumentActivityChangedInternal();
   UpdateScreenWakeLock();
   return pauseElement;
 }
 
+FrameStatistics*
+HTMLVideoElement::GetFrameStatistics()
+{
+  return mDecoder ? &(mDecoder->GetFrameStatistics()) : nullptr;
+}
+
 already_AddRefed<VideoPlaybackQuality>
 HTMLVideoElement::GetVideoPlaybackQuality()
 {
   DOMHighResTimeStamp creationTime = 0;
   uint32_t totalFrames = 0;
   uint32_t droppedFrames = 0;
   uint32_t corruptedFrames = 0;
 
@@ -237,21 +246,34 @@ HTMLVideoElement::GetVideoPlaybackQualit
     if (nsPIDOMWindowInner* window = OwnerDoc()->GetInnerWindow()) {
       Performance* perf = window->GetPerformance();
       if (perf) {
         creationTime = perf->Now();
       }
     }
 
     if (mDecoder) {
-      FrameStatistics& stats = mDecoder->GetFrameStatistics();
-      static_assert(sizeof(uint32_t) >= sizeof (stats.GetParsedFrames()),
-                    "possible truncation from FrameStatistics to VideoPlaybackQuality");
-      totalFrames = stats.GetParsedFrames();
-      droppedFrames = stats.GetDroppedFrames();
+      FrameStatisticsData stats =
+        mDecoder->GetFrameStatistics().GetFrameStatisticsData();
+      if (sizeof(totalFrames) >= sizeof(stats.mParsedFrames)) {
+        totalFrames = stats.mParsedFrames;
+        droppedFrames = stats.mDroppedFrames;
+      } else {
+        auto maxStat = std::max(stats.mParsedFrames, stats.mDroppedFrames);
+        const auto maxNumber = std::numeric_limits<uint32_t>::max();
+        if (maxStat <= maxNumber) {
+          totalFrames = static_cast<uint32_t>(stats.mParsedFrames);
+          droppedFrames = static_cast<uint32_t>(stats.mDroppedFrames);
+        } else {
+          // Too big number(s) -> Resize everything to fit in 32 bits.
+          double ratio = double(maxNumber) / double(maxStat);
+          totalFrames = double(stats.mParsedFrames) * ratio;
+          droppedFrames = double(stats.mDroppedFrames) * ratio;
+        }
+      }
       corruptedFrames = 0;
     }
   }
 
   RefPtr<VideoPlaybackQuality> playbackQuality =
     new VideoPlaybackQuality(this, creationTime, totalFrames, droppedFrames,
                              corruptedFrames);
   return playbackQuality.forget();
--- a/dom/html/HTMLVideoElement.h
+++ b/dom/html/HTMLVideoElement.h
@@ -125,16 +125,19 @@ public:
   bool MozHasAudio() const;
 
   bool MozUseScreenWakeLock() const;
 
   void SetMozUseScreenWakeLock(bool aValue);
 
   bool NotifyOwnerDocumentActivityChangedInternal() override;
 
+  // Gives access to the decoder's frame statistics, if present.
+  FrameStatistics* GetFrameStatistics();
+
   already_AddRefed<VideoPlaybackQuality> GetVideoPlaybackQuality();
 
 protected:
   virtual ~HTMLVideoElement();
 
   virtual JSObject* WrapNode(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   virtual void WakeLockCreate() override;
--- a/dom/media/AbstractMediaDecoder.h
+++ b/dom/media/AbstractMediaDecoder.h
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef AbstractMediaDecoder_h_
 #define AbstractMediaDecoder_h_
 
 #include "mozilla/Attributes.h"
 #include "mozilla/StateMirroring.h"
 
+#include "FrameStatistics.h"
 #include "MediaEventSource.h"
 #include "MediaInfo.h"
 #include "nsISupports.h"
 #include "nsDataHashtable.h"
 #include "nsThreadUtils.h"
 
 class GMPCrashHelper;
 
@@ -52,18 +53,17 @@ public:
 
   // Get the current MediaResource being used. Its URI will be returned
   // by currentSrc. Returns what was passed to Load(), if Load() has been called.
   virtual MediaResource* GetResource() const = 0;
 
   // Increments the parsed, decoded and dropped frame counters by the passed in
   // counts.
   // Can be called on any thread.
-  virtual void NotifyDecodedFrames(uint32_t aParsed, uint32_t aDecoded,
-                                   uint32_t aDropped) = 0;
+  virtual void NotifyDecodedFrames(const FrameStatisticsData& aStats) = 0;
 
   virtual AbstractCanonical<media::NullableTimeUnit>* CanonicalDurationOrNull() { return nullptr; };
 
   // Return an event that will be notified when data arrives in MediaResource.
   // MediaDecoderReader will register with this event to receive notifications
   // in order to update buffer ranges.
   // Return null if this decoder doesn't support the event.
   virtual MediaEventSource<void>* DataArrivedEvent()
@@ -94,25 +94,25 @@ public:
   virtual already_AddRefed<GMPCrashHelper> GetCrashHelper() { return nullptr; }
 
   // Stack based class to assist in notifying the frame statistics of
   // parsed and decoded frames. Use inside video demux & decode functions
   // to ensure all parsed and decoded frames are reported on all return paths.
   class AutoNotifyDecoded {
   public:
     explicit AutoNotifyDecoded(AbstractMediaDecoder* aDecoder)
-      : mParsed(0), mDecoded(0), mDropped(0), mDecoder(aDecoder) {}
+      : mDecoder(aDecoder)
+    {}
     ~AutoNotifyDecoded() {
       if (mDecoder) {
-        mDecoder->NotifyDecodedFrames(mParsed, mDecoded, mDropped);
+        mDecoder->NotifyDecodedFrames(mStats);
       }
     }
-    uint32_t mParsed;
-    uint32_t mDecoded;
-    uint32_t mDropped;
+
+    FrameStatisticsData mStats;
 
   private:
     AbstractMediaDecoder* mDecoder;
   };
 
   // Classes directly inheriting from AbstractMediaDecoder do not support
   // Observe and it should never be called directly.
   NS_IMETHOD Observe(nsISupports *aSubject, const char * aTopic, const char16_t * aData) override
--- a/dom/media/FrameStatistics.h
+++ b/dom/media/FrameStatistics.h
@@ -2,95 +2,142 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #ifndef FrameStatistics_h_
 #define FrameStatistics_h_
 
+#include "mozilla/ReentrantMonitor.h"
+
 namespace mozilla {
 
+struct FrameStatisticsData
+{
+  // Number of frames parsed and demuxed from media.
+  // Access protected by mReentrantMonitor.
+  uint64_t mParsedFrames = 0;
+
+  // Number of parsed frames which were actually decoded.
+  // Access protected by mReentrantMonitor.
+  uint64_t mDecodedFrames = 0;
+
+  // Number of decoded frames which were actually sent down the rendering
+  // pipeline to be painted ("presented"). Access protected by mReentrantMonitor.
+  uint64_t mPresentedFrames = 0;
+
+  // Number of frames that have been skipped because they have missed their
+  // composition deadline.
+  uint64_t mDroppedFrames = 0;
+
+  // Sum of all inter-keyframe segment durations, in microseconds.
+  // Dividing by count will give the average inter-keyframe time.
+  uint64_t mInterKeyframeSum_us = 0;
+  // Number of inter-keyframe segments summed so far.
+  size_t mInterKeyframeCount = 0;
+
+  // Maximum inter-keyframe segment duration, in microseconds.
+  uint64_t mInterKeyFrameMax_us = 0;
+
+  FrameStatisticsData() = default;
+  FrameStatisticsData(uint64_t aParsed, uint64_t aDecoded, uint64_t aDropped)
+    : mParsedFrames(aParsed)
+    , mDecodedFrames(aDecoded)
+    , mDroppedFrames(aDropped)
+  {}
+
+  void
+  Accumulate(const FrameStatisticsData& aStats)
+  {
+    mParsedFrames += aStats.mParsedFrames;
+    mDecodedFrames += aStats.mDecodedFrames;
+    mPresentedFrames += aStats.mPresentedFrames;
+    mDroppedFrames += aStats.mDroppedFrames;
+    mInterKeyframeSum_us += aStats.mInterKeyframeSum_us;
+    mInterKeyframeCount += aStats.mInterKeyframeCount;
+    // It doesn't make sense to add max numbers, instead keep the bigger one.
+    if (mInterKeyFrameMax_us < aStats.mInterKeyFrameMax_us) {
+      mInterKeyFrameMax_us = aStats.mInterKeyFrameMax_us;
+    }
+  }
+};
+
 // Frame decoding/painting related performance counters.
 // Threadsafe.
-class FrameStatistics {
+class FrameStatistics
+{
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FrameStatistics);
 
-  FrameStatistics() :
-      mReentrantMonitor("FrameStats"),
-      mParsedFrames(0),
-      mDecodedFrames(0),
-      mPresentedFrames(0),
-      mDroppedFrames(0) {}
+  FrameStatistics()
+    : mReentrantMonitor("FrameStats")
+  {}
+
+  // Returns a copy of all frame statistics data.
+  // Can be called on any thread.
+  FrameStatisticsData GetFrameStatisticsData() const
+  {
+    ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+    return mFrameStatisticsData;
+  }
 
   // Returns number of frames which have been parsed from the media.
   // Can be called on any thread.
-  uint32_t GetParsedFrames() {
+  uint64_t GetParsedFrames() const
+  {
     ReentrantMonitorAutoEnter mon(mReentrantMonitor);
-    return mParsedFrames;
+    return mFrameStatisticsData.mParsedFrames;
   }
 
   // Returns the number of parsed frames which have been decoded.
   // Can be called on any thread.
-  uint32_t GetDecodedFrames() {
+  uint64_t GetDecodedFrames() const
+  {
     ReentrantMonitorAutoEnter mon(mReentrantMonitor);
-    return mDecodedFrames;
+    return mFrameStatisticsData.mDecodedFrames;
   }
 
   // Returns the number of decoded frames which have been sent to the rendering
   // pipeline for painting ("presented").
   // Can be called on any thread.
-  uint32_t GetPresentedFrames() {
+  uint64_t GetPresentedFrames() const
+  {
     ReentrantMonitorAutoEnter mon(mReentrantMonitor);
-    return mPresentedFrames;
+    return mFrameStatisticsData.mPresentedFrames;
   }
 
-  // Number of frames that have been skipped because they have missed their
-  // compoisition deadline.
-  uint32_t GetDroppedFrames() {
+  // Returns the number of frames that have been skipped because they have
+  // missed their composition deadline.
+  uint64_t GetDroppedFrames() const
+  {
     ReentrantMonitorAutoEnter mon(mReentrantMonitor);
-    return mDroppedFrames;
+    return mFrameStatisticsData.mDroppedFrames;
   }
 
   // Increments the parsed and decoded frame counters by the passed in counts.
   // Can be called on any thread.
-  void NotifyDecodedFrames(uint32_t aParsed, uint32_t aDecoded,
-                           uint32_t aDropped) {
-    if (aParsed == 0 && aDecoded == 0 && aDropped == 0)
-      return;
+  void NotifyDecodedFrames(const FrameStatisticsData& aStats)
+  {
     ReentrantMonitorAutoEnter mon(mReentrantMonitor);
-    mParsedFrames += aParsed;
-    mDecodedFrames += aDecoded;
-    mDroppedFrames += aDropped;
+    mFrameStatisticsData.Accumulate(aStats);
   }
 
   // Increments the presented frame counters.
   // Can be called on any thread.
-  void NotifyPresentedFrame() {
+  void NotifyPresentedFrame()
+  {
     ReentrantMonitorAutoEnter mon(mReentrantMonitor);
-    ++mPresentedFrames;
+    ++mFrameStatisticsData.mPresentedFrames;
   }
 
 private:
   ~FrameStatistics() {}
 
   // ReentrantMonitor to protect access of playback statistics.
-  ReentrantMonitor mReentrantMonitor;
-
-  // Number of frames parsed and demuxed from media.
-  // Access protected by mReentrantMonitor.
-  uint32_t mParsedFrames;
+  mutable ReentrantMonitor mReentrantMonitor;
 
-  // Number of parsed frames which were actually decoded.
-  // Access protected by mReentrantMonitor.
-  uint32_t mDecodedFrames;
-
-  // Number of decoded frames which were actually sent down the rendering
-  // pipeline to be painted ("presented"). Access protected by mReentrantMonitor.
-  uint32_t mPresentedFrames;
-
-  uint32_t mDroppedFrames;
+  FrameStatisticsData mFrameStatisticsData;
 };
 
 } // namespace mozilla
 
-#endif
+#endif // FrameStatistics_h_
--- a/dom/media/MediaDecoder.h
+++ b/dom/media/MediaDecoder.h
@@ -22,17 +22,16 @@
 #include "necko-config.h"
 #include "nsAutoPtr.h"
 #include "nsCOMPtr.h"
 #include "nsIObserver.h"
 #include "nsISupports.h"
 #include "nsITimer.h"
 
 #include "AbstractMediaDecoder.h"
-#include "FrameStatistics.h"
 #include "MediaDecoderOwner.h"
 #include "MediaEventSource.h"
 #include "MediaMetadataManager.h"
 #include "MediaResource.h"
 #include "MediaResourceCallback.h"
 #include "MediaStatistics.h"
 #include "MediaStreamGraph.h"
 #include "TimeUnits.h"
@@ -479,20 +478,19 @@ private:
   // at any time.
   MediaStatistics GetStatistics();
 
   // Return the frame decode/paint related statistics.
   FrameStatistics& GetFrameStatistics() { return *mFrameStats; }
 
   // Increments the parsed and decoded frame counters by the passed in counts.
   // Can be called on any thread.
-  virtual void NotifyDecodedFrames(uint32_t aParsed, uint32_t aDecoded,
-                                   uint32_t aDropped) override
+  virtual void NotifyDecodedFrames(const FrameStatisticsData& aStats) override
   {
-    GetFrameStatistics().NotifyDecodedFrames(aParsed, aDecoded, aDropped);
+    GetFrameStatistics().NotifyDecodedFrames(aStats);
   }
 
   void UpdateReadyState()
   {
     MOZ_ASSERT(NS_IsMainThread());
     if (!IsShutdown()) {
       mOwner->UpdateReadyState();
     }
--- a/dom/media/MediaFormatReader.cpp
+++ b/dom/media/MediaFormatReader.cpp
@@ -62,16 +62,17 @@ MediaFormatReader::MediaFormatReader(Abs
   : MediaDecoderReader(aDecoder)
   , mAudio(this, MediaData::AUDIO_DATA, Preferences::GetUint("media.audio-decode-ahead", 2),
            Preferences::GetUint("media.audio-max-decode-error", 3))
   , mVideo(this, MediaData::VIDEO_DATA, Preferences::GetUint("media.video-decode-ahead", 2),
            Preferences::GetUint("media.video-max-decode-error", 2))
   , mDemuxer(aDemuxer)
   , mDemuxerInitDone(false)
   , mLastReportedNumDecodedFrames(0)
+  , mPreviousDecodedKeyframeTime_us(sNoPreviousDecodedKeyframe)
   , mLayersBackendType(aLayersBackend)
   , mInitDone(false)
   , mIsEncrypted(false)
   , mTrackDemuxersMayBlock(false)
   , mDemuxOnly(false)
   , mSeekScheduled(false)
   , mVideoFrameContainer(aVideoFrameContainer)
 {
@@ -1010,17 +1011,17 @@ MediaFormatReader::HandleDemuxedSamples(
     }
 
     LOGV("Input:%lld (dts:%lld kf:%d)",
          sample->mTime, sample->mTimecode, sample->mKeyframe);
     decoder.mOutputRequested = true;
     decoder.mNumSamplesInput++;
     decoder.mSizeOfQueue++;
     if (aTrack == TrackInfo::kVideoTrack) {
-      aA.mParsed++;
+      aA.mStats.mParsedFrames++;
     }
 
     if (mDemuxOnly) {
       ReturnOutput(sample, aTrack);
     } else if (!DecodeDemuxedSamples(aTrack, sample)) {
       NotifyError(aTrack);
       return;
     }
@@ -1154,16 +1155,18 @@ MediaFormatReader::Update(TrackType aTra
   // Drop any frames found prior our internal seek target.
   while (decoder.mTimeThreshold && decoder.mOutput.Length()) {
     RefPtr<MediaData>& output = decoder.mOutput[0];
     InternalSeekTarget target = decoder.mTimeThreshold.ref();
     media::TimeUnit time = media::TimeUnit::FromMicroseconds(output->mTime);
     if (time >= target.Time()) {
       // We have reached our internal seek target.
       decoder.mTimeThreshold.reset();
+      // We might have dropped some keyframes.
+      mPreviousDecodedKeyframeTime_us = sNoPreviousDecodedKeyframe;
     }
     if (time < target.Time() || (target.mDropTarget && target.Contains(time))) {
       LOGV("Internal Seeking: Dropping %s frame time:%f wanted:%f (kf:%d)",
            TrackTypeToStr(aTrack),
            media::TimeUnit::FromMicroseconds(output->mTime).ToSeconds(),
            target.Time().ToSeconds(),
            output->mKeyframe);
       decoder.mOutput.RemoveElementAt(0);
@@ -1187,18 +1190,30 @@ MediaFormatReader::Update(TrackType aTra
         Some(TimeInterval(TimeUnit::FromMicroseconds(output->mTime),
                           TimeUnit::FromMicroseconds(output->GetEndTime())));
       decoder.mNumSamplesOutputTotal++;
       ReturnOutput(output, aTrack);
       // We have a decoded sample ready to be returned.
       if (aTrack == TrackType::kVideoTrack) {
         uint64_t delta =
           decoder.mNumSamplesOutputTotal - mLastReportedNumDecodedFrames;
-        a.mDecoded = static_cast<uint32_t>(delta);
+        a.mStats.mDecodedFrames = static_cast<uint32_t>(delta);
         mLastReportedNumDecodedFrames = decoder.mNumSamplesOutputTotal;
+        if (output->mKeyframe) {
+          if (mPreviousDecodedKeyframeTime_us < output->mTime) {
+            // There is a previous keyframe -> Record inter-keyframe stats.
+            uint64_t segment_us = output->mTime - mPreviousDecodedKeyframeTime_us;
+            a.mStats.mInterKeyframeSum_us += segment_us;
+            a.mStats.mInterKeyframeCount += 1;
+            if (a.mStats.mInterKeyFrameMax_us < segment_us) {
+              a.mStats.mInterKeyFrameMax_us = segment_us;
+            }
+          }
+          mPreviousDecodedKeyframeTime_us = output->mTime;
+        }
         nsCString error;
         mVideo.mIsHardwareAccelerated =
           mVideo.mDecoder && mVideo.mDecoder->IsHardwareAccelerated(error);
       }
     } else if (decoder.HasFatalError()) {
       LOG("Rejecting %s promise: DECODE_ERROR", TrackTypeToStr(aTrack));
       decoder.RejectPromise(DECODE_ERROR, __func__);
       return;
@@ -1486,17 +1501,17 @@ MediaFormatReader::DropDecodedSamples(Tr
     if (time >= decoder.mTimeThreshold.ref().Time()) {
       // We would have reached our internal seek target.
       decoder.mTimeThreshold.reset();
     }
   }
   decoder.mOutput.Clear();
   decoder.mSizeOfQueue -= lengthDecodedQueue;
   if (aTrack == TrackInfo::kVideoTrack && mDecoder) {
-    mDecoder->NotifyDecodedFrames(0, 0, lengthDecodedQueue);
+    mDecoder->NotifyDecodedFrames({ 0, 0, lengthDecodedQueue });
   }
 }
 
 void
 MediaFormatReader::SkipVideoDemuxToNextKeyFrame(media::TimeUnit aTimeThreshold)
 {
   MOZ_ASSERT(OnTaskQueue());
   LOG("Skipping up to %lld", aTimeThreshold.ToMicroseconds());
@@ -1522,25 +1537,25 @@ MediaFormatReader::VideoSkipReset(uint32
 {
   MOZ_ASSERT(OnTaskQueue());
 
   // Some frames may have been output by the decoder since we initiated the
   // videoskip process and we know they would be late.
   DropDecodedSamples(TrackInfo::kVideoTrack);
   // Report the pending frames as dropped.
   if (mDecoder) {
-    mDecoder->NotifyDecodedFrames(0, 0, SizeOfVideoQueueInFrames());
+    mDecoder->NotifyDecodedFrames({ 0, 0, SizeOfVideoQueueInFrames() });
   }
 
   // Cancel any pending demux request and pending demuxed samples.
   mVideo.mDemuxRequest.DisconnectIfExists();
   Reset(TrackType::kVideoTrack);
 
   if (mDecoder) {
-    mDecoder->NotifyDecodedFrames(aSkipped, 0, aSkipped);
+    mDecoder->NotifyDecodedFrames({ aSkipped, 0, aSkipped });
   }
 
   mVideo.mNumSamplesSkippedTotal += aSkipped;
 }
 
 void
 MediaFormatReader::OnVideoSkipCompleted(uint32_t aSkipped)
 {
@@ -1752,16 +1767,18 @@ MediaFormatReader::DoVideoSeek()
 
 void
 MediaFormatReader::OnVideoSeekCompleted(media::TimeUnit aTime)
 {
   MOZ_ASSERT(OnTaskQueue());
   LOGV("Video seeked to %lld", aTime.ToMicroseconds());
   mVideo.mSeekRequest.Complete();
 
+  mPreviousDecodedKeyframeTime_us = sNoPreviousDecodedKeyframe;
+
   SetVideoDecodeThreshold();
 
   if (HasAudio() && !mOriginalSeekTarget.IsVideoOnly()) {
     MOZ_ASSERT(mPendingSeekTime.isSome());
     if (mOriginalSeekTarget.IsFast()) {
       // We are performing a fast seek. We need to seek audio to where the
       // video seeked to, to ensure proper A/V sync once playback resume.
       mPendingSeekTime = Some(aTime);
@@ -1769,16 +1786,23 @@ MediaFormatReader::OnVideoSeekCompleted(
     DoAudioSeek();
   } else {
     mPendingSeekTime.reset();
     mSeekPromise.Resolve(aTime, __func__);
   }
 }
 
 void
+MediaFormatReader::OnVideoSeekFailed(DemuxerFailureReason aFailure)
+{
+  mPreviousDecodedKeyframeTime_us = sNoPreviousDecodedKeyframe;
+  OnSeekFailed(TrackType::kVideoTrack, aFailure);
+}
+
+void
 MediaFormatReader::SetVideoDecodeThreshold()
 {
   MOZ_ASSERT(OnTaskQueue());
 
   if (!HasVideo() || !mVideo.mDecoder) {
     return;
   }
 
@@ -1828,16 +1852,22 @@ MediaFormatReader::OnAudioSeekCompleted(
 {
   MOZ_ASSERT(OnTaskQueue());
   LOGV("Audio seeked to %lld", aTime.ToMicroseconds());
   mAudio.mSeekRequest.Complete();
   mPendingSeekTime.reset();
   mSeekPromise.Resolve(aTime, __func__);
 }
 
+void
+MediaFormatReader::OnAudioSeekFailed(DemuxerFailureReason aFailure)
+{
+  OnSeekFailed(TrackType::kAudioTrack, aFailure);
+}
+
 media::TimeIntervals
 MediaFormatReader::GetBuffered()
 {
   MOZ_ASSERT(OnTaskQueue());
   media::TimeIntervals videoti;
   media::TimeIntervals audioti;
   media::TimeIntervals intervals;
 
--- a/dom/media/MediaFormatReader.h
+++ b/dom/media/MediaFormatReader.h
@@ -510,16 +510,21 @@ private:
   void OnVideoSkipFailed(MediaTrackDemuxer::SkipFailureHolder aFailure);
 
   // The last number of decoded output frames that we've reported to
   // MediaDecoder::NotifyDecoded(). We diff the number of output video
   // frames every time that DecodeVideoData() is called, and report the
   // delta there.
   uint64_t mLastReportedNumDecodedFrames;
 
+  // Timestamp of the previous decoded keyframe, in microseconds.
+  int64_t mPreviousDecodedKeyframeTime_us;
+  // Default mLastDecodedKeyframeTime_us value, must be bigger than anything.
+  static const int64_t sNoPreviousDecodedKeyframe = INT64_MAX;
+
   layers::LayersBackend mLayersBackendType;
 
   // Metadata objects
   // True if we've read the streams' metadata.
   bool mInitDone;
   MozPromiseHolder<MetadataPromise> mMetadataPromise;
   bool IsEncrypted()
   {
@@ -541,28 +546,22 @@ private:
   {
     return IsSeeking() && mOriginalSeekTarget.IsVideoOnly();
   }
   void ScheduleSeek();
   void AttemptSeek();
   void OnSeekFailed(TrackType aTrack, DemuxerFailureReason aFailure);
   void DoVideoSeek();
   void OnVideoSeekCompleted(media::TimeUnit aTime);
-  void OnVideoSeekFailed(DemuxerFailureReason aFailure)
-  {
-    OnSeekFailed(TrackType::kVideoTrack, aFailure);
-  }
+  void OnVideoSeekFailed(DemuxerFailureReason aFailure);
   bool mSeekScheduled;
 
   void DoAudioSeek();
   void OnAudioSeekCompleted(media::TimeUnit aTime);
-  void OnAudioSeekFailed(DemuxerFailureReason aFailure)
-  {
-    OnSeekFailed(TrackType::kAudioTrack, aFailure);
-  }
+  void OnAudioSeekFailed(DemuxerFailureReason aFailure);
   // The SeekTarget that was last given to Seek()
   SeekTarget mOriginalSeekTarget;
   // Temporary seek information while we wait for the data
   Maybe<media::TimeUnit> mFallbackSeekTime;
   Maybe<media::TimeUnit> mPendingSeekTime;
   MozPromiseHolder<SeekPromise> mSeekPromise;
 
   RefPtr<VideoFrameContainer> mVideoFrameContainer;
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -356,16 +356,19 @@ public:
            mVideoDevice->GetMediaSource() == dom::MediaSourceEnum::Browser;
   }
 
   void GetSettings(dom::MediaTrackSettings& aOutSettings)
   {
     if (mVideoDevice) {
       mVideoDevice->GetSource()->GetSettings(aOutSettings);
     }
+    if (mAudioDevice) {
+      mAudioDevice->GetSource()->GetSettings(aOutSettings);
+    }
   }
 
   // implement in .cpp to avoid circular dependency with MediaOperationTask
   // Can be invoked from EITHER MainThread or MSG thread
   void Stop();
 
   void
   AudioConfig(bool aEchoOn, uint32_t aEcho,
@@ -1613,17 +1616,17 @@ private:
  * EnumerateRawDevices - Enumerate a list of audio & video devices that
  * satisfy passed-in constraints. List contains raw id's.
  */
 
 already_AddRefed<MediaManager::PledgeSourceSet>
 MediaManager::EnumerateRawDevices(uint64_t aWindowId,
                                   MediaSourceEnum aVideoType,
                                   MediaSourceEnum aAudioType,
-                                  bool aFake, bool aFakeTracks)
+                                  bool aFake)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aVideoType != MediaSourceEnum::Other ||
              aAudioType != MediaSourceEnum::Other);
   RefPtr<PledgeSourceSet> p = new PledgeSourceSet();
   uint32_t id = mOutstandingPledges.Append(*p);
 
   nsAdoptingCString audioLoopDev, videoLoopDev;
@@ -1634,34 +1637,28 @@ MediaManager::EnumerateRawDevices(uint64
     if (aVideoType == MediaSourceEnum::Camera) {
       videoLoopDev = Preferences::GetCString("media.video_loopback_dev");
     }
     if (aAudioType == MediaSourceEnum::Microphone) {
       audioLoopDev = Preferences::GetCString("media.audio_loopback_dev");
     }
   }
 
-  if (!aFake) {
-    // Fake tracks only make sense when we have a fake stream.
-    aFakeTracks = false;
-  }
-
   MediaManager::PostTask(NewTaskFrom([id, aWindowId, audioLoopDev,
                                       videoLoopDev, aVideoType,
-                                      aAudioType, aFake,
-                                      aFakeTracks]() mutable {
+                                      aAudioType, aFake]() mutable {
     // Only enumerate what's asked for, and only fake cams and mics.
     bool hasVideo = aVideoType != MediaSourceEnum::Other;
     bool hasAudio = aAudioType != MediaSourceEnum::Other;
     bool fakeCams = aFake && aVideoType == MediaSourceEnum::Camera;
     bool fakeMics = aFake && aAudioType == MediaSourceEnum::Microphone;
 
     RefPtr<MediaEngine> fakeBackend, realBackend;
     if (fakeCams || fakeMics) {
-      fakeBackend = new MediaEngineDefault(aFakeTracks);
+      fakeBackend = new MediaEngineDefault();
     }
     if ((!fakeCams && hasVideo) || (!fakeMics && hasAudio)) {
       RefPtr<MediaManager> manager = MediaManager_GetInstance();
       realBackend = manager->GetBackend(aWindowId);
     }
 
     auto result = MakeUnique<SourceSet>();
 
@@ -2340,24 +2337,21 @@ MediaManager::GetUserMedia(nsPIDOMWindow
 
   nsString callID;
   rv = GenerateUUID(callID);
   NS_ENSURE_SUCCESS(rv, rv);
 
   bool fake = c.mFake.WasPassed()? c.mFake.Value() :
       Preferences::GetBool("media.navigator.streams.fake");
 
-  bool fakeTracks = c.mFakeTracks.WasPassed()? c.mFakeTracks.Value() : false;
-
   bool askPermission = !privileged &&
       (!fake || Preferences::GetBool("media.navigator.permission.fake"));
 
   RefPtr<PledgeSourceSet> p = EnumerateDevicesImpl(windowID, videoType,
-                                                   audioType, fake,
-                                                   fakeTracks);
+                                                   audioType, fake);
   p->Then([this, onSuccess, onFailure, windowID, c, listener, askPermission,
            prefs, isHTTPS, callID, origin](SourceSet*& aDevices) mutable {
 
     RefPtr<Refcountable<UniquePtr<SourceSet>>> devices(
          new Refcountable<UniquePtr<SourceSet>>(aDevices)); // grab result
 
     // Ensure that the captured 'this' pointer and our windowID are still good.
     if (!MediaManager::Exists() ||
@@ -2534,17 +2528,17 @@ MediaManager::ToJSArray(SourceSet& aDevi
   }
   return var.forget();
 }
 
 already_AddRefed<MediaManager::PledgeSourceSet>
 MediaManager::EnumerateDevicesImpl(uint64_t aWindowId,
                                    MediaSourceEnum aVideoType,
                                    MediaSourceEnum aAudioType,
-                                   bool aFake, bool aFakeTracks)
+                                   bool aFake)
 {
   MOZ_ASSERT(NS_IsMainThread());
   nsPIDOMWindowInner* window =
     nsGlobalWindow::GetInnerWindowWithId(aWindowId)->AsInner();
 
   // This function returns a pledge, a promise-like object with the future result
   RefPtr<PledgeSourceSet> pledge = new PledgeSourceSet();
   uint32_t id = mOutstandingPledges.Append(*pledge);
@@ -2563,23 +2557,22 @@ MediaManager::EnumerateDevicesImpl(uint6
   // GetOriginKey is an async API that returns a pledge (a promise-like
   // pattern). We use .Then() to pass in a lambda to run back on this same
   // thread later once GetOriginKey resolves. Needed variables are "captured"
   // (passed by value) safely into the lambda.
 
   RefPtr<Pledge<nsCString>> p = media::GetOriginKey(origin, privateBrowsing,
                                                       persist);
   p->Then([id, aWindowId, aVideoType, aAudioType,
-           aFake, aFakeTracks](const nsCString& aOriginKey) mutable {
+           aFake](const nsCString& aOriginKey) mutable {
     MOZ_ASSERT(NS_IsMainThread());
     RefPtr<MediaManager> mgr = MediaManager_GetInstance();
 
-    RefPtr<PledgeSourceSet> p = mgr->EnumerateRawDevices(aWindowId,
-                                                         aVideoType, aAudioType,
-                                                         aFake, aFakeTracks);
+    RefPtr<PledgeSourceSet> p = mgr->EnumerateRawDevices(aWindowId, aVideoType,
+                                                         aAudioType, aFake);
     p->Then([id, aWindowId, aOriginKey](SourceSet*& aDevices) mutable {
       UniquePtr<SourceSet> devices(aDevices); // secondary result
 
       // Only run if window is still on our active list.
       RefPtr<MediaManager> mgr = MediaManager_GetInstance();
       if (!mgr) {
         return NS_OK;
       }
--- a/dom/media/MediaManager.h
+++ b/dom/media/MediaManager.h
@@ -91,17 +91,17 @@ private:
                              nsString aN);
   static uint32_t FitnessDistance(nsString aN,
       const dom::ConstrainDOMStringParameters& aParams);
 protected:
   nsString mName;
   nsString mID;
   dom::MediaSourceEnum mMediaSource;
   RefPtr<MediaEngineSource> mSource;
-  RefPtr<MediaEngineSource::BaseAllocationHandle> mAllocationHandle;
+  RefPtr<MediaEngineSource::AllocationHandle> mAllocationHandle;
 public:
   dom::MediaSourceEnum GetMediaSource() {
     return mMediaSource;
   }
   bool mIsVideo;
 };
 
 class VideoDevice : public MediaDevice
@@ -268,22 +268,22 @@ private:
 public: // TODO: make private once we upgrade to GCC 4.8+ on linux.
   static void AnonymizeDevices(SourceSet& aDevices, const nsACString& aOriginKey);
   static already_AddRefed<nsIWritableVariant> ToJSArray(SourceSet& aDevices);
 private:
   already_AddRefed<PledgeSourceSet>
   EnumerateRawDevices(uint64_t aWindowId,
                       dom::MediaSourceEnum aVideoType,
                       dom::MediaSourceEnum aAudioType,
-                      bool aFake, bool aFakeTracks);
+                      bool aFake);
   already_AddRefed<PledgeSourceSet>
   EnumerateDevicesImpl(uint64_t aWindowId,
                        dom::MediaSourceEnum aVideoSrcType,
                        dom::MediaSourceEnum aAudioSrcType,
-                       bool aFake = false, bool aFakeTracks = false);
+                       bool aFake = false);
   already_AddRefed<PledgeChar>
   SelectSettings(
       dom::MediaStreamConstraints& aConstraints,
       RefPtr<media::Refcountable<UniquePtr<SourceSet>>>& aSources);
 
   StreamListeners* AddWindowID(uint64_t aWindowId);
   WindowTable *GetActiveWindows() {
     MOZ_ASSERT(NS_IsMainThread());
--- a/dom/media/VideoPlaybackQuality.h
+++ b/dom/media/VideoPlaybackQuality.h
@@ -31,27 +31,27 @@ public:
 
   JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
 
   DOMHighResTimeStamp CreationTime() const
   {
     return mCreationTime;
   }
 
-  uint32_t TotalVideoFrames()
+  uint32_t TotalVideoFrames() const
   {
     return mTotalFrames;
   }
 
-  uint32_t DroppedVideoFrames()
+  uint32_t DroppedVideoFrames() const
   {
     return mDroppedFrames;
   }
 
-  uint32_t CorruptedVideoFrames()
+  uint32_t CorruptedVideoFrames() const
   {
     return mCorruptedFrames;
   }
 
 private:
   ~VideoPlaybackQuality() {}
 
   RefPtr<HTMLMediaElement> mElement;
@@ -59,9 +59,9 @@ private:
   uint32_t mTotalFrames;
   uint32_t mDroppedFrames;
   uint32_t mCorruptedFrames;
 };
 
 } // namespace dom
 } // namespace mozilla
 
-#endif /* mozilla_dom_VideoPlaybackQuality_h_ */
+#endif // mozilla_dom_VideoPlaybackQuality_h_
--- a/dom/media/android/AndroidMediaReader.cpp
+++ b/dom/media/android/AndroidMediaReader.cpp
@@ -150,18 +150,18 @@ bool AndroidMediaReader::DecodeVideoFram
     mVideoSeekTimeUs = -1;
 
     if (aKeyframeSkip) {
       // Disable keyframe skipping for now as
       // stagefright doesn't seem to be telling us
       // when a frame is a keyframe.
 #if 0
       if (!frame.mKeyFrame) {
-        ++a.mParsed;
-        ++a.mDropped;
+        ++a.mStats.mParsedFrames;
+        ++a.mStats.mDroppedFrames;
         continue;
       }
 #endif
       aKeyframeSkip = false;
     }
 
     if (frame.mSize == 0)
       return true;
@@ -239,19 +239,19 @@ bool AndroidMediaReader::DecodeVideoFram
                             frame.mKeyFrame,
                             -1,
                             picture);
     }
 
     if (!v) {
       return false;
     }
-    a.mParsed++;
-    a.mDecoded++;
-    NS_ASSERTION(a.mDecoded <= a.mParsed, "Expect to decode fewer frames than parsed in AndroidMedia...");
+    a.mStats.mParsedFrames++;
+    a.mStats.mDecodedFrames++;
+    NS_ASSERTION(a.mStats.mDecodedFrames <= a.mStats.mParsedFrames, "Expect to decode fewer frames than parsed in AndroidMedia...");
 
     // Since MPAPI doesn't give us the end time of frames, we keep one frame
     // buffered in AndroidMediaReader and push it into the queue as soon
     // we read the following frame so we can use that frame's start time as
     // the end time of the buffered frame.
     if (!mLastVideoFrame) {
       mLastVideoFrame = v;
       continue;
--- a/dom/media/mediasink/VideoSink.cpp
+++ b/dom/media/mediasink/VideoSink.cpp
@@ -401,17 +401,17 @@ VideoSink::UpdateRenderedVideoFrames()
     while (VideoQueue().GetSize() > 0) {
       RefPtr<MediaData> nextFrame = VideoQueue().PeekFront();
       if (nextFrame->mTime > clockTime) {
         remainingTime = nextFrame->mTime - clockTime;
         break;
       }
       ++framesRemoved;
       if (!currentFrame->As<VideoData>()->mSentToCompositor) {
-        mFrameStats.NotifyDecodedFrames(0, 0, 1);
+        mFrameStats.NotifyDecodedFrames({ 0, 0, 1 });
         VSINK_LOG_V("discarding video frame mTime=%lld clock_time=%lld",
                     currentFrame->mTime, clockTime);
       }
       currentFrame = VideoQueue().PopFront();
     }
     VideoQueue().PushFront(currentFrame);
     if (framesRemoved > 0) {
       mVideoFrameEndTime = currentFrame->GetEndTime();
--- a/dom/media/ogg/OggReader.cpp
+++ b/dom/media/ogg/OggReader.cpp
@@ -916,27 +916,27 @@ bool OggReader::DecodeVideoFrame(bool &a
     }
     packet = NextOggPacket(mTheoraState);
   } while (packet && mTheoraState->IsHeader(packet));
   if (!packet) {
     return false;
   }
   nsAutoRef<ogg_packet> autoRelease(packet);
 
-  a.mParsed++;
+  a.mStats.mParsedFrames++;
   NS_ASSERTION(packet && packet->granulepos != -1,
                 "Must know first packet's granulepos");
   bool eos = packet->e_o_s;
   int64_t frameEndTime = mTheoraState->Time(packet->granulepos);
   if (!aKeyframeSkip ||
      (th_packet_iskeyframe(packet) && frameEndTime >= aTimeThreshold))
   {
     aKeyframeSkip = false;
     nsresult res = DecodeTheora(packet, aTimeThreshold);
-    a.mDecoded++;
+    a.mStats.mDecodedFrames++;
     if (NS_FAILED(res)) {
       return false;
     }
   }
 
   if (eos) {
     // We've encountered an end of bitstream packet. Inform the queue that
     // there will be no more frames.
--- a/dom/media/omx/AudioOffloadPlayer.cpp
+++ b/dom/media/omx/AudioOffloadPlayer.cpp
@@ -15,16 +15,17 @@
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 #include "AudioOffloadPlayer.h"
 #include "nsComponentManagerUtils.h"
 #include "nsITimer.h"
+#include "MediaOmxCommonDecoder.h"
 #include "mozilla/dom/HTMLMediaElement.h"
 #include "VideoUtils.h"
 #include "mozilla/dom/power/PowerManagerService.h"
 #include "mozilla/dom/WakeLock.h"
 
 #include <binder/IPCThreadState.h>
 #include <stagefright/foundation/ADebug.h>
 #include <stagefright/foundation/ALooper.h>
@@ -53,41 +54,58 @@ static const uint64_t OFFLOAD_PAUSE_MAX_
 AudioOffloadPlayer::AudioOffloadPlayer(MediaOmxCommonDecoder* aObserver) :
   mStarted(false),
   mPlaying(false),
   mReachedEOS(false),
   mIsElementVisible(true),
   mSampleRate(0),
   mStartPosUs(0),
   mPositionTimeMediaUs(-1),
-  mInputBuffer(nullptr),
-  mObserver(aObserver)
+  mInputBuffer(nullptr)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   CHECK(aObserver);
 #if ANDROID_VERSION >= 21
   mSessionId = AudioSystem::newAudioUniqueId();
   AudioSystem::acquireAudioSessionId(mSessionId, -1);
 #else
   mSessionId = AudioSystem::newAudioSessionId();
   AudioSystem::acquireAudioSessionId(mSessionId);
 #endif
   mAudioSink = new AudioOutput(mSessionId,
       IPCThreadState::self()->getCallingUid());
+
+  nsCOMPtr<nsIThread> thread;
+  MOZ_ALWAYS_SUCCEEDS(NS_GetMainThread(getter_AddRefs(thread)));
+  mPositionChanged = mOnPositionChanged.Connect(
+    thread, aObserver, &MediaOmxCommonDecoder::NotifyOffloadPlayerPositionChanged);
+  mPlaybackEnded = mOnPlaybackEnded.Connect(
+    thread, aObserver, &MediaDecoder::PlaybackEnded);
+  mPlayerTearDown = mOnPlayerTearDown.Connect(
+    thread, aObserver, &MediaOmxCommonDecoder::AudioOffloadTearDown);
+  mSeekingStarted = mOnSeekingStarted.Connect(
+    thread, aObserver, &MediaDecoder::SeekingStarted);
 }
 
 AudioOffloadPlayer::~AudioOffloadPlayer()
 {
   Reset();
 #if ANDROID_VERSION >= 21
   AudioSystem::releaseAudioSessionId(mSessionId, -1);
 #else
   AudioSystem::releaseAudioSessionId(mSessionId);
 #endif
+
+  // Disconnect the listeners to prevent notifications from reaching
+  // the MediaOmxCommonDecoder object after shutdown.
+  mPositionChanged.Disconnect();
+  mPlaybackEnded.Disconnect();
+  mPlayerTearDown.Disconnect();
+  mSeekingStarted.Disconnect();
 }
 
 void AudioOffloadPlayer::SetSource(const sp<MediaSource> &aSource)
 {
   MOZ_ASSERT(NS_IsMainThread());
   CHECK(!mSource.get());
 
   mSource = aSource;
@@ -348,22 +366,17 @@ status_t AudioOffloadPlayer::DoSeek()
   AUDIO_OFFLOAD_LOG(LogLevel::Debug,
                     ("DoSeek ( %lld )", mSeekTarget.GetTime().ToMicroseconds()));
 
   mReachedEOS = false;
   mPositionTimeMediaUs = -1;
   mStartPosUs = mSeekTarget.GetTime().ToMicroseconds();
 
   if (!mSeekPromise.IsEmpty()) {
-    nsCOMPtr<nsIRunnable> nsEvent =
-      NewRunnableMethod<MediaDecoderEventVisibility>(
-        mObserver,
-        &MediaDecoder::SeekingStarted,
-        mSeekTarget.mEventVisibility);
-    NS_DispatchToCurrentThread(nsEvent);
+    mOnSeekingStarted.Notify(mSeekTarget.mEventVisibility);
   }
 
   if (mPlaying) {
     mAudioSink->Pause();
     mAudioSink->Flush();
     mAudioSink->Start();
 
   } else {
@@ -420,39 +433,36 @@ int64_t AudioOffloadPlayer::GetOutputPla
 void AudioOffloadPlayer::NotifyAudioEOS()
 {
   android::Mutex::Autolock autoLock(mLock);
   // We do not reset mSeekTarget here.
   if (!mSeekPromise.IsEmpty()) {
     MediaDecoder::SeekResolveValue val(mReachedEOS, mSeekTarget.mEventVisibility);
     mSeekPromise.Resolve(val, __func__);
   }
-  NS_DispatchToMainThread(NewRunnableMethod(mObserver,
-                                            &MediaDecoder::PlaybackEnded));
+  mOnPlaybackEnded.Notify();
 }
 
 void AudioOffloadPlayer::NotifyPositionChanged()
 {
-  NS_DispatchToMainThread(NewRunnableMethod(mObserver,
-                                            &MediaOmxCommonDecoder::NotifyOffloadPlayerPositionChanged));
+  mOnPositionChanged.Notify();
 }
 
 void AudioOffloadPlayer::NotifyAudioTearDown()
 {
   // Fallback to state machine.
   // state machine's seeks will be done with
   // MediaDecoderEventVisibility::Suppressed.
   android::Mutex::Autolock autoLock(mLock);
   // We do not reset mSeekTarget here.
   if (!mSeekPromise.IsEmpty()) {
     MediaDecoder::SeekResolveValue val(mReachedEOS, mSeekTarget.mEventVisibility);
     mSeekPromise.Resolve(val, __func__);
   }
-  NS_DispatchToMainThread(NewRunnableMethod(mObserver,
-                                            &MediaOmxCommonDecoder::AudioOffloadTearDown));
+  mOnPlayerTearDown.Notify();
 }
 
 // static
 size_t AudioOffloadPlayer::AudioSinkCallback(GonkAudioSink* aAudioSink,
                                              void* aBuffer,
                                              size_t aSize,
                                              void* aCookie,
                                              GonkAudioSink::cb_event_t aEvent)
--- a/dom/media/omx/AudioOffloadPlayer.h
+++ b/dom/media/omx/AudioOffloadPlayer.h
@@ -24,17 +24,17 @@
 #include <stagefright/MediaSource.h>
 #include <stagefright/TimeSource.h>
 #include <utils/threads.h>
 #include <utils/RefBase.h>
 
 #include "AudioOutput.h"
 #include "AudioOffloadPlayerBase.h"
 #include "MediaDecoderOwner.h"
-#include "MediaOmxCommonDecoder.h"
+#include "MediaEventSource.h"
 
 namespace mozilla {
 
 namespace dom {
 class WakeLock;
 }
 
 /**
@@ -53,32 +53,34 @@ class WakeLock;
  * MediaOmxCommonDecoder to GonkAudioSink as well as provide GonkAudioSink status
  * (position changed, playback ended, seek complete, audio tear down) back to
  * MediaOmxCommonDecoder
  *
  * It acts as a bridge between MediaOmxCommonDecoder and GonkAudioSink during
  * offload playback
  */
 
+class MediaOmxCommonDecoder;
+
 class AudioOffloadPlayer : public AudioOffloadPlayerBase
 {
   typedef android::Mutex Mutex;
   typedef android::MetaData MetaData;
   typedef android::status_t status_t;
   typedef android::AudioTrack AudioTrack;
   typedef android::MediaBuffer MediaBuffer;
   typedef android::MediaSource MediaSource;
 
 public:
   enum {
     REACHED_EOS,
     SEEK_COMPLETE
   };
 
-  AudioOffloadPlayer(MediaOmxCommonDecoder* aDecoder = nullptr);
+  AudioOffloadPlayer(MediaOmxCommonDecoder* aDecoder);
 
   ~AudioOffloadPlayer();
 
   // Caller retains ownership of "aSource".
   void SetSource(const android::sp<MediaSource> &aSource) override;
 
   // Start the source if it's not already started and open the GonkAudioSink to
   // create an offloaded audio track
@@ -170,33 +172,39 @@ private:
   // Audio sink wrapper to access offloaded audio tracks
   // Used in main thread and offload callback thread
   // Race conditions are protected in underlying Android::AudioTrack class
   android::sp<GonkAudioSink> mAudioSink;
 
   // Buffer used to get date from audio source. Used in offload callback thread
   MediaBuffer* mInputBuffer;
 
-  // MediaOmxCommonDecoder object used mainly to notify the audio sink status
-  MediaOmxCommonDecoder* mObserver;
-
   TimeStamp mLastFireUpdateTime;
 
   // Timer to trigger position changed events
   nsCOMPtr<nsITimer> mTimeUpdateTimer;
 
   // Timer to reset GonkAudioSink when audio is paused for OFFLOAD_PAUSE_MAX_USECS.
   // It is triggered in Pause() and canceled when there is a Play() within
   // OFFLOAD_PAUSE_MAX_USECS. Used only from main thread so no lock is needed.
   nsCOMPtr<nsITimer> mResetTimer;
 
   // To avoid device suspend when mResetTimer is going to be triggered.
   // Used only from main thread so no lock is needed.
   RefPtr<mozilla::dom::WakeLock> mWakeLock;
 
+  MediaEventProducer<void> mOnPositionChanged;
+  MediaEventProducer<void> mOnPlaybackEnded;
+  MediaEventProducer<void> mOnPlayerTearDown;
+  MediaEventProducer<MediaDecoderEventVisibility> mOnSeekingStarted;
+  MediaEventListener mPositionChanged;
+  MediaEventListener mPlaybackEnded;
+  MediaEventListener mPlayerTearDown;
+  MediaEventListener mSeekingStarted;
+
   // Provide the playback position in microseconds from total number of
   // frames played by audio track
   int64_t GetOutputPlayPositionUs_l() const;
 
   // Fill the buffer given by audio sink with data from compressed audio
   // source. Also handles the seek by seeking audio source and stop the sink in
   // case of error
   size_t FillBuffer(void *aData, size_t aSize);
--- a/dom/media/omx/MediaOmxCommonDecoder.cpp
+++ b/dom/media/omx/MediaOmxCommonDecoder.cpp
@@ -160,16 +160,17 @@ MediaOmxCommonDecoder::ResumeStateMachin
   GetStateMachine()->DispatchSetDormant(false);
   UpdateLogicalPosition();
 }
 
 void
 MediaOmxCommonDecoder::AudioOffloadTearDown()
 {
   MOZ_ASSERT(NS_IsMainThread());
+  MOZ_ASSERT(!IsShutdown());
   DECODER_LOG(LogLevel::Debug, ("%s", __PRETTY_FUNCTION__));
 
   // mAudioOffloadPlayer can be null here if ResumeStateMachine was called
   // just before because of some other error.
   if (mAudioOffloadPlayer) {
     ResumeStateMachine();
   }
 }
@@ -281,9 +282,16 @@ MediaOmxCommonDecoder::CreateStateMachin
 {
   mReader = CreateReader();
   if (mReader != nullptr) {
     mReader->SetAudioChannel(GetAudioChannel());
   }
   return CreateStateMachineFromReader(mReader);
 }
 
+void
+MediaOmxCommonDecoder::Shutdown()
+{
+  mAudioOffloadPlayer = nullptr;
+  MediaDecoder::Shutdown();
+}
+
 } // namespace mozilla
--- a/dom/media/omx/MediaOmxCommonDecoder.h
+++ b/dom/media/omx/MediaOmxCommonDecoder.h
@@ -41,16 +41,18 @@ public:
 
   MediaDecoderStateMachine* CreateStateMachine() override;
 
   virtual MediaOmxCommonReader* CreateReader() = 0;
   virtual MediaDecoderStateMachine* CreateStateMachineFromReader(MediaOmxCommonReader* aReader) = 0;
 
   void NotifyOffloadPlayerPositionChanged() { UpdateLogicalPosition(); }
 
+  void Shutdown() override;
+
 protected:
   virtual ~MediaOmxCommonDecoder();
   void PauseStateMachine();
   void ResumeStateMachine();
   bool CheckDecoderCanOffloadAudio();
   void DisableStateMachineAudioOffloading();
 
   MediaOmxCommonReader* mReader;
--- a/dom/media/omx/MediaOmxReader.cpp
+++ b/dom/media/omx/MediaOmxReader.cpp
@@ -352,17 +352,17 @@ bool MediaOmxReader::DecodeVideoFrame(bo
     doSeek = false;
     mVideoSeekTimeUs = -1;
 
     // Ignore empty buffer which stagefright media read will sporadically return
     if (frame.mSize == 0 && !frame.mGraphicBuffer) {
       continue;
     }
 
-    a.mParsed++;
+    a.mStats.mParsedFrames++;
     if (frame.mShouldSkip && mSkipCount < MAX_DROPPED_FRAMES) {
       mSkipCount++;
       continue;
     }
 
     mSkipCount = 0;
 
     aKeyframeSkip = false;
@@ -429,18 +429,18 @@ bool MediaOmxReader::DecodeVideoFrame(bo
                             picture);
     }
 
     if (!v) {
       NS_WARNING("Unable to create VideoData");
       return false;
     }
 
-    a.mDecoded++;
-    NS_ASSERTION(a.mDecoded <= a.mParsed, "Expect to decode fewer frames than parsed in OMX decoder...");
+    a.mStats.mDecodedFrames++;
+    NS_ASSERTION(a.mStats.mDecodedFrames <= a.mStats.mParsedFrames, "Expect to decode fewer frames than parsed in OMX decoder...");
 
     mVideoQueue.Push(v);
 
     break;
   }
 
   return true;
 }
--- a/dom/media/raw/RawReader.cpp
+++ b/dom/media/raw/RawReader.cpp
@@ -151,17 +151,17 @@ bool RawReader::DecodeVideoFrame(bool &a
         !(header.packetID == 0xFF && header.codecID == RAW_ID /* "YUV" */)) {
       return false;
     }
 
     if (!ReadFromResource(buffer.get(), length)) {
       return false;
     }
 
-    a.mParsed++;
+    a.mStats.mParsedFrames++;
 
     if (currentFrameTime >= aTimeThreshold)
       break;
 
     mCurrentFrame++;
     currentFrameTime += static_cast<double>(USECS_PER_S) / mFrameRate;
   }
 
@@ -195,17 +195,17 @@ bool RawReader::DecodeVideoFrame(bool &a
                                             1, // In raw video every frame is a keyframe
                                             -1,
                                             mPicture);
   if (!v)
     return false;
 
   mVideoQueue.Push(v);
   mCurrentFrame++;
-  a.mDecoded++;
+  a.mStats.mDecodedFrames++;
 
   return true;
 }
 
 RefPtr<MediaDecoderReader::SeekPromise>
 RawReader::Seek(SeekTarget aTarget, int64_t aEndTime)
 {
   MOZ_ASSERT(OnTaskQueue());
--- a/dom/media/test/test_multiple_mediastreamtracks.html
+++ b/dom/media/test/test_multiple_mediastreamtracks.html
@@ -5,28 +5,30 @@
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
   <script type="text/javascript" src="manifest.js"></script>
 </head>
 <body>
 <pre id="test">
 <script class="testbody" type="text/javascript">
 function startTest() {
-  navigator.mediaDevices.getUserMedia({audio:true, video:true, fake:true, fakeTracks:true})
-  .then(function(stream) {
+  navigator.mediaDevices.getUserMedia({audio:true, video:true, fake:true})
+  .then(function(orgStream) {
+    var a = orgStream.getAudioTracks()[0];
+    var v = orgStream.getVideoTracks()[0];
+    var stream = new MediaStream([a, a, a, a, v, v, v].map(track => track.clone()));
     var element = document.createElement("video");
 
     element.onloadedmetadata = function() {
       is(stream.getAudioTracks().length, 4, 'Length of audio tracks should be 4.');
       is(stream.getVideoTracks().length, 3, 'Length of vudio tracks should be 3.');
       SimpleTest.finish();
     };
 
-    mStream = stream;
-    element.srcObject = mStream;
+    element.srcObject = stream;
     element.play();
   })
   .catch(function(reason) {
     ok(false, "unexpected error = " + reason.message);
     SimpleTest.finish();
   });
 }
 
--- a/dom/media/webaudio/BufferDecoder.cpp
+++ b/dom/media/webaudio/BufferDecoder.cpp
@@ -37,18 +37,17 @@ BufferDecoder::BeginDecoding(TaskQueue* 
 
 MediaResource*
 BufferDecoder::GetResource() const
 {
   return mResource;
 }
 
 void
-BufferDecoder::NotifyDecodedFrames(uint32_t aParsed, uint32_t aDecoded,
-                                   uint32_t aDropped)
+BufferDecoder::NotifyDecodedFrames(const FrameStatisticsData& aStats)
 {
   // ignore
 }
 
 VideoFrameContainer*
 BufferDecoder::GetVideoFrameContainer()
 {
   // no video frame
--- a/dom/media/webaudio/BufferDecoder.h
+++ b/dom/media/webaudio/BufferDecoder.h
@@ -28,18 +28,17 @@ public:
 
   NS_DECL_THREADSAFE_ISUPPORTS
 
   // This has to be called before decoding begins
   void BeginDecoding(TaskQueue* aTaskQueueIdentity);
 
   MediaResource* GetResource() const final override;
 
-  void NotifyDecodedFrames(uint32_t aParsed, uint32_t aDecoded,
-                           uint32_t aDropped) final override;
+  void NotifyDecodedFrames(const FrameStatisticsData& aStats) final override;
 
   VideoFrameContainer* GetVideoFrameContainer() final override;
   layers::ImageContainer* GetImageContainer() final override;
 
   MediaDecoderOwner* GetOwner() final override;
 
   already_AddRefed<GMPCrashHelper> GetCrashHelper() override;
 
--- a/dom/media/webrtc/MediaEngine.h
+++ b/dom/media/webrtc/MediaEngine.h
@@ -3,16 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef MEDIAENGINE_H_
 #define MEDIAENGINE_H_
 
 #include "mozilla/RefPtr.h"
 #include "DOMMediaStream.h"
 #include "MediaStreamGraph.h"
+#include "MediaTrackConstraints.h"
 #include "mozilla/dom/MediaStreamTrackBinding.h"
 #include "mozilla/dom/VideoStreamTrack.h"
 
 namespace mozilla {
 
 namespace dom {
 class Blob;
 } // namespace dom
@@ -27,17 +28,16 @@ enum {
  * Abstract interface for managing audio and video devices. Each platform
  * must implement a concrete class that will map these classes and methods
  * to the appropriate backend. For example, on Desktop platforms, these will
  * correspond to equivalent webrtc (GIPS) calls, and on B2G they will map to
  * a Gonk interface.
  */
 class MediaEngineVideoSource;
 class MediaEngineAudioSource;
-class MediaEnginePrefs;
 
 enum MediaEngineState {
   kAllocated,
   kStarted,
   kStopped,
   kReleased
 };
 
@@ -75,164 +75,16 @@ public:
 
   virtual void Shutdown() = 0;
 
 protected:
   virtual ~MediaEngine() {}
 };
 
 /**
- * Callback interface for TakePhoto(). Either PhotoComplete() or PhotoError()
- * should be called.
- */
-class MediaEnginePhotoCallback {
-public:
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaEnginePhotoCallback)
-
-  // aBlob is the image captured by MediaEngineSource. It is
-  // called on main thread.
-  virtual nsresult PhotoComplete(already_AddRefed<dom::Blob> aBlob) = 0;
-
-  // It is called on main thread. aRv is the error code.
-  virtual nsresult PhotoError(nsresult aRv) = 0;
-
-protected:
-  virtual ~MediaEnginePhotoCallback() {}
-};
-
-/**
- * Common abstract base class for audio and video sources.
- */
-class MediaEngineSource : public nsISupports
-{
-public:
-  // code inside webrtc.org assumes these sizes; don't use anything smaller
-  // without verifying it's ok
-  static const unsigned int kMaxDeviceNameLength = 128;
-  static const unsigned int kMaxUniqueIdLength = 256;
-
-  virtual ~MediaEngineSource() {}
-
-  virtual void Shutdown() = 0;
-
-  /* Populate the human readable name of this device in the nsAString */
-  virtual void GetName(nsAString&) const = 0;
-
-  /* Populate the UUID of this device in the nsACString */
-  virtual void GetUUID(nsACString&) const = 0;
-
-  class BaseAllocationHandle
-  {
-  public:
-    NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BaseAllocationHandle);
-  protected:
-    virtual ~BaseAllocationHandle() {}
-  };
-
-  /* Release the device back to the system. */
-  virtual nsresult Deallocate(BaseAllocationHandle* aHandle) = 0;
-
-  /* Start the device and add the track to the provided SourceMediaStream, with
-   * the provided TrackID. You may start appending data to the track
-   * immediately after. */
-  virtual nsresult Start(SourceMediaStream*, TrackID, const PrincipalHandle&) = 0;
-
-  /* tell the source if there are any direct listeners attached */
-  virtual void SetDirectListeners(bool) = 0;
-
-  /* Called when the stream wants more data */
-  virtual void NotifyPull(MediaStreamGraph* aGraph,
-                          SourceMediaStream *aSource,
-                          TrackID aId,
-                          StreamTime aDesiredTime,
-                          const PrincipalHandle& aPrincipalHandle) = 0;
-
-  /* Stop the device and release the corresponding MediaStream */
-  virtual nsresult Stop(SourceMediaStream *aSource, TrackID aID) = 0;
-
-  /* Restart with new capability */
-  virtual nsresult Restart(BaseAllocationHandle* aHandle,
-                           const dom::MediaTrackConstraints& aConstraints,
-                           const MediaEnginePrefs &aPrefs,
-                           const nsString& aDeviceId,
-                           const char** aOutBadConstraint) = 0;
-
-  /* Returns true if a source represents a fake capture device and
-   * false otherwise
-   */
-  virtual bool IsFake() = 0;
-
-  /* Returns the type of media source (camera, microphone, screen, window, etc) */
-  virtual dom::MediaSourceEnum GetMediaSource() const = 0;
-
-  /* If implementation of MediaEngineSource supports TakePhoto(), the picture
-   * should be return via aCallback object. Otherwise, it returns NS_ERROR_NOT_IMPLEMENTED.
-   * Currently, only Gonk MediaEngineSource implementation supports it.
-   */
-  virtual nsresult TakePhoto(MediaEnginePhotoCallback* aCallback) = 0;
-
-  /* Return false if device is currently allocated or started */
-  bool IsAvailable() {
-    if (mState == kAllocated || mState == kStarted) {
-      return false;
-    } else {
-      return true;
-    }
-  }
-
-  /* It is an error to call Start() before an Allocate(), and Stop() before
-   * a Start(). Only Allocate() may be called after a Deallocate(). */
-
-  void SetHasFakeTracks(bool aHasFakeTracks) {
-    mHasFakeTracks = aHasFakeTracks;
-  }
-
-  /* This call reserves but does not start the device. */
-  virtual nsresult Allocate(const dom::MediaTrackConstraints &aConstraints,
-                            const MediaEnginePrefs &aPrefs,
-                            const nsString& aDeviceId,
-                            const nsACString& aOrigin,
-                            BaseAllocationHandle** aOutHandle,
-                            const char** aOutBadConstraint) = 0;
-
-  virtual uint32_t GetBestFitnessDistance(
-      const nsTArray<const NormalizedConstraintSet*>& aConstraintSets,
-      const nsString& aDeviceId) const = 0;
-
-  void GetSettings(dom::MediaTrackSettings& aOutSettings)
-  {
-    MOZ_ASSERT(NS_IsMainThread());
-    aOutSettings = mSettings;
-  }
-
-protected:
-  // Only class' own members can be initialized in constructor initializer list.
-  explicit MediaEngineSource(MediaEngineState aState)
-    : mState(aState)
-#ifdef DEBUG
-    , mOwningThread(PR_GetCurrentThread())
-#endif
-    , mHasFakeTracks(false)
-  {}
-
-  void AssertIsOnOwningThread()
-  {
-    MOZ_ASSERT(PR_GetCurrentThread() == mOwningThread);
-  }
-
-  MediaEngineState mState;
-#ifdef DEBUG
-  PRThread* mOwningThread;
-#endif
-  bool mHasFakeTracks;
-  // Main-thread only:
-  dom::MediaTrackSettings mSettings;
-};
-
-/**
  * Video source and friends.
  */
 class MediaEnginePrefs {
 public:
   MediaEnginePrefs()
     : mWidth(0)
     , mHeight(0)
     , mFPS(0)
@@ -296,16 +148,301 @@ private:
     if (aHD) {
       return MediaEngine::DEFAULT_169_VIDEO_HEIGHT;
     }
 
     return MediaEngine::DEFAULT_43_VIDEO_HEIGHT;
   }
 };
 
+/**
+ * Callback interface for TakePhoto(). Either PhotoComplete() or PhotoError()
+ * should be called.
+ */
+class MediaEnginePhotoCallback {
+public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaEnginePhotoCallback)
+
+  // aBlob is the image captured by MediaEngineSource. It is
+  // called on main thread.
+  virtual nsresult PhotoComplete(already_AddRefed<dom::Blob> aBlob) = 0;
+
+  // It is called on main thread. aRv is the error code.
+  virtual nsresult PhotoError(nsresult aRv) = 0;
+
+protected:
+  virtual ~MediaEnginePhotoCallback() {}
+};
+
+/**
+ * Common abstract base class for audio and video sources.
+ *
+ * By default, the base class implements Allocate and Deallocate using its
+ * UpdateSingleSource pattern, which manages allocation handles and calculates
+ * net constraints from competing allocations and updates a single shared device.
+ *
+ * Classes that don't operate as a single shared device can override Allocate
+ * and Deallocate and simply not pass the methods up.
+ */
+class MediaEngineSource : public nsISupports,
+                          protected MediaConstraintsHelper
+{
+public:
+  // code inside webrtc.org assumes these sizes; don't use anything smaller
+  // without verifying it's ok
+  static const unsigned int kMaxDeviceNameLength = 128;
+  static const unsigned int kMaxUniqueIdLength = 256;
+
+  virtual ~MediaEngineSource()
+  {
+    if (!mInShutdown) {
+      Shutdown();
+    }
+  }
+
+  virtual void Shutdown()
+  {
+    mInShutdown = true;
+  };
+
+  /* Populate the human readable name of this device in the nsAString */
+  virtual void GetName(nsAString&) const = 0;
+
+  /* Populate the UUID of this device in the nsACString */
+  virtual void GetUUID(nsACString&) const = 0;
+
+  class AllocationHandle
+  {
+  public:
+    NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AllocationHandle);
+  protected:
+    ~AllocationHandle() {}
+  public:
+    AllocationHandle(const dom::MediaTrackConstraints& aConstraints,
+                     const nsACString& aOrigin,
+                     const MediaEnginePrefs& aPrefs,
+                     const nsString& aDeviceId)
+    : mConstraints(aConstraints),
+      mOrigin(aOrigin),
+      mPrefs(aPrefs),
+      mDeviceId(aDeviceId) {}
+  public:
+    NormalizedConstraints mConstraints;
+    nsCString mOrigin;
+    MediaEnginePrefs mPrefs;
+    nsString mDeviceId;
+  };
+
+  /* Release the device back to the system. */
+  virtual nsresult Deallocate(AllocationHandle* aHandle)
+  {
+    MOZ_ASSERT(aHandle);
+    RefPtr<AllocationHandle> handle = aHandle;
+
+    class Comparator {
+    public:
+      static bool Equals(const RefPtr<AllocationHandle>& a,
+                         const RefPtr<AllocationHandle>& b) {
+        return a.get() == b.get();
+      }
+    };
+    MOZ_ASSERT(mRegisteredHandles.Contains(handle, Comparator()));
+    mRegisteredHandles.RemoveElementAt(mRegisteredHandles.IndexOf(handle, 0,
+                                                                  Comparator()));
+    if (mRegisteredHandles.Length() && !mInShutdown) {
+      // Whenever constraints are removed, other parties may get closer to ideal.
+      auto& first = mRegisteredHandles[0];
+      const char* badConstraint = nullptr;
+      return ReevaluateAllocation(nullptr, nullptr, first->mPrefs,
+                                  first->mDeviceId, &badConstraint);
+    }
+    return NS_OK;
+  }
+
+  /* Start the device and add the track to the provided SourceMediaStream, with
+   * the provided TrackID. You may start appending data to the track
+   * immediately after. */
+  virtual nsresult Start(SourceMediaStream*, TrackID, const PrincipalHandle&) = 0;
+
+  /* tell the source if there are any direct listeners attached */
+  virtual void SetDirectListeners(bool) = 0;
+
+  /* Called when the stream wants more data */
+  virtual void NotifyPull(MediaStreamGraph* aGraph,
+                          SourceMediaStream *aSource,
+                          TrackID aId,
+                          StreamTime aDesiredTime,
+                          const PrincipalHandle& aPrincipalHandle) = 0;
+
+  /* Stop the device and release the corresponding MediaStream */
+  virtual nsresult Stop(SourceMediaStream *aSource, TrackID aID) = 0;
+
+  /* Restart with new capability */
+  virtual nsresult Restart(AllocationHandle* aHandle,
+                           const dom::MediaTrackConstraints& aConstraints,
+                           const MediaEnginePrefs &aPrefs,
+                           const nsString& aDeviceId,
+                           const char** aOutBadConstraint) = 0;
+
+  /* Returns true if a source represents a fake capture device and
+   * false otherwise
+   */
+  virtual bool IsFake() = 0;
+
+  /* Returns the type of media source (camera, microphone, screen, window, etc) */
+  virtual dom::MediaSourceEnum GetMediaSource() const = 0;
+
+  /* If implementation of MediaEngineSource supports TakePhoto(), the picture
+   * should be return via aCallback object. Otherwise, it returns NS_ERROR_NOT_IMPLEMENTED.
+   * Currently, only Gonk MediaEngineSource implementation supports it.
+   */
+  virtual nsresult TakePhoto(MediaEnginePhotoCallback* aCallback) = 0;
+
+  /* Return false if device is currently allocated or started */
+  bool IsAvailable() {
+    if (mState == kAllocated || mState == kStarted) {
+      return false;
+    } else {
+      return true;
+    }
+  }
+
+  /* It is an error to call Start() before an Allocate(), and Stop() before
+   * a Start(). Only Allocate() may be called after a Deallocate(). */
+
+  /* This call reserves but does not start the device. */
+  virtual nsresult Allocate(const dom::MediaTrackConstraints &aConstraints,
+                            const MediaEnginePrefs &aPrefs,
+                            const nsString& aDeviceId,
+                            const nsACString& aOrigin,
+                            AllocationHandle** aOutHandle,
+                            const char** aOutBadConstraint)
+  {
+    AssertIsOnOwningThread();
+    MOZ_ASSERT(aOutHandle);
+    RefPtr<AllocationHandle> handle = new AllocationHandle(aConstraints, aOrigin,
+                                                           aPrefs, aDeviceId);
+    nsresult rv = ReevaluateAllocation(handle, nullptr, aPrefs, aDeviceId,
+                                       aOutBadConstraint);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+    mRegisteredHandles.AppendElement(handle);
+    handle.forget(aOutHandle);
+    return NS_OK;
+  }
+
+  virtual uint32_t GetBestFitnessDistance(
+      const nsTArray<const NormalizedConstraintSet*>& aConstraintSets,
+      const nsString& aDeviceId) const = 0;
+
+  void GetSettings(dom::MediaTrackSettings& aOutSettings)
+  {
+    MOZ_ASSERT(NS_IsMainThread());
+    aOutSettings = mSettings;
+  }
+
+protected:
+  // Only class' own members can be initialized in constructor initializer list.
+  explicit MediaEngineSource(MediaEngineState aState)
+    : mState(aState)
+#ifdef DEBUG
+    , mOwningThread(PR_GetCurrentThread())
+#endif
+    , mInShutdown(false)
+  {}
+
+  /* UpdateSingleSource - Centralized abstract function to implement in those
+   * cases where a single device is being shared between users. Should apply net
+   * constraints and restart the device as needed.
+   *
+   * aHandle           - New or existing handle, or null to update after removal.
+   * aNetConstraints   - Net constraints to be applied to the single device.
+   * aPrefs            - As passed in (in case of changes in about:config).
+   * aDeviceId         - As passed in (origin dependent).
+   * aOutBadConstraint - Result: nonzero if failed to apply. Name of culprit.
+   */
+
+  virtual nsresult
+  UpdateSingleSource(const AllocationHandle* aHandle,
+                     const NormalizedConstraints& aNetConstraints,
+                     const MediaEnginePrefs& aPrefs,
+                     const nsString& aDeviceId,
+                     const char** aOutBadConstraint) {
+    return NS_ERROR_NOT_IMPLEMENTED;
+  };
+
+  /* ReevaluateAllocation - Call to change constraints for an allocation of
+   * a single device. Manages allocation handles, calculates net constraints
+   * from all competing allocations, and calls UpdateSingleSource with the net
+   * result, to restart the single device as needed.
+   *
+   * aHandle            - New or existing handle, or null to update after removal.
+   * aConstraintsUpdate - Constraints to be applied to existing handle, or null.
+   * aPrefs             - As passed in (in case of changes from about:config).
+   * aDeviceId          - As passed in (origin-dependent id).
+   * aOutBadConstraint  - Result: nonzero if failed to apply. Name of culprit.
+   */
+
+  nsresult
+  ReevaluateAllocation(AllocationHandle* aHandle,
+                       NormalizedConstraints* aConstraintsUpdate,
+                       const MediaEnginePrefs& aPrefs,
+                       const nsString& aDeviceId,
+                       const char** aOutBadConstraint)
+  {
+    // aHandle and/or aConstraintsUpdate may be nullptr (see below)
+
+    AutoTArray<const NormalizedConstraints*, 10> allConstraints;
+    for (auto& registered : mRegisteredHandles) {
+      if (aConstraintsUpdate && registered.get() == aHandle) {
+        continue; // Don't count old constraints
+      }
+      allConstraints.AppendElement(&registered->mConstraints);
+    }
+    if (aConstraintsUpdate) {
+      allConstraints.AppendElement(aConstraintsUpdate);
+    } else if (aHandle) {
+      // In the case of AddShareOfSingleSource, the handle isn't registered yet.
+      allConstraints.AppendElement(&aHandle->mConstraints);
+    }
+
+    NormalizedConstraints netConstraints(allConstraints);
+    if (netConstraints.mBadConstraint) {
+      *aOutBadConstraint = netConstraints.mBadConstraint;
+      return NS_ERROR_FAILURE;
+    }
+
+    nsresult rv = UpdateSingleSource(aHandle, netConstraints, aPrefs, aDeviceId,
+                                     aOutBadConstraint);
+    if (NS_FAILED(rv)) {
+      return rv;
+    }
+    if (aHandle && aConstraintsUpdate) {
+      aHandle->mConstraints = *aConstraintsUpdate;
+    }
+    return NS_OK;
+  }
+
+  void AssertIsOnOwningThread()
+  {
+    MOZ_ASSERT(PR_GetCurrentThread() == mOwningThread);
+  }
+
+  MediaEngineState mState;
+#ifdef DEBUG
+  PRThread* mOwningThread;
+#endif
+  nsTArray<RefPtr<AllocationHandle>> mRegisteredHandles;
+  bool mInShutdown;
+
+  // Main-thread only:
+  dom::MediaTrackSettings mSettings;
+};
+
 class MediaEngineVideoSource : public MediaEngineSource
 {
 public:
   virtual ~MediaEngineVideoSource() {}
 
 protected:
   explicit MediaEngineVideoSource(MediaEngineState aState)
     : MediaEngineSource(aState) {}
--- a/dom/media/webrtc/MediaEngineCameraVideoSource.h
+++ b/dom/media/webrtc/MediaEngineCameraVideoSource.h
@@ -1,48 +1,48 @@
 /* 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/. */
 
 #ifndef MediaEngineCameraVideoSource_h
 #define MediaEngineCameraVideoSource_h
 
 #include "MediaEngine.h"
-#include "MediaTrackConstraints.h"
 
 #include "nsDirectoryServiceDefs.h"
 
 // conflicts with #include of scoped_ptr.h
 #undef FF
 #include "webrtc/video_engine/include/vie_capture.h"
 
 namespace mozilla {
 
 bool operator == (const webrtc::CaptureCapability& a,
                   const webrtc::CaptureCapability& b);
 bool operator != (const webrtc::CaptureCapability& a,
                   const webrtc::CaptureCapability& b);
 
-class MediaEngineCameraVideoSource : public MediaEngineVideoSource,
-                                     protected MediaConstraintsHelper
+class MediaEngineCameraVideoSource : public MediaEngineVideoSource
 {
 public:
+  // Some subclasses use an index to track multiple instances.
   explicit MediaEngineCameraVideoSource(int aIndex,
                                         const char* aMonitorName = "Camera.Monitor")
     : MediaEngineVideoSource(kReleased)
     , mMonitor(aMonitorName)
     , mWidth(0)
     , mHeight(0)
     , mInitDone(false)
     , mHasDirectListeners(false)
-    , mNrAllocations(0)
     , mCaptureIndex(aIndex)
     , mTrackID(0)
   {}
 
+  explicit MediaEngineCameraVideoSource(const char* aMonitorName = "Camera.Monitor")
+    : MediaEngineCameraVideoSource(0, aMonitorName) {}
 
   void GetName(nsAString& aName) const override;
   void GetUUID(nsACString& aUUID) const override;
   void SetDirectListeners(bool aHasListeners) override;
 
   bool IsFake() override
   {
     return false;
@@ -109,17 +109,16 @@ protected:
   RefPtr<layers::Image> mImage;
   RefPtr<layers::ImageContainer> mImageContainer;
   int mWidth, mHeight; // protected with mMonitor on Gonk due to different threading
   // end of data protected by mMonitor
 
 
   bool mInitDone;
   bool mHasDirectListeners;
-  int mNrAllocations; // When this becomes 0, we shut down HW
   int mCaptureIndex;
   TrackID mTrackID;
 
   webrtc::CaptureCapability mCapability;
 
   mutable nsTArray<webrtc::CaptureCapability> mHardcodedCapabilities;
 private:
   nsString mDeviceName;
--- a/dom/media/webrtc/MediaEngineDefault.cpp
+++ b/dom/media/webrtc/MediaEngineDefault.cpp
@@ -27,28 +27,23 @@
 #endif
 
 #define AUDIO_RATE mozilla::MediaEngine::DEFAULT_SAMPLE_RATE
 #define DEFAULT_AUDIO_TIMER_MS 10
 namespace mozilla {
 
 using namespace mozilla::gfx;
 
-// Enable the testing flag fakeTracks and fake in MediaStreamConstraints, will
-// return you a MediaStream with additional fake video tracks and audio tracks.
-static const int kFakeVideoTrackCount = 2;
-static const int kFakeAudioTrackCount = 3;
-
 NS_IMPL_ISUPPORTS(MediaEngineDefaultVideoSource, nsITimerCallback)
 /**
  * Default video source.
  */
 
 MediaEngineDefaultVideoSource::MediaEngineDefaultVideoSource()
-  : MediaEngineVideoSource(kReleased)
+  : MediaEngineCameraVideoSource("FakeVideo.Monitor")
   , mTimer(nullptr)
   , mMonitor("Fake video")
   , mCb(16), mCr(16)
 {
   mImageContainer =
     layers::LayerManager::CreateImageContainer(layers::ImageContainer::ASYNCHRONOUS);
 }
 
@@ -84,39 +79,44 @@ MediaEngineDefaultVideoSource::GetBestFi
   return distance;
 }
 
 nsresult
 MediaEngineDefaultVideoSource::Allocate(const dom::MediaTrackConstraints &aConstraints,
                                         const MediaEnginePrefs &aPrefs,
                                         const nsString& aDeviceId,
                                         const nsACString& aOrigin,
-                                        BaseAllocationHandle** aOutHandle,
+                                        AllocationHandle** aOutHandle,
                                         const char** aOutBadConstraint)
 {
   if (mState != kReleased) {
     return NS_ERROR_FAILURE;
   }
 
+  FlattenedConstraints c(aConstraints);
+
   // Mock failure for automated tests.
-  if (aConstraints.mDeviceId.IsString() &&
-      aConstraints.mDeviceId.GetAsString().EqualsASCII("bad device")) {
+  if (c.mDeviceId.mIdeal.find(NS_LITERAL_STRING("bad device")) !=
+      c.mDeviceId.mIdeal.end()) {
     return NS_ERROR_FAILURE;
   }
 
+
   mOpts = aPrefs;
-  mOpts.mWidth = mOpts.mWidth ? mOpts.mWidth : MediaEngine::DEFAULT_43_VIDEO_WIDTH;
-  mOpts.mHeight = mOpts.mHeight ? mOpts.mHeight : MediaEngine::DEFAULT_43_VIDEO_HEIGHT;
+  mOpts.mWidth = c.mWidth.Get(aPrefs.mWidth ? aPrefs.mWidth :
+                              MediaEngine::DEFAULT_43_VIDEO_WIDTH);
+  mOpts.mHeight = c.mHeight.Get(aPrefs.mHeight ? aPrefs.mHeight :
+                                MediaEngine::DEFAULT_43_VIDEO_HEIGHT);
   mState = kAllocated;
   aOutHandle = nullptr;
   return NS_OK;
 }
 
 nsresult
-MediaEngineDefaultVideoSource::Deallocate(BaseAllocationHandle* aHandle)
+MediaEngineDefaultVideoSource::Deallocate(AllocationHandle* aHandle)
 {
   MOZ_ASSERT(!aHandle);
   if (mState != kStopped && mState != kAllocated) {
     return NS_ERROR_FAILURE;
   }
   mState = kReleased;
   mImage = nullptr;
   return NS_OK;
@@ -165,22 +165,16 @@ MediaEngineDefaultVideoSource::Start(Sou
 
   mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
   if (!mTimer) {
     return NS_ERROR_FAILURE;
   }
 
   aStream->AddTrack(aID, 0, new VideoSegment(), SourceMediaStream::ADDTRACK_QUEUED);
 
-  if (mHasFakeTracks) {
-    for (int i = 0; i < kFakeVideoTrackCount; ++i) {
-      aStream->AddTrack(kTrackCount + i, 0, new VideoSegment(), SourceMediaStream::ADDTRACK_QUEUED);
-    }
-  }
-
   // Remember TrackID so we can end it later
   mTrackID = aID;
 
   // Start timer for subsequent frames
 #if defined(MOZ_WIDGET_GONK) && defined(DEBUG)
 // B2G emulator debug is very, very slow and has problems dealing with realtime audio inputs
   mTimer->InitWithCallback(this, (1000 / mOpts.mFPS)*10, nsITimer::TYPE_REPEATING_SLACK);
 #else
@@ -200,30 +194,25 @@ MediaEngineDefaultVideoSource::Stop(Sour
   if (!mTimer) {
     return NS_ERROR_FAILURE;
   }
 
   mTimer->Cancel();
   mTimer = nullptr;
 
   aSource->EndTrack(aID);
-  if (mHasFakeTracks) {
-    for (int i = 0; i < kFakeVideoTrackCount; ++i) {
-      aSource->EndTrack(kTrackCount + i);
-    }
-  }
 
   mState = kStopped;
   mImage = nullptr;
   return NS_OK;
 }
 
 nsresult
 MediaEngineDefaultVideoSource::Restart(
-    BaseAllocationHandle* aHandle,
+    AllocationHandle* aHandle,
     const dom::MediaTrackConstraints& aConstraints,
     const MediaEnginePrefs &aPrefs,
     const nsString& aDeviceId,
     const char** aOutBadConstraint)
 {
   return NS_OK;
 }
 
@@ -304,24 +293,16 @@ MediaEngineDefaultVideoSource::NotifyPul
 
   if (delta > 0) {
     // nullptr images are allowed
     IntSize size(image ? mOpts.mWidth : 0, image ? mOpts.mHeight : 0);
     segment.AppendFrame(image.forget(), delta, size, aPrincipalHandle);
     // This can fail if either a) we haven't added the track yet, or b)
     // we've removed or finished the track.
     aSource->AppendToTrack(aID, &segment);
-    // Generate null data for fake tracks.
-    if (mHasFakeTracks) {
-      for (int i = 0; i < kFakeVideoTrackCount; ++i) {
-        VideoSegment nullSegment;
-        nullSegment.AppendNullData(delta);
-        aSource->AppendToTrack(kTrackCount + i, &nullSegment);
-      }
-    }
   }
 }
 
 // generate 1k sine wave per second
 class SineWaveGenerator
 {
 public:
   static const int bytesPerSample = 2;
@@ -412,17 +393,17 @@ MediaEngineDefaultAudioSource::GetBestFi
   return distance;
 }
 
 nsresult
 MediaEngineDefaultAudioSource::Allocate(const dom::MediaTrackConstraints &aConstraints,
                                         const MediaEnginePrefs &aPrefs,
                                         const nsString& aDeviceId,
                                         const nsACString& aOrigin,
-                                        BaseAllocationHandle** aOutHandle,
+                                        AllocationHandle** aOutHandle,
                                         const char** aOutBadConstraint)
 {
   if (mState != kReleased) {
     return NS_ERROR_FAILURE;
   }
 
   // Mock failure for automated tests.
   if (aConstraints.mDeviceId.IsString() &&
@@ -434,17 +415,17 @@ MediaEngineDefaultAudioSource::Allocate(
   // generate sine wave (default 1KHz)
   mSineGenerator = new SineWaveGenerator(AUDIO_RATE,
                                          static_cast<uint32_t>(aPrefs.mFreq ? aPrefs.mFreq : 1000));
   aOutHandle = nullptr;
   return NS_OK;
 }
 
 nsresult
-MediaEngineDefaultAudioSource::Deallocate(BaseAllocationHandle* aHandle)
+MediaEngineDefaultAudioSource::Deallocate(AllocationHandle* aHandle)
 {
   MOZ_ASSERT(!aHandle);
   if (mState != kStopped && mState != kAllocated) {
     return NS_ERROR_FAILURE;
   }
   mState = kReleased;
   return NS_OK;
 }
@@ -468,25 +449,16 @@ MediaEngineDefaultAudioSource::Start(Sou
   // Make it two timer intervals to try to avoid underruns.
   mBufferSize = 2 * (AUDIO_RATE * DEFAULT_AUDIO_TIMER_MS) / 1000;
 
   // AddTrack will take ownership of segment
   AudioSegment* segment = new AudioSegment();
   AppendToSegment(*segment, mBufferSize);
   mSource->AddAudioTrack(aID, AUDIO_RATE, 0, segment, SourceMediaStream::ADDTRACK_QUEUED);
 
-  if (mHasFakeTracks) {
-    for (int i = 0; i < kFakeAudioTrackCount; ++i) {
-      segment = new AudioSegment();
-      segment->AppendNullData(mBufferSize);
-      mSource->AddAudioTrack(kTrackCount + kFakeVideoTrackCount+i,
-                             AUDIO_RATE, 0, segment, SourceMediaStream::ADDTRACK_QUEUED);
-    }
-  }
-
   // Remember TrackID so we can finish later
   mTrackID = aID;
 
   // Remember PrincipalHandle since we don't append in NotifyPull.
   mPrincipalHandle = aPrincipalHandle;
 
   mLastNotify = TimeStamp::Now();
 
@@ -513,28 +485,23 @@ MediaEngineDefaultAudioSource::Stop(Sour
   if (!mTimer) {
     return NS_ERROR_FAILURE;
   }
 
   mTimer->Cancel();
   mTimer = nullptr;
 
   aSource->EndTrack(aID);
-  if (mHasFakeTracks) {
-    for (int i = 0; i < kFakeAudioTrackCount; ++i) {
-      aSource->EndTrack(kTrackCount + kFakeVideoTrackCount+i);
-    }
-  }
 
   mState = kStopped;
   return NS_OK;
 }
 
 nsresult
-MediaEngineDefaultAudioSource::Restart(BaseAllocationHandle* aHandle,
+MediaEngineDefaultAudioSource::Restart(AllocationHandle* aHandle,
                                        const dom::MediaTrackConstraints& aConstraints,
                                        const MediaEnginePrefs &aPrefs,
                                        const nsString& aDeviceId,
                                        const char** aOutBadConstraint)
 {
   return NS_OK;
 }
 
@@ -564,24 +531,16 @@ MediaEngineDefaultAudioSource::Notify(ns
   // have underrun and the MSG had to append silence while waiting for us
   // to push more data. In this case we reset to mBufferSize again.
   TrackTicks samplesToAppend = std::min(samplesSinceLastNotify, mBufferSize);
 
   AudioSegment segment;
   AppendToSegment(segment, samplesToAppend);
   mSource->AppendToTrack(mTrackID, &segment);
 
-  // Generate null data for fake tracks.
-  if (mHasFakeTracks) {
-    for (int i = 0; i < kFakeAudioTrackCount; ++i) {
-      AudioSegment nullSegment;
-      nullSegment.AppendNullData(samplesToAppend);
-      mSource->AppendToTrack(kTrackCount + kFakeVideoTrackCount+i, &nullSegment);
-    }
-  }
   return NS_OK;
 }
 
 void
 MediaEngineDefault::EnumerateVideoDevices(dom::MediaSourceEnum aMediaSource,
                                           nsTArray<RefPtr<MediaEngineVideoSource> >* aVSources) {
   MutexAutoLock lock(mMutex);
 
@@ -589,17 +548,16 @@ MediaEngineDefault::EnumerateVideoDevice
   if (aMediaSource != dom::MediaSourceEnum::Camera) {
     return;
   }
 
   // We once had code here to find a VideoSource with the same settings and re-use that.
   // This no longer is possible since the resolution is being set in Allocate().
 
   RefPtr<MediaEngineVideoSource> newSource = new MediaEngineDefaultVideoSource();
-  newSource->SetHasFakeTracks(mHasFakeTracks);
   mVSources.AppendElement(newSource);
   aVSources->AppendElement(newSource);
 
   return;
 }
 
 void
 MediaEngineDefault::EnumerateAudioDevices(dom::MediaSourceEnum aMediaSource,
@@ -615,16 +573,15 @@ MediaEngineDefault::EnumerateAudioDevice
       aASources->AppendElement(source);
     }
   }
 
   // All streams are currently busy, just make a new one.
   if (aASources->Length() == 0) {
     RefPtr<MediaEngineAudioSource> newSource =
       new MediaEngineDefaultAudioSource();
-    newSource->SetHasFakeTracks(mHasFakeTracks);
     mASources.AppendElement(newSource);
     aASources->AppendElement(newSource);
   }
   return;
 }
 
 } // namespace mozilla
--- a/dom/media/webrtc/MediaEngineDefault.h
+++ b/dom/media/webrtc/MediaEngineDefault.h
@@ -13,52 +13,50 @@
 #include "nsComponentManagerUtils.h"
 #include "mozilla/Monitor.h"
 
 #include "VideoUtils.h"
 #include "MediaEngine.h"
 #include "VideoSegment.h"
 #include "AudioSegment.h"
 #include "StreamTracks.h"
+#include "MediaEngineCameraVideoSource.h"
 #include "MediaStreamGraph.h"
 #include "MediaTrackConstraints.h"
 
 namespace mozilla {
 
 namespace layers {
 class ImageContainer;
 } // namespace layers
 
 class MediaEngineDefault;
 
 /**
  * The default implementation of the MediaEngine interface.
  */
 class MediaEngineDefaultVideoSource : public nsITimerCallback,
-                                      public MediaEngineVideoSource,
-                                      private MediaConstraintsHelper
+                                      public MediaEngineCameraVideoSource
 {
 public:
   MediaEngineDefaultVideoSource();
 
-  void Shutdown() override {};
-
   void GetName(nsAString&) const override;
   void GetUUID(nsACString&) const override;
 
   nsresult Allocate(const dom::MediaTrackConstraints &aConstraints,
                     const MediaEnginePrefs &aPrefs,
                     const nsString& aDeviceId,
                     const nsACString& aOrigin,
-                    BaseAllocationHandle** aOutHandle,
+                    AllocationHandle** aOutHandle,
                     const char** aOutBadConstraint) override;
-  nsresult Deallocate(BaseAllocationHandle* aHandle) override;
+  nsresult Deallocate(AllocationHandle* aHandle) override;
   nsresult Start(SourceMediaStream*, TrackID, const PrincipalHandle&) override;
   nsresult Stop(SourceMediaStream*, TrackID) override;
-  nsresult Restart(BaseAllocationHandle* aHandle,
+  nsresult Restart(AllocationHandle* aHandle,
                    const dom::MediaTrackConstraints& aConstraints,
                    const MediaEnginePrefs &aPrefs,
                    const nsString& aDeviceId,
                    const char** aOutBadConstraint) override;
   void SetDirectListeners(bool aHasDirectListeners) override {};
   void NotifyPull(MediaStreamGraph* aGraph,
                   SourceMediaStream *aSource,
                   TrackID aId,
@@ -103,37 +101,34 @@ protected:
   MediaEnginePrefs mOpts;
   int mCb;
   int mCr;
 };
 
 class SineWaveGenerator;
 
 class MediaEngineDefaultAudioSource : public nsITimerCallback,
-                                      public MediaEngineAudioSource,
-                                      private MediaConstraintsHelper
+                                      public MediaEngineAudioSource
 {
 public:
   MediaEngineDefaultAudioSource();
 
-  void Shutdown() override {};
-
   void GetName(nsAString&) const override;
   void GetUUID(nsACString&) const override;
 
   nsresult Allocate(const dom::MediaTrackConstraints &aConstraints,
                     const MediaEnginePrefs &aPrefs,
                     const nsString& aDeviceId,
                     const nsACString& aOrigin,
-                    BaseAllocationHandle** aOutHandle,
+                    AllocationHandle** aOutHandle,
                     const char** aOutBadConstraint) override;
-  nsresult Deallocate(BaseAllocationHandle* aHandle) override;
+  nsresult Deallocate(AllocationHandle* aHandle) override;
   nsresult Start(SourceMediaStream*, TrackID, const PrincipalHandle&) override;
   nsresult Stop(SourceMediaStream*, TrackID) override;
-  nsresult Restart(BaseAllocationHandle* aHandle,
+  nsresult Restart(AllocationHandle* aHandle,
                    const dom::MediaTrackConstraints& aConstraints,
                    const MediaEnginePrefs &aPrefs,
                    const nsString& aDeviceId,
                    const char** aOutBadConstraint) override;
   void SetDirectListeners(bool aHasDirectListeners) override {};
   void AppendToSegment(AudioSegment& aSegment,
                        TrackTicks aSamples);
   void NotifyPull(MediaStreamGraph* aGraph,
@@ -192,40 +187,33 @@ protected:
 
   SourceMediaStream* mSource;
   nsAutoPtr<SineWaveGenerator> mSineGenerator;
 };
 
 
 class MediaEngineDefault : public MediaEngine
 {
+  typedef MediaEngine Super;
 public:
-  explicit MediaEngineDefault(bool aHasFakeTracks = false)
-    : mHasFakeTracks(aHasFakeTracks)
-    , mMutex("mozilla::MediaEngineDefault")
-  {}
+  explicit MediaEngineDefault() : mMutex("mozilla::MediaEngineDefault") {}
 
   void EnumerateVideoDevices(dom::MediaSourceEnum,
                              nsTArray<RefPtr<MediaEngineVideoSource> >*) override;
   void EnumerateAudioDevices(dom::MediaSourceEnum,
                              nsTArray<RefPtr<MediaEngineAudioSource> >*) override;
   void Shutdown() override {
     MutexAutoLock lock(mMutex);
 
     mVSources.Clear();
     mASources.Clear();
   };
 
-protected:
-  bool mHasFakeTracks;
-
 private:
-  ~MediaEngineDefault() {
-    Shutdown();
-  }
+  ~MediaEngineDefault() {}
 
   Mutex mMutex;
   // protected with mMutex:
 
   nsTArray<RefPtr<MediaEngineVideoSource> > mVSources;
   nsTArray<RefPtr<MediaEngineAudioSource> > mASources;
 };
 
--- a/dom/media/webrtc/MediaEngineRemoteVideoSource.cpp
+++ b/dom/media/webrtc/MediaEngineRemoteVideoSource.cpp
@@ -27,18 +27,17 @@ using dom::ConstrainLongRange;
 
 NS_IMPL_ISUPPORTS0(MediaEngineRemoteVideoSource)
 
 MediaEngineRemoteVideoSource::MediaEngineRemoteVideoSource(
   int aIndex, mozilla::camera::CaptureEngine aCapEngine,
   dom::MediaSourceEnum aMediaSource, const char* aMonitorName)
   : MediaEngineCameraVideoSource(aIndex, aMonitorName),
     mMediaSource(aMediaSource),
-    mCapEngine(aCapEngine),
-    mInShutdown(false)
+    mCapEngine(aCapEngine)
 {
   MOZ_ASSERT(aMediaSource != dom::MediaSourceEnum::Other);
   mSettings.mWidth.Construct(0);
   mSettings.mHeight.Construct(0);
   mSettings.mFrameRate.Construct(0);
   Init();
 }
 
@@ -67,17 +66,17 @@ MediaEngineRemoteVideoSource::Init()
 
 void
 MediaEngineRemoteVideoSource::Shutdown()
 {
   LOG((__PRETTY_FUNCTION__));
   if (!mInitDone) {
     return;
   }
-  mInShutdown = true;
+  Super::Shutdown();
   if (mState == kStarted) {
     SourceMediaStream *source;
     bool empty;
 
     while (1) {
       {
         MonitorAutoLock lock(mMonitor);
         empty = mSources.IsEmpty();
@@ -103,90 +102,64 @@ MediaEngineRemoteVideoSource::Shutdown()
 }
 
 nsresult
 MediaEngineRemoteVideoSource::Allocate(
     const dom::MediaTrackConstraints& aConstraints,
     const MediaEnginePrefs& aPrefs,
     const nsString& aDeviceId,
     const nsACString& aOrigin,
-    BaseAllocationHandle** aOutHandle,
+    AllocationHandle** aOutHandle,
     const char** aOutBadConstraint)
 {
   LOG((__PRETTY_FUNCTION__));
   AssertIsOnOwningThread();
 
   if (!mInitDone) {
     LOG(("Init not done"));
     return NS_ERROR_FAILURE;
   }
 
-  RefPtr<AllocationHandle> handle = new AllocationHandle(aConstraints, aOrigin,
-                                                         aPrefs, aDeviceId);
-
-  nsresult rv = UpdateNew(handle, aPrefs, aDeviceId, aOutBadConstraint);
+  nsresult rv = Super::Allocate(aConstraints, aPrefs, aDeviceId, aOrigin,
+                                aOutHandle, aOutBadConstraint);
   if (NS_FAILED(rv)) {
     return rv;
   }
   if (mState == kStarted &&
       MOZ_LOG_TEST(GetMediaManagerLog(), mozilla::LogLevel::Debug)) {
     MonitorAutoLock lock(mMonitor);
     if (mSources.IsEmpty()) {
       MOZ_ASSERT(mPrincipalHandles.IsEmpty());
       LOG(("Video device %d reallocated", mCaptureIndex));
     } else {
       LOG(("Video device %d allocated shared", mCaptureIndex));
     }
   }
-  mRegisteredHandles.AppendElement(handle);
-  ++mNrAllocations;
-  handle.forget(aOutHandle);
   return NS_OK;
 }
 
 nsresult
-MediaEngineRemoteVideoSource::Deallocate(BaseAllocationHandle* aHandle)
+MediaEngineRemoteVideoSource::Deallocate(AllocationHandle* aHandle)
 {
   LOG((__PRETTY_FUNCTION__));
   AssertIsOnOwningThread();
-  MOZ_ASSERT(aHandle);
-  RefPtr<AllocationHandle> handle = static_cast<AllocationHandle*>(aHandle);
 
-  class Comparator {
-  public:
-    static bool Equals(const RefPtr<AllocationHandle>& a,
-                       const RefPtr<AllocationHandle>& b) {
-      return a.get() == b.get();
-    }
-  };
-  MOZ_ASSERT(mRegisteredHandles.Contains(handle, Comparator()));
-  mRegisteredHandles.RemoveElementAt(mRegisteredHandles.IndexOf(handle, 0,
-                                                                Comparator()));
-  --mNrAllocations;
-  MOZ_ASSERT(mNrAllocations >= 0, "Double-deallocations are prohibited");
+  Super::Deallocate(aHandle);
 
-  if (mNrAllocations == 0) {
-    MOZ_ASSERT(!mRegisteredHandles.Length());
+  if (!mRegisteredHandles.Length()) {
     if (mState != kStopped && mState != kAllocated) {
       return NS_ERROR_FAILURE;
     }
     mozilla::camera::GetChildAndCall(
       &mozilla::camera::CamerasChild::ReleaseCaptureDevice,
       mCapEngine, mCaptureIndex);
     mState = kReleased;
     LOG(("Video device %d deallocated", mCaptureIndex));
   } else {
     LOG(("Video device %d deallocated but still in use", mCaptureIndex));
-    MOZ_ASSERT(mRegisteredHandles.Length());
-    if (!mInShutdown) {
-      // Whenever constraints are removed, other parties may get closer to ideal.
-      auto& first = mRegisteredHandles[0];
-      const char* badConstraint = nullptr;
-      return UpdateRemove(first->mPrefs, first->mDeviceId, &badConstraint);
-    }
   }
   return NS_OK;
 }
 
 nsresult
 MediaEngineRemoteVideoSource::Start(SourceMediaStream* aStream, TrackID aID,
                                     const PrincipalHandle& aPrincipalHandle)
 {
@@ -262,72 +235,49 @@ MediaEngineRemoteVideoSource::Stop(mozil
   mozilla::camera::GetChildAndCall(
     &mozilla::camera::CamerasChild::StopCapture,
     mCapEngine, mCaptureIndex);
 
   return NS_OK;
 }
 
 nsresult
-MediaEngineRemoteVideoSource::Restart(BaseAllocationHandle* aHandle,
+MediaEngineRemoteVideoSource::Restart(AllocationHandle* aHandle,
                                       const dom::MediaTrackConstraints& aConstraints,
                                       const MediaEnginePrefs& aPrefs,
                                       const nsString& aDeviceId,
                                       const char** aOutBadConstraint)
 {
   AssertIsOnOwningThread();
   if (!mInitDone) {
     LOG(("Init not done"));
     return NS_ERROR_FAILURE;
   }
   MOZ_ASSERT(aHandle);
   NormalizedConstraints constraints(aConstraints);
-  return UpdateExisting(static_cast<AllocationHandle*>(aHandle), &constraints,
-                        aPrefs, aDeviceId, aOutBadConstraint);
+  return ReevaluateAllocation(aHandle, &constraints, aPrefs, aDeviceId,
+                              aOutBadConstraint);
 }
 
 nsresult
-MediaEngineRemoteVideoSource::UpdateExisting(AllocationHandle* aHandle,
-                                             NormalizedConstraints* aNewConstraints,
-                                             const MediaEnginePrefs& aPrefs,
-                                             const nsString& aDeviceId,
-                                             const char** aOutBadConstraint)
+MediaEngineRemoteVideoSource::UpdateSingleSource(
+    const AllocationHandle* aHandle,
+    const NormalizedConstraints& aNetConstraints,
+    const MediaEnginePrefs& aPrefs,
+    const nsString& aDeviceId,
+    const char** aOutBadConstraint)
 {
-  // aHandle and/or aNewConstraints may be nullptr
-
-  AutoTArray<const NormalizedConstraints*, 10> allConstraints;
-  for (auto& registered : mRegisteredHandles) {
-    if (aNewConstraints && registered.get() == aHandle) {
-      continue; // Don't count old constraints
-    }
-    allConstraints.AppendElement(&registered->mConstraints);
-  }
-  if (aNewConstraints) {
-    allConstraints.AppendElement(aNewConstraints);
-  } else if (aHandle) {
-    // In the case of UpdateNew, the handle isn't registered yet.
-    allConstraints.AppendElement(&aHandle->mConstraints);
-  }
-
-  NormalizedConstraints netConstraints(allConstraints);
-  if (netConstraints.mBadConstraint) {
-    *aOutBadConstraint = netConstraints.mBadConstraint;
-    return NS_ERROR_FAILURE;
-  }
-
-  if (!ChooseCapability(netConstraints, aPrefs, aDeviceId)) {
-    *aOutBadConstraint = FindBadConstraint(netConstraints, *this, aDeviceId);
+  if (!ChooseCapability(aNetConstraints, aPrefs, aDeviceId)) {
+    *aOutBadConstraint = FindBadConstraint(aNetConstraints, *this, aDeviceId);
     return NS_ERROR_FAILURE;
   }
 
   switch (mState) {
     case kReleased:
       MOZ_ASSERT(aHandle);
-      MOZ_ASSERT(!aNewConstraints);
-      MOZ_ASSERT(!mRegisteredHandles.Length());
       if (camera::GetChildAndCall(&camera::CamerasChild::AllocateCaptureDevice,
                                   mCapEngine, GetUUID().get(),
                                   kMaxUniqueIdLength, mCaptureIndex,
                                   aHandle->mOrigin)) {
         return NS_ERROR_FAILURE;
       }
       mState = kAllocated;
       SetLastCapability(mCapability);
@@ -349,19 +299,16 @@ MediaEngineRemoteVideoSource::UpdateExis
       }
       break;
 
     default:
       LOG(("Video device %d %s in ignored state %d", mCaptureIndex,
              (aHandle? aHandle->mOrigin.get() : ""), mState));
       break;
   }
-  if (aHandle && aNewConstraints) {
-    aHandle->mConstraints = *aNewConstraints;
-  }
   return NS_OK;
 }
 
 void
 MediaEngineRemoteVideoSource::SetLastCapability(
     const webrtc::CaptureCapability& aCapability)
 {
   mLastCapability = mCapability;
--- a/dom/media/webrtc/MediaEngineRemoteVideoSource.h
+++ b/dom/media/webrtc/MediaEngineRemoteVideoSource.h
@@ -45,16 +45,17 @@ class I420VideoFrame;
 namespace mozilla {
 
 /**
  * The WebRTC implementation of the MediaEngine interface.
  */
 class MediaEngineRemoteVideoSource : public MediaEngineCameraVideoSource,
                                      public webrtc::ExternalRenderer
 {
+  typedef MediaEngineCameraVideoSource Super;
 public:
   NS_DECL_THREADSAFE_ISUPPORTS
 
   // ExternalRenderer
   int FrameSizeChange(unsigned int w, unsigned int h,
                       unsigned int streams) override;
   int DeliverFrame(unsigned char* buffer,
                    size_t size,
@@ -66,46 +67,26 @@ public:
   int DeliverI420Frame(const webrtc::I420VideoFrame& webrtc_frame) override { return 0; };
   bool IsTextureSupported() override { return false; };
 
   // MediaEngineCameraVideoSource
   MediaEngineRemoteVideoSource(int aIndex, mozilla::camera::CaptureEngine aCapEngine,
                                dom::MediaSourceEnum aMediaSource,
                                const char* aMonitorName = "RemoteVideo.Monitor");
 
-  class AllocationHandle : public BaseAllocationHandle
-  {
-  public:
-    AllocationHandle(const dom::MediaTrackConstraints& aConstraints,
-                     const nsACString& aOrigin,
-                     const MediaEnginePrefs& aPrefs,
-                     const nsString& aDeviceId)
-    : mConstraints(aConstraints),
-      mOrigin(aOrigin),
-      mPrefs(aPrefs),
-      mDeviceId(aDeviceId) {}
-  private:
-    ~AllocationHandle() override {}
-  public:
-    NormalizedConstraints mConstraints;
-    nsCString mOrigin;
-    MediaEnginePrefs mPrefs;
-    nsString mDeviceId;
-  };
-
   nsresult Allocate(const dom::MediaTrackConstraints& aConstraints,
                     const MediaEnginePrefs& aPrefs,
                     const nsString& aDeviceId,
                     const nsACString& aOrigin,
-                    BaseAllocationHandle** aOutHandle,
+                    AllocationHandle** aOutHandle,
                     const char** aOutBadConstraint) override;
-  nsresult Deallocate(BaseAllocationHandle* aHandle) override;
+  nsresult Deallocate(AllocationHandle* aHandle) override;
   nsresult Start(SourceMediaStream*, TrackID, const PrincipalHandle&) override;
   nsresult Stop(SourceMediaStream*, TrackID) override;
-  nsresult Restart(BaseAllocationHandle* aHandle,
+  nsresult Restart(AllocationHandle* aHandle,
                    const dom::MediaTrackConstraints& aConstraints,
                    const MediaEnginePrefs &aPrefs,
                    const nsString& aDeviceId,
                    const char** aOutBadConstraint) override;
   void NotifyPull(MediaStreamGraph* aGraph,
                   SourceMediaStream* aSource,
                   TrackID aId,
                   StreamTime aDesiredTime,
@@ -118,62 +99,34 @@ public:
                         const MediaEnginePrefs &aPrefs,
                         const nsString& aDeviceId) override;
 
   void Refresh(int aIndex);
 
   void Shutdown() override;
 
 protected:
-  ~MediaEngineRemoteVideoSource() { Shutdown(); }
+  ~MediaEngineRemoteVideoSource() { }
 
 private:
   // Initialize the needed Video engine interfaces.
   void Init();
   size_t NumCapabilities() const override;
   void GetCapability(size_t aIndex, webrtc::CaptureCapability& aOut) const override;
   void SetLastCapability(const webrtc::CaptureCapability& aCapability);
 
-  /* UpdateExisting - Centralized function to apply constraints and restart
-   * device as needed, considering all allocations and changes to one.
-   *
-   * aHandle           - New or existing handle, or null to update after removal.
-   * aNewConstraints   - Constraints to be applied to existing handle, or null.
-   * aPrefs            - As passed in (in case of changes in about:config).
-   * aDeviceId         - As passed in (origin dependent).
-   * aOutBadConstraint - Result: nonzero if failed to apply. Name of culprit.
-   */
-
   nsresult
-  UpdateExisting(AllocationHandle* aHandle,
-                 NormalizedConstraints* aNewConstraints,
-                 const MediaEnginePrefs& aPrefs,
-                 const nsString& aDeviceId,
-                 const char** aOutBadConstraint);
-
-  nsresult
-  UpdateNew(AllocationHandle* aHandle,
-            const MediaEnginePrefs& aPrefs,
-            const nsString& aDeviceId,
-            const char** aOutBadConstraint) {
-    return UpdateExisting(aHandle, nullptr, aPrefs, aDeviceId, aOutBadConstraint);
-  }
-
-  nsresult
-  UpdateRemove(const MediaEnginePrefs& aPrefs,
-               const nsString& aDeviceId,
-               const char** aOutBadConstraint) {
-    return UpdateExisting(nullptr, nullptr, aPrefs, aDeviceId, aOutBadConstraint);
-  }
+  UpdateSingleSource(const AllocationHandle* aHandle,
+                     const NormalizedConstraints& aNetConstraints,
+                     const MediaEnginePrefs& aPrefs,
+                     const nsString& aDeviceId,
+                     const char** aOutBadConstraint) override;
 
   dom::MediaSourceEnum mMediaSource; // source of media (camera | application | screen)
   mozilla::camera::CaptureEngine mCapEngine;
 
-  nsTArray<RefPtr<AllocationHandle>> mRegisteredHandles;
-
   // To only restart camera when needed, we keep track previous settings.
   webrtc::CaptureCapability mLastCapability;
-  bool mInShutdown;
 };
 
 }
 
 #endif /* MEDIAENGINE_REMOTE_VIDEO_SOURCE_H_ */
--- a/dom/media/webrtc/MediaEngineTabVideoSource.cpp
+++ b/dom/media/webrtc/MediaEngineTabVideoSource.cpp
@@ -135,30 +135,30 @@ MediaEngineTabVideoSource::GetUUID(nsACS
 #define DEFAULT_TABSHARE_VIDEO_MAX_HEIGHT 4096
 #define DEFAULT_TABSHARE_VIDEO_FRAMERATE 30
 
 nsresult
 MediaEngineTabVideoSource::Allocate(const dom::MediaTrackConstraints& aConstraints,
                                     const MediaEnginePrefs& aPrefs,
                                     const nsString& aDeviceId,
                                     const nsACString& aOrigin,
-                                    BaseAllocationHandle** aOutHandle,
+                                    AllocationHandle** aOutHandle,
                                     const char** aOutBadConstraint)
 {
   // windowId is not a proper constraint, so just read it.
   // It has no well-defined behavior in advanced, so ignore it there.
 
   mWindowId = aConstraints.mBrowserWindow.WasPassed() ?
               aConstraints.mBrowserWindow.Value() : -1;
   aOutHandle = nullptr;
   return Restart(nullptr, aConstraints, aPrefs, aDeviceId, aOutBadConstraint);
 }
 
 nsresult
-MediaEngineTabVideoSource::Restart(BaseAllocationHandle* aHandle,
+MediaEngineTabVideoSource::Restart(AllocationHandle* aHandle,
                                    const dom::MediaTrackConstraints& aConstraints,
                                    const mozilla::MediaEnginePrefs& aPrefs,
                                    const nsString& aDeviceId,
                                    const char** aOutBadConstraint)
 {
   MOZ_ASSERT(!aHandle);
 
   // scrollWithPage is not proper a constraint, so just read it.
@@ -179,17 +179,17 @@ MediaEngineTabVideoSource::Restart(BaseA
     mViewportOffsetY = c.mViewportOffsetY.Get(0);
     mViewportWidth = c.mViewportWidth.Get(INT32_MAX);
     mViewportHeight = c.mViewportHeight.Get(INT32_MAX);
   }
   return NS_OK;
 }
 
 nsresult
-MediaEngineTabVideoSource::Deallocate(BaseAllocationHandle* aHandle)
+MediaEngineTabVideoSource::Deallocate(AllocationHandle* aHandle)
 {
   MOZ_ASSERT(!aHandle);
   return NS_OK;
 }
 
 nsresult
 MediaEngineTabVideoSource::Start(SourceMediaStream* aStream, TrackID aID,
                                  const PrincipalHandle& aPrincipalHandle)
--- a/dom/media/webrtc/MediaEngineTabVideoSource.h
+++ b/dom/media/webrtc/MediaEngineTabVideoSource.h
@@ -14,31 +14,30 @@ namespace mozilla {
 
 class MediaEngineTabVideoSource : public MediaEngineVideoSource, nsIDOMEventListener, nsITimerCallback {
   public:
     NS_DECL_THREADSAFE_ISUPPORTS
     NS_DECL_NSIDOMEVENTLISTENER
     NS_DECL_NSITIMERCALLBACK
     MediaEngineTabVideoSource();
 
-    void Shutdown() override {};
     void GetName(nsAString_internal&) const override;
     void GetUUID(nsACString_internal&) const override;
     nsresult Allocate(const dom::MediaTrackConstraints &,
                       const mozilla::MediaEnginePrefs&,
                       const nsString& aDeviceId,
                       const nsACString& aOrigin,
-                      BaseAllocationHandle** aOutHandle,
+                      AllocationHandle** aOutHandle,
                       const char** aOutBadConstraint) override;
-    nsresult Deallocate(BaseAllocationHandle* aHandle) override;
+    nsresult Deallocate(AllocationHandle* aHandle) override;
     nsresult Start(mozilla::SourceMediaStream*, mozilla::TrackID, const mozilla::PrincipalHandle&) override;
     void SetDirectListeners(bool aHasDirectListeners) override {};
     void NotifyPull(mozilla::MediaStreamGraph*, mozilla::SourceMediaStream*, mozilla::TrackID, mozilla::StreamTime, const mozilla::PrincipalHandle& aPrincipalHandle) override;
     nsresult Stop(mozilla::SourceMediaStream*, mozilla::TrackID) override;
-    nsresult Restart(BaseAllocationHandle* aHandle,
+    nsresult Restart(AllocationHandle* aHandle,
                      const dom::MediaTrackConstraints& aConstraints,
                      const mozilla::MediaEnginePrefs& aPrefs,
                      const nsString& aDeviceId,
                      const char** aOutBadConstraint) override;
     bool IsFake() override;
     dom::MediaSourceEnum GetMediaSource() const override {
       return dom::MediaSourceEnum::Browser;
     }
--- a/dom/media/webrtc/MediaEngineWebRTC.h
+++ b/dom/media/webrtc/MediaEngineWebRTC.h
@@ -73,38 +73,34 @@ public:
   {
   }
   void GetName(nsAString& aName) const override;
   void GetUUID(nsACString& aUUID) const override;
   nsresult Allocate(const dom::MediaTrackConstraints& aConstraints,
                     const MediaEnginePrefs& aPrefs,
                     const nsString& aDeviceId,
                     const nsACString& aOrigin,
-                    BaseAllocationHandle** aOutHandle,
+                    AllocationHandle** aOutHandle,
                     const char** aOutBadConstraint) override
   {
     // Nothing to do here, everything is managed in MediaManager.cpp
     aOutHandle = nullptr;
     return NS_OK;
   }
-  nsresult Deallocate(BaseAllocationHandle* aHandle) override
+  nsresult Deallocate(AllocationHandle* aHandle) override
   {
     // Nothing to do here, everything is managed in MediaManager.cpp
     MOZ_ASSERT(!aHandle);
     return NS_OK;
   }
-  void Shutdown() override
-  {
-    // Nothing to do here, everything is managed in MediaManager.cpp
-  }
   nsresult Start(SourceMediaStream* aMediaStream,
                  TrackID aId,
                  const PrincipalHandle& aPrincipalHandle) override;
   nsresult Stop(SourceMediaStream* aMediaStream, TrackID aId) override;
-  nsresult Restart(BaseAllocationHandle* aHandle,
+  nsresult Restart(AllocationHandle* aHandle,
                    const dom::MediaTrackConstraints& aConstraints,
                    const MediaEnginePrefs &aPrefs,
                    const nsString& aDeviceId,
                    const char** aOutBadConstraint) override;
   void SetDirectListeners(bool aDirect) override
   {}
   void NotifyOutputData(MediaStreamGraph* aGraph,
                         AudioDataValue* aBuffer, size_t aFrames,
@@ -134,17 +130,17 @@ public:
   {
     return NS_ERROR_NOT_IMPLEMENTED;
   }
   uint32_t GetBestFitnessDistance(
     const nsTArray<const NormalizedConstraintSet*>& aConstraintSets,
     const nsString& aDeviceId) const override;
 
 protected:
-  virtual ~MediaEngineWebRTCAudioCaptureSource() { Shutdown(); }
+  virtual ~MediaEngineWebRTCAudioCaptureSource() {}
   nsCString mUUID;
 };
 
 // Small subset of VoEHardware
 class AudioInput
 {
 public:
   explicit AudioInput(webrtc::VoiceEngine* aVoiceEngine) : mVoiceEngine(aVoiceEngine) {};
@@ -414,63 +410,36 @@ public:
   }
 
 private:
   Mutex mMutex;
   RefPtr<MediaEngineAudioSource> mAudioSource;
 };
 
 class MediaEngineWebRTCMicrophoneSource : public MediaEngineAudioSource,
-                                          public webrtc::VoEMediaProcess,
-                                          private MediaConstraintsHelper
+                                          public webrtc::VoEMediaProcess
 {
+  typedef MediaEngineAudioSource Super;
 public:
   MediaEngineWebRTCMicrophoneSource(nsIThread* aThread,
                                     webrtc::VoiceEngine* aVoiceEnginePtr,
                                     mozilla::AudioInput* aAudioInput,
                                     int aIndex,
                                     const char* name,
-                                    const char* uuid)
-    : MediaEngineAudioSource(kReleased)
-    , mVoiceEngine(aVoiceEnginePtr)
-    , mAudioInput(aAudioInput)
-    , mMonitor("WebRTCMic.Monitor")
-    , mThread(aThread)
-    , mCapIndex(aIndex)
-    , mChannel(-1)
-    , mNrAllocations(0)
-    , mStarted(false)
-    , mSampleFrequency(MediaEngine::DEFAULT_SAMPLE_RATE)
-    , mPlayoutDelay(0)
-    , mNullTransport(nullptr)
-    , mSkipProcessing(false)
-  {
-    MOZ_ASSERT(aVoiceEnginePtr);
-    MOZ_ASSERT(aAudioInput);
-    mDeviceName.Assign(NS_ConvertUTF8toUTF16(name));
-    mDeviceUUID.Assign(uuid);
-    mListener = new mozilla::WebRTCAudioDataListener(this);
-    // We'll init lazily as needed
-  }
+                                    const char* uuid);
 
   void GetName(nsAString& aName) const override;
   void GetUUID(nsACString& aUUID) const override;
 
-  nsresult Allocate(const dom::MediaTrackConstraints& aConstraints,
-                    const MediaEnginePrefs& aPrefs,
-                    const nsString& aDeviceId,
-                    const nsACString& aOrigin,
-                    BaseAllocationHandle** aOutHandle,
-                    const char** aOutBadConstraint) override;
-  nsresult Deallocate(BaseAllocationHandle* aHandle) override;
+  nsresult Deallocate(AllocationHandle* aHandle) override;
   nsresult Start(SourceMediaStream* aStream,
                  TrackID aID,
                  const PrincipalHandle& aPrincipalHandle) override;
   nsresult Stop(SourceMediaStream* aSource, TrackID aID) override;
-  nsresult Restart(BaseAllocationHandle* aHandle,
+  nsresult Restart(AllocationHandle* aHandle,
                    const dom::MediaTrackConstraints& aConstraints,
                    const MediaEnginePrefs &aPrefs,
                    const nsString& aDeviceId,
                    const char** aOutBadConstraint) override;
   void SetDirectListeners(bool aHasDirectListeners) override {};
 
   void NotifyPull(MediaStreamGraph* aGraph,
                   SourceMediaStream* aSource,
@@ -510,21 +479,28 @@ public:
                int16_t audio10ms[], int length,
                int samplingFreq, bool isStereo) override;
 
   void Shutdown() override;
 
   NS_DECL_THREADSAFE_ISUPPORTS
 
 protected:
-  ~MediaEngineWebRTCMicrophoneSource() {
-    Shutdown();
-  }
+  ~MediaEngineWebRTCMicrophoneSource() {}
 
 private:
+  nsresult
+  UpdateSingleSource(const AllocationHandle* aHandle,
+                     const NormalizedConstraints& aNetConstraints,
+                     const MediaEnginePrefs& aPrefs,
+                     const nsString& aDeviceId,
+                     const char** aOutBadConstraint) override;
+
+  void SetLastPrefs(const MediaEnginePrefs& aPrefs);
+
   // These allocate/configure and release the channel
   bool AllocChannel();
   void FreeChannel();
   // These start/stop VoEBase and associated interfaces
   bool InitEngine();
   void DeInitEngine();
 
   // This is true when all processing is disabled, we can skip
@@ -565,54 +541,56 @@ private:
   // threads.
   Monitor mMonitor;
   nsTArray<RefPtr<SourceMediaStream>> mSources;
   nsTArray<PrincipalHandle> mPrincipalHandles; // Maps to mSources.
 
   nsCOMPtr<nsIThread> mThread;
   int mCapIndex;
   int mChannel;
-  int mNrAllocations; // Per-channel - When this becomes 0, we shut down HW for the channel
   TrackID mTrackID;
   bool mStarted;
 
   nsString mDeviceName;
   nsCString mDeviceUUID;
 
   int32_t mSampleFrequency;
   int32_t mPlayoutDelay;
 
   NullTransport *mNullTransport;
 
   nsTArray<int16_t> mInputBuffer;
   // mSkipProcessing is true if none of the processing passes are enabled,
   // because of prefs or constraints. This allows simply copying the audio into
   // the MSG, skipping resampling and the whole webrtc.org code.
   bool mSkipProcessing;
+
+  // To only update microphone when needed, we keep track of previous settings.
+  MediaEnginePrefs mLastPrefs;
 };
 
 class MediaEngineWebRTC : public MediaEngine
 {
+  typedef MediaEngine Super;
 public:
   explicit MediaEngineWebRTC(MediaEnginePrefs& aPrefs);
 
   // Clients should ensure to clean-up sources video/audio sources
   // before invoking Shutdown on this class.
   void Shutdown() override;
 
   // Returns whether the host supports duplex audio stream.
   bool SupportsDuplex();
 
   void EnumerateVideoDevices(dom::MediaSourceEnum,
                              nsTArray<RefPtr<MediaEngineVideoSource>>*) override;
   void EnumerateAudioDevices(dom::MediaSourceEnum,
                              nsTArray<RefPtr<MediaEngineAudioSource>>*) override;
 private:
   ~MediaEngineWebRTC() {
-    Shutdown();
 #if defined(MOZ_B2G_CAMERA) && defined(MOZ_WIDGET_GONK)
     AsyncLatencyLogger::Get()->Release();
 #endif
     gFarendObserver = nullptr;
   }
 
   nsCOMPtr<nsIThread> mThread;
 
--- a/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
+++ b/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
@@ -178,16 +178,47 @@ AudioOutputObserver::InsertFarEnd(const 
         mPlayoutFifo->Push((int8_t *) mSaved); // takes ownership
         mSaved = nullptr;
         mSamplesSaved = 0;
       }
     }
   }
 }
 
+MediaEngineWebRTCMicrophoneSource::MediaEngineWebRTCMicrophoneSource(
+    nsIThread* aThread,
+    webrtc::VoiceEngine* aVoiceEnginePtr,
+    mozilla::AudioInput* aAudioInput,
+    int aIndex,
+    const char* name,
+    const char* uuid)
+  : MediaEngineAudioSource(kReleased)
+  , mVoiceEngine(aVoiceEnginePtr)
+  , mAudioInput(aAudioInput)
+  , mMonitor("WebRTCMic.Monitor")
+  , mThread(aThread)
+  , mCapIndex(aIndex)
+  , mChannel(-1)
+  , mStarted(false)
+  , mSampleFrequency(MediaEngine::DEFAULT_SAMPLE_RATE)
+  , mPlayoutDelay(0)
+  , mNullTransport(nullptr)
+  , mSkipProcessing(false)
+{
+  MOZ_ASSERT(aVoiceEnginePtr);
+  MOZ_ASSERT(aAudioInput);
+  mDeviceName.Assign(NS_ConvertUTF8toUTF16(name));
+  mDeviceUUID.Assign(uuid);
+  mListener = new mozilla::WebRTCAudioDataListener(this);
+  mSettings.mEchoCancellation.Construct(0);
+  mSettings.mMozAutoGainControl.Construct(0);
+  mSettings.mMozNoiseSuppression.Construct(0);
+  // We'll init lazily as needed
+}
+
 void
 MediaEngineWebRTCMicrophoneSource::GetName(nsAString& aName) const
 {
   aName.Assign(mDeviceName);
   return;
 }
 
 void
@@ -214,119 +245,162 @@ uint32_t MediaEngineWebRTCMicrophoneSour
   for (const auto* cs : aConstraintSets) {
     distance = GetMinimumFitnessDistance(*cs, aDeviceId);
     break; // distance is read from first entry only
   }
   return distance;
 }
 
 nsresult
-MediaEngineWebRTCMicrophoneSource::Allocate(const dom::MediaTrackConstraints &aConstraints,
-                                            const MediaEnginePrefs &aPrefs,
-                                            const nsString& aDeviceId,
-                                            const nsACString& aOrigin,
-                                            BaseAllocationHandle** aOutHandle,
-                                            const char** aOutBadConstraint)
-{
-  AssertIsOnOwningThread();
-  if (mState == kReleased) {
-    if (sChannelsOpen == 0) {
-      if (!InitEngine()) {
-        LOG(("Audio engine is not initalized"));
-        return NS_ERROR_FAILURE;
-      }
-    }
-    if (!AllocChannel()) {
-      if (sChannelsOpen == 0) {
-        DeInitEngine();
-      }
-      LOG(("Audio device is not initalized"));
-      return NS_ERROR_FAILURE;
-    }
-    if (mAudioInput->SetRecordingDevice(mCapIndex)) {
-      FreeChannel();
-      if (sChannelsOpen == 0) {
-        DeInitEngine();
-      }
-      return NS_ERROR_FAILURE;
-    }
-    sChannelsOpen++;
-    mState = kAllocated;
-    LOG(("Audio device %d allocated", mCapIndex));
-  } else if (MOZ_LOG_TEST(GetMediaManagerLog(), LogLevel::Debug)) {
-    MonitorAutoLock lock(mMonitor);
-    if (mSources.IsEmpty()) {
-      LOG(("Audio device %d reallocated", mCapIndex));
-    } else {
-      LOG(("Audio device %d allocated shared", mCapIndex));
-    }
-  }
-  ++mNrAllocations;
-  aOutHandle = nullptr;
-  return Restart(nullptr, aConstraints, aPrefs, aDeviceId, aOutBadConstraint);
-}
-
-nsresult
-MediaEngineWebRTCMicrophoneSource::Restart(BaseAllocationHandle* aHandle,
+MediaEngineWebRTCMicrophoneSource::Restart(AllocationHandle* aHandle,
                                            const dom::MediaTrackConstraints& aConstraints,
                                            const MediaEnginePrefs &aPrefs,
                                            const nsString& aDeviceId,
                                            const char** aOutBadConstraint)
 {
-  MOZ_ASSERT(!aHandle);
-  FlattenedConstraints c(aConstraints);
+  AssertIsOnOwningThread();
+  MOZ_ASSERT(aHandle);
+  NormalizedConstraints constraints(aConstraints);
+  return ReevaluateAllocation(aHandle, &constraints, aPrefs, aDeviceId,
+                              aOutBadConstraint);
+}
+
+bool operator == (const MediaEnginePrefs& a, const MediaEnginePrefs& b)
+{
+  return !memcmp(&a, &b, sizeof(MediaEnginePrefs));
+};
 
-  bool aec_on = c.mEchoCancellation.Get(aPrefs.mAecOn);
-  bool agc_on = c.mMozAutoGainControl.Get(aPrefs.mAgcOn);
-  bool noise_on = c.mMozNoiseSuppression.Get(aPrefs.mNoiseOn);
+nsresult
+MediaEngineWebRTCMicrophoneSource::UpdateSingleSource(
+    const AllocationHandle* aHandle,
+    const NormalizedConstraints& aNetConstraints,
+    const MediaEnginePrefs& aPrefs,
+    const nsString& aDeviceId,
+    const char** aOutBadConstraint)
+{
+  FlattenedConstraints c(aNetConstraints);
+
+  MediaEnginePrefs prefs = aPrefs;
+  prefs.mAecOn = c.mEchoCancellation.Get(prefs.mAecOn);
+  prefs.mAgcOn = c.mMozAutoGainControl.Get(prefs.mAgcOn);
+  prefs.mNoiseOn = c.mMozNoiseSuppression.Get(prefs.mNoiseOn);
 
   LOG(("Audio config: aec: %d, agc: %d, noise: %d, delay: %d",
-       aec_on ? aPrefs.mAec : -1,
-       agc_on ? aPrefs.mAgc : -1,
-       noise_on ? aPrefs.mNoise : -1,
-       aPrefs.mPlayoutDelay));
+       prefs.mAecOn ? prefs.mAec : -1,
+       prefs.mAgcOn ? prefs.mAgc : -1,
+       prefs.mNoiseOn ? prefs.mNoise : -1,
+       prefs.mPlayoutDelay));
+
+  mPlayoutDelay = prefs.mPlayoutDelay;
 
-  mPlayoutDelay = aPrefs.mPlayoutDelay;
+  switch (mState) {
+    case kReleased:
+      MOZ_ASSERT(aHandle);
+      if (sChannelsOpen == 0) {
+        if (!InitEngine()) {
+          LOG(("Audio engine is not initalized"));
+          return NS_ERROR_FAILURE;
+        }
+      }
+      if (!AllocChannel()) {
+        if (sChannelsOpen == 0) {
+          DeInitEngine();
+        }
+        LOG(("Audio device is not initalized"));
+        return NS_ERROR_FAILURE;
+      }
+      if (mAudioInput->SetRecordingDevice(mCapIndex)) {
+        FreeChannel();
+        if (sChannelsOpen == 0) {
+          DeInitEngine();
+        }
+        return NS_ERROR_FAILURE;
+      }
+      sChannelsOpen++;
+      mState = kAllocated;
+      LOG(("Audio device %d allocated", mCapIndex));
+      break;
+
+    case kStarted:
+      if (prefs == mLastPrefs) {
+        return NS_OK;
+      }
+      if (MOZ_LOG_TEST(GetMediaManagerLog(), LogLevel::Debug)) {
+        MonitorAutoLock lock(mMonitor);
+        if (mSources.IsEmpty()) {
+          LOG(("Audio device %d reallocated", mCapIndex));
+        } else {
+          LOG(("Audio device %d allocated shared", mCapIndex));
+        }
+      }
+      break;
+
+    default:
+      LOG(("Audio device %d %s in ignored state %d", mCapIndex,
+           (aHandle? aHandle->mOrigin.get() : ""), mState));
+      break;
+  }
 
   if (sChannelsOpen > 0) {
     int error;
 
-    if (0 != (error = mVoEProcessing->SetEcStatus(aec_on, (webrtc::EcModes) aPrefs.mAec))) {
+    error = mVoEProcessing->SetEcStatus(prefs.mAecOn, (webrtc::EcModes)prefs.mAec);
+    if (error) {
       LOG(("%s Error setting Echo Status: %d ",__FUNCTION__, error));
       // Overhead of capturing all the time is very low (<0.1% of an audio only call)
-      if (aec_on) {
-        if (0 != (error = mVoEProcessing->SetEcMetricsStatus(true))) {
+      if (prefs.mAecOn) {
+        error = mVoEProcessing->SetEcMetricsStatus(true);
+        if (error) {
           LOG(("%s Error setting Echo Metrics: %d ",__FUNCTION__, error));
         }
       }
     }
-    if (0 != (error = mVoEProcessing->SetAgcStatus(agc_on, (webrtc::AgcModes) aPrefs.mAgc))) {
+    error = mVoEProcessing->SetAgcStatus(prefs.mAgcOn, (webrtc::AgcModes)prefs.mAgc);
+    if (error) {
       LOG(("%s Error setting AGC Status: %d ",__FUNCTION__, error));
     }
-    if (0 != (error = mVoEProcessing->SetNsStatus(noise_on, (webrtc::NsModes) aPrefs.mNoise))) {
+    error = mVoEProcessing->SetNsStatus(prefs.mNoiseOn, (webrtc::NsModes)prefs.mNoise);
+    if (error) {
       LOG(("%s Error setting NoiseSuppression Status: %d ",__FUNCTION__, error));
     }
   }
 
-  mSkipProcessing = !(aec_on || agc_on || noise_on);
+  mSkipProcessing = !(prefs.mAecOn || prefs.mAgcOn || prefs.mNoiseOn);
   if (mSkipProcessing) {
     mSampleFrequency = MediaEngine::USE_GRAPH_RATE;
   }
-
+  SetLastPrefs(prefs);
   return NS_OK;
 }
 
+void
+MediaEngineWebRTCMicrophoneSource::SetLastPrefs(
+    const MediaEnginePrefs& aPrefs)
+{
+  mLastPrefs = aPrefs;
+
+  RefPtr<MediaEngineWebRTCMicrophoneSource> that = this;
+
+  NS_DispatchToMainThread(media::NewRunnableFrom([this, that, aPrefs]() mutable {
+    mSettings.mEchoCancellation.Value() = aPrefs.mAecOn;
+    mSettings.mMozAutoGainControl.Value() = aPrefs.mAgcOn;
+    mSettings.mMozNoiseSuppression.Value() = aPrefs.mNoiseOn;
+    return NS_OK;
+  }));
+}
+
+
 nsresult
-MediaEngineWebRTCMicrophoneSource::Deallocate(BaseAllocationHandle* aHandle)
+MediaEngineWebRTCMicrophoneSource::Deallocate(AllocationHandle* aHandle)
 {
   AssertIsOnOwningThread();
-  MOZ_ASSERT(!aHandle);
-  --mNrAllocations;
-  MOZ_ASSERT(mNrAllocations >= 0, "Double-deallocations are prohibited");
-  if (mNrAllocations == 0) {
+
+  Super::Deallocate(aHandle);
+
+  if (!mRegisteredHandles.Length()) {
     // If empty, no callbacks to deliver data should be occuring
     if (mState != kStopped && mState != kAllocated) {
       return NS_ERROR_FAILURE;
     }
 
     FreeChannel();
     mState = kReleased;
     LOG(("Audio device %d deallocated", mCapIndex));
@@ -709,16 +783,17 @@ MediaEngineWebRTCMicrophoneSource::FreeC
     mChannel = -1;
   }
   mState = kReleased;
 }
 
 void
 MediaEngineWebRTCMicrophoneSource::Shutdown()
 {
+  Super::Shutdown();
   if (mListener) {
     // breaks a cycle, since the WebRTCAudioDataListener has a RefPtr to us
     mListener->Shutdown();
     // Don't release the webrtc.org pointers yet until the Listener is (async) shutdown
     mListener = nullptr;
   }
 
   if (mState == kStarted) {
@@ -734,17 +809,17 @@ MediaEngineWebRTCMicrophoneSource::Shutd
         }
         source = mSources[0];
       }
       Stop(source, kAudioTrack); // XXX change to support multiple tracks
     }
     MOZ_ASSERT(mState == kStopped);
   }
 
-  while (mNrAllocations) {
+  while (mRegisteredHandles.Length()) {
     MOZ_ASSERT(mState == kAllocated || mState == kStopped);
     Deallocate(nullptr); // XXX Extend concurrent constraints code to mics.
   }
 
   FreeChannel();
   DeInitEngine();
 
   mAudioInput = nullptr;
@@ -842,17 +917,17 @@ MediaEngineWebRTCAudioCaptureSource::Sto
 {
   AssertIsOnOwningThread();
   aMediaStream->EndAllTrackAndFinish();
   return NS_OK;
 }
 
 nsresult
 MediaEngineWebRTCAudioCaptureSource::Restart(
-    BaseAllocationHandle* aHandle,
+    AllocationHandle* aHandle,
     const dom::MediaTrackConstraints& aConstraints,
     const MediaEnginePrefs &aPrefs,
     const nsString& aDeviceId,
     const char** aOutBadConstraint)
 {
   MOZ_ASSERT(!aHandle);
   return NS_OK;
 }
--- a/dom/media/webrtc/MediaTrackConstraints.cpp
+++ b/dom/media/webrtc/MediaTrackConstraints.cpp
@@ -1,14 +1,15 @@
 /* -*- 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 "MediaTrackConstraints.h"
+#include "mozilla/dom/MediaStreamTrackBinding.h"
 
 #include <limits>
 #include <algorithm>
 #include <iterator>
 
 namespace mozilla {
 
 template<class ValueType>
@@ -139,17 +140,17 @@ NormalizedConstraintSet::BooleanRange::B
 {
   if (aOther.IsBoolean()) {
     if (advanced) {
       mMin = mMax = aOther.GetAsBoolean();
     } else {
       mIdeal.emplace(aOther.GetAsBoolean());
     }
   } else {
-    const ConstrainBooleanParameters& r = aOther.GetAsConstrainBooleanParameters();
+    const dom::ConstrainBooleanParameters& r = aOther.GetAsConstrainBooleanParameters();
     if (r.mIdeal.WasPassed()) {
       mIdeal.emplace(r.mIdeal.Value());
     }
     if (r.mExact.WasPassed()) {
       mMin = r.mExact.Value();
       mMax = r.mExact.Value();
     }
   }
@@ -183,17 +184,17 @@ NormalizedConstraintSet::StringRange::St
     }
   } else {
     SetFrom(aOther.GetAsConstrainDOMStringParameters());
   }
 }
 
 void
 NormalizedConstraintSet::StringRange::SetFrom(
-    const ConstrainDOMStringParameters& aOther)
+    const dom::ConstrainDOMStringParameters& aOther)
 {
   if (aOther.mIdeal.WasPassed()) {
     mIdeal.clear();
     if (aOther.mIdeal.Value().IsString()) {
       mIdeal.insert(aOther.mIdeal.Value().GetAsString());
     } else {
       for (auto& str : aOther.mIdeal.Value().GetAsStringSequence()) {
         mIdeal.insert(str);
@@ -290,17 +291,17 @@ NormalizedConstraints::NormalizedConstra
 
 NormalizedConstraints::NormalizedConstraints(
     const nsTArray<const NormalizedConstraints*>& aOthers)
   : NormalizedConstraintSet(*aOthers[0])
   , mBadConstraint(nullptr)
 {
   // Create a list of member pointers.
   nsTArray<MemberPtrType> list;
-  NormalizedConstraints dummy(MediaTrackConstraints(), &list);
+  NormalizedConstraints dummy(dom::MediaTrackConstraints(), &list);
 
   // Do intersection of all required constraints, and average of ideals,
 
   for (uint32_t i = 1; i < aOthers.Length(); i++) {
     auto& other = *aOthers[i];
 
     for (auto& memberPtr : list) {
       auto& member = this->*memberPtr;
--- a/dom/media/webrtc/moz.build
+++ b/dom/media/webrtc/moz.build
@@ -28,17 +28,16 @@ if CONFIG['MOZ_WEBRTC']:
                 'MediaEngineRemoteVideoSource.h',
                 'MediaEngineWebRTC.h']
     EXPORTS.mozilla.dom += [ 'RTCIdentityProviderRegistrar.h' ]
     UNIFIED_SOURCES += [
         'MediaEngineCameraVideoSource.cpp',
         'MediaEngineRemoteVideoSource.cpp',
         'MediaEngineTabVideoSource.cpp',
         'MediaEngineWebRTCAudio.cpp',
-        'MediaTrackConstraints.cpp',
         'RTCCertificate.cpp',
         'RTCIdentityProviderRegistrar.cpp',
     ]
     # MediaEngineWebRTC.cpp needs to be built separately.
     SOURCES += [
         'MediaEngineWebRTC.cpp',
     ]
     LOCAL_INCLUDES += [
@@ -61,16 +60,17 @@ if CONFIG['MOZ_WEBRTC']:
         ]
 
 XPIDL_SOURCES += [
     'nsITabSource.idl'
 ]
 
 UNIFIED_SOURCES += [
     'MediaEngineDefault.cpp',
+    'MediaTrackConstraints.cpp',
     'PeerIdentity.cpp',
 ]
 
 EXPORTS.mozilla += [
     'PeerIdentity.h',
 ]
 EXPORTS.mozilla.dom += [
     'RTCCertificate.h',
--- a/dom/webidl/MediaStream.webidl
+++ b/dom/webidl/MediaStream.webidl
@@ -15,20 +15,16 @@
 
 dictionary MediaStreamConstraints {
     (boolean or MediaTrackConstraints) audio = false;
     (boolean or MediaTrackConstraints) video = false;
     boolean picture = false; // Mozilla legacy
     boolean fake;       // For testing purpose. Generates frames of solid
                         // colors if video is enabled, and sound of 1Khz sine
                         // wave if audio is enabled.
-    boolean fakeTracks; // For testing purpose, works only if fake is
-                        // enabled. Enable fakeTracks returns a stream
-                        // with two extra empty video tracks and three
-                        // extra empty audio tracks.
     DOMString? peerIdentity = null;
 };
 
 [Exposed=Window,
  Constructor,
  Constructor (MediaStream stream),
  Constructor (sequence<MediaStreamTrack> tracks)]
 interface MediaStream : EventTarget {
--- a/dom/webidl/MediaTrackSettings.webidl
+++ b/dom/webidl/MediaTrackSettings.webidl
@@ -8,16 +8,19 @@
  */
 
 dictionary MediaTrackSettings {
     long      width;
     long      height;
     double    frameRate;
     DOMString facingMode;
     DOMString deviceId;
+    boolean echoCancellation;
+    boolean mozNoiseSuppression;
+    boolean mozAutoGainControl;
 
     // Mozilla-specific extensions:
 
     // http://fluffy.github.io/w3c-screen-share/#screen-based-video-constraints
     // OBE by http://w3c.github.io/mediacapture-screen-share
 
     DOMString mediaSource;
 
--- a/js/src/gc/Heap.h
+++ b/js/src/gc/Heap.h
@@ -178,38 +178,38 @@ IsObjectAllocKind(AllocKind kind)
 inline bool
 IsShapeAllocKind(AllocKind kind)
 {
     return kind == AllocKind::SHAPE || kind == AllocKind::ACCESSOR_SHAPE;
 }
 
 // Returns a sequence for use in a range-based for loop,
 // to iterate over all alloc kinds.
-inline decltype(mozilla::MakeEnumeratedRange<int>(AllocKind::FIRST, AllocKind::LIMIT))
+inline decltype(mozilla::MakeEnumeratedRange(AllocKind::FIRST, AllocKind::LIMIT))
 AllAllocKinds()
 {
-    return mozilla::MakeEnumeratedRange<int>(AllocKind::FIRST, AllocKind::LIMIT);
+    return mozilla::MakeEnumeratedRange(AllocKind::FIRST, AllocKind::LIMIT);
 }
 
 // Returns a sequence for use in a range-based for loop,
 // to iterate over all object alloc kinds.
-inline decltype(mozilla::MakeEnumeratedRange<int>(AllocKind::OBJECT_FIRST, AllocKind::OBJECT_LIMIT))
+inline decltype(mozilla::MakeEnumeratedRange(AllocKind::OBJECT_FIRST, AllocKind::OBJECT_LIMIT))
 ObjectAllocKinds()
 {
-    return mozilla::MakeEnumeratedRange<int>(AllocKind::OBJECT_FIRST, AllocKind::OBJECT_LIMIT);
+    return mozilla::MakeEnumeratedRange(AllocKind::OBJECT_FIRST, AllocKind::OBJECT_LIMIT);
 }
 
 // Returns a sequence for use in a range-based for loop,
 // to iterate over alloc kinds from |first| to |limit|, exclusive.
-inline decltype(mozilla::MakeEnumeratedRange<int>(AllocKind::FIRST, AllocKind::LIMIT))
+inline decltype(mozilla::MakeEnumeratedRange(AllocKind::FIRST, AllocKind::LIMIT))
 SomeAllocKinds(AllocKind first = AllocKind::FIRST, AllocKind limit = AllocKind::LIMIT)
 {
     MOZ_ASSERT(IsAllocKind(first), "|first| is not a valid AllocKind!");
     MOZ_ASSERT(IsAllocKind(limit), "|limit| is not a valid AllocKind!");
-    return mozilla::MakeEnumeratedRange<int>(first, limit);
+    return mozilla::MakeEnumeratedRange(first, limit);
 }
 
 // AllAllocKindArray<ValueType> gives an enumerated array of ValueTypes,
 // with each index corresponding to a particular alloc kind.
 template<typename ValueType> using AllAllocKindArray =
     mozilla::EnumeratedArray<AllocKind, AllocKind::LIMIT, ValueType>;
 
 // ObjectAllocKindArray<ValueType> gives an enumerated array of ValueTypes,
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -2083,17 +2083,17 @@ nsComputedDOMStyle::SetValueToStyleImage
 {
   switch (aStyleImage.GetType()) {
     case eStyleImageType_Image:
     {
       imgIRequest *req = aStyleImage.GetImageData();
       nsCOMPtr<nsIURI> uri;
       req->GetURI(getter_AddRefs(uri));
 
-      const nsStyleSides* cropRect = aStyleImage.GetCropRect();
+      const UniquePtr<nsStyleSides>& cropRect = aStyleImage.GetCropRect();
       if (cropRect) {
         nsAutoString imageRectString;
         GetImageRectString(uri, *cropRect, imageRectString);
         aValue->SetString(imageRectString);
       } else {
         aValue->SetURI(uri);
       }
       break;
--- a/layout/style/nsRuleNode.cpp
+++ b/layout/style/nsRuleNode.cpp
@@ -1211,17 +1211,17 @@ static void SetStyleImageToImageRect(nsS
 
 #ifdef DEBUG
     bool unitOk =
 #endif
       SetAbsCoord(val, coord, SETCOORD_FACTOR | SETCOORD_PERCENT);
     MOZ_ASSERT(unitOk, "Incorrect data structure created by CSS parser");
     cropRect.Set(side, coord);
   }
-  aResult.SetCropRect(&cropRect);
+  aResult.SetCropRect(MakeUnique<nsStyleSides>(cropRect));
 }
 
 static void SetStyleImage(nsStyleContext* aStyleContext,
                           const nsCSSValue& aValue,
                           nsStyleImage& aResult,
                           RuleNodeCacheConditions& aConditions)
 {
   if (aValue.GetUnit() == eCSSUnit_Null) {
--- a/layout/style/nsStyleSet.cpp
+++ b/layout/style/nsStyleSet.cpp
@@ -13,16 +13,17 @@
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/CSSStyleSheet.h"
 #include "mozilla/EffectCompositor.h"
 #include "mozilla/EnumeratedRange.h"
 #include "mozilla/EventStates.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/RuleProcessorCache.h"
+#include "mozilla/StyleSheetHandleInlines.h"
 #include "nsIDocumentInlines.h"
 #include "nsRuleWalker.h"
 #include "nsStyleContext.h"
 #include "mozilla/css/StyleRule.h"
 #include "nsCSSAnonBoxes.h"
 #include "nsCSSPseudoElements.h"
 #include "nsCSSRuleProcessor.h"
 #include "nsDataHashtable.h"
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -15,16 +15,18 @@
 #include "nsThemeConstants.h"
 #include "nsString.h"
 #include "nsPresContext.h"
 #include "nsIAppShellService.h"
 #include "nsIWidget.h"
 #include "nsCRTGlue.h"
 #include "nsCSSParser.h"
 #include "nsCSSProps.h"
+#include "nsDeviceContext.h"
+#include "nsStyleUtil.h"
 
 #include "nsCOMPtr.h"
 
 #include "nsBidiUtils.h"
 #include "nsLayoutUtils.h"
 
 #include "imgIRequest.h"
 #include "imgIContainer.h"
@@ -1962,17 +1964,21 @@ nsStyleImage::DoCopy(const nsStyleImage&
   if (aOther.mType == eStyleImageType_Image) {
     SetImageData(aOther.mImage);
   } else if (aOther.mType == eStyleImageType_Gradient) {
     SetGradientData(aOther.mGradient);
   } else if (aOther.mType == eStyleImageType_Element) {
     SetElementId(aOther.mElementId);
   }
 
-  SetCropRect(aOther.mCropRect);
+  UniquePtr<nsStyleSides> cropRectCopy;
+  if (aOther.mCropRect) {
+    cropRectCopy = MakeUnique<nsStyleSides>(*aOther.mCropRect.get());
+  }
+  SetCropRect(Move(cropRectCopy));
 }
 
 void
 nsStyleImage::SetNull()
 {
   MOZ_ASSERT(!mImageTracked,
              "Calling SetNull() with image tracked!");
 
@@ -2073,24 +2079,19 @@ nsStyleImage::SetElementId(const char16_
 
   if (aElementId) {
     mElementId = NS_strdup(aElementId);
     mType = eStyleImageType_Element;
   }
 }
 
 void
-nsStyleImage::SetCropRect(nsStyleSides* aCropRect)
+nsStyleImage::SetCropRect(UniquePtr<nsStyleSides> aCropRect)
 {
-  if (aCropRect) {
-    mCropRect = new nsStyleSides(*aCropRect);
-    // There is really not much we can do if 'new' fails
-  } else {
-    mCropRect = nullptr;
-  }
+    mCropRect = Move(aCropRect);
 }
 
 static int32_t
 ConvertToPixelCoord(const nsStyleCoord& aCoord, int32_t aPercentScale)
 {
   double pixelValue;
   switch (aCoord.GetUnit()) {
     case eStyleUnit_Percent:
@@ -2232,17 +2233,17 @@ nsStyleImage::IsLoaded() const
     }
     default:
       NS_NOTREACHED("unexpected image type");
       return false;
   }
 }
 
 static inline bool
-EqualRects(const nsStyleSides* aRect1, const nsStyleSides* aRect2)
+EqualRects(const UniquePtr<nsStyleSides>& aRect1, const UniquePtr<nsStyleSides>& aRect2)
 {
   return aRect1 == aRect2 || /* handles null== null, and optimize */
          (aRect1 && aRect2 && *aRect1 == *aRect2);
 }
 
 bool
 nsStyleImage::operator==(const nsStyleImage& aOther) const
 {
--- a/layout/style/nsStyleStruct.h
+++ b/layout/style/nsStyleStruct.h
@@ -13,17 +13,17 @@
 #define nsStyleStruct_h___
 
 #include "mozilla/ArenaObjectID.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/CSSVariableValues.h"
 #include "mozilla/SheetType.h"
 #include "mozilla/StaticPtr.h"
 #include "mozilla/StyleStructContext.h"
-#include "nsAutoPtr.h"
+#include "mozilla/UniquePtr.h"
 #include "nsColor.h"
 #include "nsCoord.h"
 #include "nsMargin.h"
 #include "nsFont.h"
 #include "nsStyleCoord.h"
 #include "nsStyleConsts.h"
 #include "nsChangeHint.h"
 #include "nsPresContext.h"
@@ -264,17 +264,17 @@ struct nsStyleImage
   nsStyleImage& operator=(const nsStyleImage& aOther);
 
   void SetNull();
   void SetImageData(imgRequestProxy* aImage);
   void TrackImage(nsPresContext* aContext);
   void UntrackImage(nsPresContext* aContext);
   void SetGradientData(nsStyleGradient* aGradient);
   void SetElementId(const char16_t* aElementId);
-  void SetCropRect(nsStyleSides* aCropRect);
+  void SetCropRect(mozilla::UniquePtr<nsStyleSides> aCropRect);
 
   nsStyleImageType GetType() const {
     return mType;
   }
   imgRequestProxy* GetImageData() const {
     MOZ_ASSERT(mType == eStyleImageType_Image, "Data is not an image!");
     MOZ_ASSERT(mImageTracked,
                "Should be tracking any image we're going to use!");
@@ -283,17 +283,17 @@ struct nsStyleImage
   nsStyleGradient* GetGradientData() const {
     NS_ASSERTION(mType == eStyleImageType_Gradient, "Data is not a gradient!");
     return mGradient;
   }
   const char16_t* GetElementId() const {
     NS_ASSERTION(mType == eStyleImageType_Element, "Data is not an element!");
     return mElementId;
   }
-  nsStyleSides* GetCropRect() const {
+  const mozilla::UniquePtr<nsStyleSides>& GetCropRect() const {
     NS_ASSERTION(mType == eStyleImageType_Image,
                  "Only image data can have a crop rect");
     return mCropRect;
   }
 
   /**
    * Compute the actual crop rect in pixels, using the source image bounds.
    * The computation involves converting percentage unit to pixel unit and
@@ -368,17 +368,17 @@ private:
   nsStyleImageType mType;
   union {
     imgRequestProxy* mImage;
     nsStyleGradient* mGradient;
     char16_t* mElementId;
   };
 
   // This is _currently_ used only in conjunction with eStyleImageType_Image.
-  nsAutoPtr<nsStyleSides> mCropRect;
+  mozilla::UniquePtr<nsStyleSides> mCropRect;
 #ifdef DEBUG
   bool mImageTracked;
 #endif
 };
 
 struct MOZ_NEEDS_MEMMOVABLE_MEMBERS nsStyleColor
 {
   explicit nsStyleColor(StyleStructContext aContext);
--- a/media/mtransport/third_party/nICEr/src/net/transport_addr.c
+++ b/media/mtransport/third_party/nICEr/src/net/transport_addr.c
@@ -45,16 +45,17 @@ static char *RCSSTRING __UNUSED__="$Id: 
 #else
 #include <unistd.h>
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #endif
 #include <assert.h>
 #include "nr_api.h"
+#include "util.h"
 #include "transport_addr.h"
 
 int nr_transport_addr_fmt_addr_string(nr_transport_addr *addr)
   {
     int _status;
     /* Max length for normalized IPv6 address string representation is 39 */
     char buffer[40];
     const char *protocol;
--- a/media/mtransport/third_party/nICEr/src/net/transport_addr_reg.c
+++ b/media/mtransport/third_party/nICEr/src/net/transport_addr_reg.c
@@ -45,16 +45,17 @@ static char *RCSSTRING __UNUSED__="$Id: 
 #include <strings.h>
 #include <unistd.h>
 #include <sys/socket.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #endif
 #include <assert.h>
 #include "nr_api.h"
+#include "util.h"
 #include "transport_addr.h"
 #include "transport_addr_reg.h"
 
 #ifndef INET6_ADDRSTRLEN
 #define INET6_ADDRSTRLEN 46 /* Value used by linux/BSD */
 #endif
 
 int
--- a/media/mtransport/third_party/nICEr/src/stun/stun_client_ctx.c
+++ b/media/mtransport/third_party/nICEr/src/stun/stun_client_ctx.c
@@ -30,16 +30,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
 
 
 static char *RCSSTRING __UNUSED__="$Id: stun_client_ctx.c,v 1.2 2008/04/28 18:21:30 ekr Exp $";
 
 #include <assert.h>
 #include <string.h>
+#include <math.h>
 
 #include <nr_api.h>
 #include "stun.h"
 #include "async_timer.h"
 #include "registry.h"
 #include "stun_reg.h"
 #include "nr_crypto.h"
 #include "r_time.h"
--- a/mfbt/EnumeratedRange.h
+++ b/mfbt/EnumeratedRange.h
@@ -15,33 +15,36 @@
  * Note that the enum values should be contiguous in the iterated range;
  * unfortunately there exists no way for EnumeratedRange to enforce this
  * either dynamically or at compile time.
  */
 
 #ifndef mozilla_EnumeratedRange_h
 #define mozilla_EnumeratedRange_h
 
-#include "mozilla/IntegerTypeTraits.h"
+#include <type_traits>
+
 #include "mozilla/ReverseIterator.h"
 
 namespace mozilla {
 
 namespace detail {
 
-template<typename IntTypeT, typename EnumTypeT>
+template<typename EnumTypeT>
 class EnumeratedIterator
 {
 public:
+  typedef typename std::underlying_type<EnumTypeT>::type IntTypeT;
+
   template<typename EnumType>
   explicit EnumeratedIterator(EnumType aCurrent)
     : mCurrent(aCurrent) { }
 
-  template<typename IntType, typename EnumType>
-  explicit EnumeratedIterator(const EnumeratedIterator<IntType, EnumType>& aOther)
+  template<typename EnumType>
+  explicit EnumeratedIterator(const EnumeratedIterator<EnumType>& aOther)
     : mCurrent(aOther.mCurrent) { }
 
   EnumTypeT operator*() const { return mCurrent; }
 
   /* Increment and decrement operators */
 
   EnumeratedIterator& operator++()
   {
@@ -63,87 +66,87 @@ public:
   {
     auto ret = *this;
     mCurrent = EnumTypeT(IntTypeT(mCurrent) - IntTypeT(1));
     return ret;
   }
 
   /* Comparison operators */
 
-  template<typename IntType, typename EnumType>
-  friend bool operator==(const EnumeratedIterator<IntType, EnumType>& aIter1,
-                         const EnumeratedIterator<IntType, EnumType>& aIter2);
-  template<typename IntType, typename EnumType>
-  friend bool operator!=(const EnumeratedIterator<IntType, EnumType>& aIter1,
-                         const EnumeratedIterator<IntType, EnumType>& aIter2);
-  template<typename IntType, typename EnumType>
-  friend bool operator<(const EnumeratedIterator<IntType, EnumType>& aIter1,
-                        const EnumeratedIterator<IntType, EnumType>& aIter2);
-  template<typename IntType, typename EnumType>
-  friend bool operator<=(const EnumeratedIterator<IntType, EnumType>& aIter1,
-                         const EnumeratedIterator<IntType, EnumType>& aIter2);
-  template<typename IntType, typename EnumType>
-  friend bool operator>(const EnumeratedIterator<IntType, EnumType>& aIter1,
-                        const EnumeratedIterator<IntType, EnumType>& aIter2);
-  template<typename IntType, typename EnumType>
-  friend bool operator>=(const EnumeratedIterator<IntType, EnumType>& aIter1,
-                         const EnumeratedIterator<IntType, EnumType>& aIter2);
+  template<typename EnumType>
+  friend bool operator==(const EnumeratedIterator<EnumType>& aIter1,
+                         const EnumeratedIterator<EnumType>& aIter2);
+  template<typename EnumType>
+  friend bool operator!=(const EnumeratedIterator<EnumType>& aIter1,
+                         const EnumeratedIterator<EnumType>& aIter2);
+  template<typename EnumType>
+  friend bool operator<(const EnumeratedIterator<EnumType>& aIter1,
+                        const EnumeratedIterator<EnumType>& aIter2);
+  template<typename EnumType>
+  friend bool operator<=(const EnumeratedIterator<EnumType>& aIter1,
+                         const EnumeratedIterator<EnumType>& aIter2);
+  template<typename EnumType>
+  friend bool operator>(const EnumeratedIterator<EnumType>& aIter1,
+                        const EnumeratedIterator<EnumType>& aIter2);
+  template<typename EnumType>
+  friend bool operator>=(const EnumeratedIterator<EnumType>& aIter1,
+                         const EnumeratedIterator<EnumType>& aIter2);
 
 private:
   EnumTypeT mCurrent;
 };
 
-template<typename IntType, typename EnumType>
-bool operator==(const EnumeratedIterator<IntType, EnumType>& aIter1,
-                const EnumeratedIterator<IntType, EnumType>& aIter2)
+template<typename EnumType>
+bool operator==(const EnumeratedIterator<EnumType>& aIter1,
+                const EnumeratedIterator<EnumType>& aIter2)
 {
   return aIter1.mCurrent == aIter2.mCurrent;
 }
 
-template<typename IntType, typename EnumType>
-bool operator!=(const EnumeratedIterator<IntType, EnumType>& aIter1,
-                const EnumeratedIterator<IntType, EnumType>& aIter2)
+template<typename EnumType>
+bool operator!=(const EnumeratedIterator<EnumType>& aIter1,
+                const EnumeratedIterator<EnumType>& aIter2)
 {
   return aIter1.mCurrent != aIter2.mCurrent;
 }
 
-template<typename IntType, typename EnumType>
-bool operator<(const EnumeratedIterator<IntType, EnumType>& aIter1,
-               const EnumeratedIterator<IntType, EnumType>& aIter2)
+template<typename EnumType>
+bool operator<(const EnumeratedIterator<EnumType>& aIter1,
+               const EnumeratedIterator<EnumType>& aIter2)
 {
   return aIter1.mCurrent < aIter2.mCurrent;
 }
 
-template<typename IntType, typename EnumType>
-bool operator<=(const EnumeratedIterator<IntType, EnumType>& aIter1,
-                const EnumeratedIterator<IntType, EnumType>& aIter2)
+template<typename EnumType>
+bool operator<=(const EnumeratedIterator<EnumType>& aIter1,
+                const EnumeratedIterator<EnumType>& aIter2)
 {
   return aIter1.mCurrent <= aIter2.mCurrent;
 }
 
-template<typename IntType, typename EnumType>
-bool operator>(const EnumeratedIterator<IntType, EnumType>& aIter1,
-               const EnumeratedIterator<IntType, EnumType>& aIter2)
+template<typename EnumType>
+bool operator>(const EnumeratedIterator<EnumType>& aIter1,
+               const EnumeratedIterator<EnumType>& aIter2)
 {
   return aIter1.mCurrent > aIter2.mCurrent;
 }
 
-template<typename IntType, typename EnumType>
-bool operator>=(const EnumeratedIterator<IntType, EnumType>& aIter1,
-                const EnumeratedIterator<IntType, EnumType>& aIter2)
+template<typename EnumType>
+bool operator>=(const EnumeratedIterator<EnumType>& aIter1,
+                const EnumeratedIterator<EnumType>& aIter2)
 {
   return aIter1.mCurrent >= aIter2.mCurrent;
 }
 
-template<typename IntTypeT, typename EnumTypeT>
+template<typename EnumTypeT>
 class EnumeratedRange
 {
 public:
-  typedef EnumeratedIterator<IntTypeT, EnumTypeT> iterator;
-  typedef EnumeratedIterator<IntTypeT, EnumTypeT> const_iterator;
+  typedef EnumeratedIterator<EnumTypeT> iterator;
+  typedef EnumeratedIterator<EnumTypeT> const_iterator;
   typedef ReverseIterator<iterator> reverse_iterator;
   typedef ReverseIterator<const_iterator> const_reverse_iterator;
 
   template<typename EnumType>
   EnumeratedRange(EnumType aBegin, EnumType aEnd)
     : mBegin(aBegin), mEnd(aEnd) { }
 
   iterator begin() const { return iterator(mBegin); }
@@ -166,48 +169,31 @@ private:
 // Enums can have an unsigned underlying type, which makes some of the
 // comparisons below always true or always false. Temporarily disable
 // -Wtype-limits to avoid breaking -Werror builds.
 #  pragma GCC diagnostic push
 #  pragma GCC diagnostic ignored "-Wtype-limits"
 #endif
 
 // Create a range to iterate from aBegin to aEnd, exclusive.
-//
-// (Once we can rely on std::underlying_type, we can remove the IntType
-// template parameter.)
-template<typename IntType, typename EnumType>
-inline detail::EnumeratedRange<IntType, EnumType>
+template<typename EnumType>
+inline detail::EnumeratedRange<EnumType>
 MakeEnumeratedRange(EnumType aBegin, EnumType aEnd)
 {
-#ifdef DEBUG
-  typedef typename MakeUnsigned<IntType>::Type UnsignedType;
-#endif
-  static_assert(sizeof(IntType) >= sizeof(EnumType),
-                "IntType should be at least as big as EnumType!");
   MOZ_ASSERT(aBegin <= aEnd, "Cannot generate invalid, unbounded range!");
-  MOZ_ASSERT_IF(aBegin < EnumType(0), IsSigned<IntType>::value);
-  MOZ_ASSERT_IF(aBegin >= EnumType(0) && IsSigned<IntType>::value,
-                UnsignedType(aEnd) <= UnsignedType(MaxValue<IntType>::value));
-  return detail::EnumeratedRange<IntType, EnumType>(aBegin, aEnd);
+  return detail::EnumeratedRange<EnumType>(aBegin, aEnd);
 }
 
 // Create a range to iterate from EnumType(0) to aEnd, exclusive. EnumType(0)
 // should exist, but note that there is no way for us to ensure that it does!
-// Since the enumeration starts at EnumType(0), we know for sure that the values
-// will be in range of our deduced IntType.
 template<typename EnumType>
-inline detail::EnumeratedRange<
-  typename UnsignedStdintTypeForSize<sizeof(EnumType)>::Type,
-  EnumType>
+inline detail::EnumeratedRange<EnumType>
 MakeEnumeratedRange(EnumType aEnd)
 {
-  return MakeEnumeratedRange<
-    typename UnsignedStdintTypeForSize<sizeof(EnumType)>::Type>(EnumType(0),
-                                                                aEnd);
+  return MakeEnumeratedRange(EnumType(0), aEnd);
 }
 
 #ifdef __GNUC__
 #  pragma GCC diagnostic pop
 #endif
 
 } // namespace mozilla
 
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -2144,26 +2144,23 @@ pref("services.blocklist.addons.collecti
 pref("services.blocklist.addons.checked", 0);
 pref("services.blocklist.plugins.collection", "plugins");
 pref("services.blocklist.plugins.checked", 0);
 pref("services.blocklist.gfx.collection", "gfx");
 pref("services.blocklist.gfx.checked", 0);
 
 // Controls whether signing should be enforced on signature-capable blocklist
 // collections.
-pref("services.blocklist.signing.enforced", false);
-
-// For now, let's keep settings server update out of the release builds
-#ifdef RELEASE_BUILD
-pref("services.blocklist.update_enabled", false);
-pref("security.onecrl.via.amo", true);
-#else
+pref("services.blocklist.signing.enforced", true);
+
+// Enable blocklists via the services settings mechanism
 pref("services.blocklist.update_enabled", true);
+
+// Enable certificate blocklist updates via services settings
 pref("security.onecrl.via.amo", false);
-#endif
 
 
 // Modifier key prefs: default to Windows settings,
 // menu access key = alt, accelerator key = control.
 // Use 17 for Ctrl, 18 for Alt, 224 for Meta, 91 for Win, 0 for none. Mac settings in macprefs.js
 pref("ui.key.accelKey", 17);
 pref("ui.key.menuAccessKey", 18);
 pref("ui.key.generalAccessKey", -1);
@@ -4567,17 +4564,16 @@ pref("gfx.content.use-native-pushlayer",
 #ifdef ANDROID
 pref("gfx.apitrace.enabled",false);
 #endif
 
 #ifdef MOZ_X11
 pref("gfx.content.use-native-pushlayer", true);
 #ifdef MOZ_WIDGET_GTK
 pref("gfx.xrender.enabled",false);
-pref("widget.allow-gtk-dark-theme", false);
 #endif
 #endif
 
 #ifdef XP_WIN
 pref("gfx.content.use-native-pushlayer", true);
 
 // Whether to disable the automatic detection and use of direct2d.
 pref("gfx.direct2d.disabled", false);
--- a/security/manager/android_stub.h
+++ b/security/manager/android_stub.h
@@ -12,16 +12,21 @@
  * we may be able to implement it ourselves. */
 #define _SYS_SYSINFO_H_
 
 #include <sys/cdefs.h>
 #include <sys/resource.h>
 #include <linux/kernel.h>
 #include <unistd.h>
 
+#ifndef ANDROID_VERSION
+#include <android/api-level.h>
+#define ANDROID_VERSION __ANDROID_API__
+#endif
+
 /* Use this stub version of getdtablesize
  * instead of the one in the header */
 __attribute__((unused))
 static int getdtablesize_stub(void)
 {
     struct rlimit r;
     if (getrlimit(RLIMIT_NOFILE, &r) < 0) {
         return sysconf(_SC_OPEN_MAX);
--- a/services/sync/locales/en-US/sync.properties
+++ b/services/sync/locales/en-US/sync.properties
@@ -1,14 +1,14 @@
 # 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/.
 
 # %1: the user name (Ed), %2: the app name (Firefox), %3: the operating system (Android)
-client.name2 = %1$S's %2$S on %3$S
+client.name2 = %1$S’s %2$S on %3$S
 
 # %S is the date and time at which the last sync successfully completed
 lastSync2.label = Last sync: %S
 
 # signInToSync.description is the tooltip for the Sync buttons when Sync is
 # not configured.
 signInToSync.description = Sign In To Sync
 
--- a/services/sync/modules/addonsreconciler.js
+++ b/services/sync/modules/addonsreconciler.js
@@ -429,17 +429,18 @@ AddonsReconciler.prototype = {
       let record = {
         id: id,
         guid: guid,
         enabled: enabled,
         installed: true,
         modified: now,
         type: addon.type,
         scope: addon.scope,
-        foreignInstall: addon.foreignInstall
+        foreignInstall: addon.foreignInstall,
+        isSyncable: addon.isSyncable,
       };
       this._addons[id] = record;
       this._log.debug("Adding change because add-on not present locally: " +
                       id);
       this._addChange(now, CHANGE_INSTALLED, record);
       return;
     }
 
--- a/services/sync/modules/addonutils.js
+++ b/services/sync/modules/addonutils.js
@@ -369,16 +369,19 @@ AddonUtilsInternal.prototype = {
     if (!addon.sourceURI) {
       this._log.info("Skipping install of add-on because missing " +
                      "sourceURI: " + addon.id);
       return false;
     }
     // Verify that the source URI uses TLS. We don't allow installs from
     // insecure sources for security reasons. The Addon Manager ensures
     // that cert validation etc is performed.
+    // (We should also consider just dropping this entirely and calling
+    // XPIProvider.isInstallAllowed, but that has additional semantics we might
+    // need to think through...)
     let requireSecureURI = true;
     if (options && options.requireSecureURI !== undefined) {
       requireSecureURI = options.requireSecureURI;
     }
 
     if (requireSecureURI) {
       let scheme = addon.sourceURI.scheme;
       if (scheme != "https") {
--- a/services/sync/modules/engines/addons.js
+++ b/services/sync/modules/engines/addons.js
@@ -20,20 +20,23 @@
  * We currently synchronize:
  *
  *  - Installations
  *  - Uninstallations
  *  - User enabling and disabling
  *
  * Synchronization is influenced by the following preferences:
  *
- *  - services.sync.addons.ignoreRepositoryChecking
  *  - services.sync.addons.ignoreUserEnabledChanges
  *  - services.sync.addons.trustedSourceHostnames
  *
+ *  and also influenced by whether addons have repository caching enabled and
+ *  whether they allow installation of addons from insecure options (both of
+ *  which are themselves influenced by the "extensions." pref branch)
+ *
  * See the documentation in services-sync.js for the behavior of these prefs.
  */
 "use strict";
 
 var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://services-sync/addonutils.js");
 Cu.import("resource://services-sync/addonsreconciler.js");
@@ -273,30 +276,38 @@ AddonsStore.prototype = {
       // is our current policy.
       if (record.source != "amo") {
         this._log.info("Ignoring unknown add-on source (" + record.source + ")" +
                        " for " + record.id);
         return;
       }
     }
 
+    // Ignore incoming records for which an existing non-syncable addon
+    // exists.
+    let existingMeta = this.reconciler.addons[record.addonID];
+    if (existingMeta && !this.isAddonSyncable(existingMeta)) {
+      this._log.info("Ignoring incoming record for an existing but non-syncable addon", record.addonID);
+      return;
+    }
+
     Store.prototype.applyIncoming.call(this, record);
   },
 
 
   /**
    * Provides core Store API to create/install an add-on from a record.
    */
   create: function create(record) {
     let cb = Async.makeSpinningCallback();
     AddonUtils.installAddons([{
       id:               record.addonID,
       syncGUID:         record.id,
       enabled:          record.enabled,
-      requireSecureURI: !Svc.Prefs.get("addons.ignoreRepositoryChecking", false),
+      requireSecureURI: this._extensionsPrefs.get("install.requireSecureOrigin", true),
     }], cb);
 
     // This will throw if there was an error. This will get caught by the sync
     // engine and the record will try to be applied later.
     let results = cb.wait();
 
     if (results.skipped.includes(record.addonID)) {
       this._log.info("Add-on skipped: " + record.addonID);
@@ -526,17 +537,20 @@ AddonsStore.prototype = {
    */
   isAddonSyncable: function isAddonSyncable(addon) {
     // Currently, we limit syncable add-ons to those that are:
     //   1) In a well-defined set of types
     //   2) Installed in the current profile
     //   3) Not installed by a foreign entity (i.e. installed by the app)
     //      since they act like global extensions.
     //   4) Is not a hotfix.
-    //   5) Are installed from AMO
+    //   5) The addons XPIProvider doesn't veto it (i.e not being installed in
+    //      the profile directory, or any other reasons it says the addon can't
+    //      be synced)
+    //   6) Are installed from AMO
 
     // We could represent the test as a complex boolean expression. We go the
     // verbose route so the failure reason is logged.
     if (!addon) {
       this._log.debug("Null object passed to isAddonSyncable.");
       return false;
     }
 
@@ -546,35 +560,45 @@ AddonsStore.prototype = {
       return false;
     }
 
     if (!(addon.scope & AddonManager.SCOPE_PROFILE)) {
       this._log.debug(addon.id + " not syncable: not installed in profile.");
       return false;
     }
 
+    // If the addon manager says it's not syncable, we skip it.
+    if (!addon.isSyncable) {
+      this._log.debug(addon.id + " not syncable: vetoed by the addon manager.");
+      return false;
+    }
+
     // This may be too aggressive. If an add-on is downloaded from AMO and
     // manually placed in the profile directory, foreignInstall will be set.
     // Arguably, that add-on should be syncable.
     // TODO Address the edge case and come up with more robust heuristics.
     if (addon.foreignInstall) {
       this._log.debug(addon.id + " not syncable: is foreign install.");
       return false;
     }
 
     // Ignore hotfix extensions (bug 741670). The pref may not be defined.
+    // XXX - note that addon.isSyncable will be false for hotfix addons, so
+    // this check isn't strictly necessary - except for Sync tests which aren't
+    // setup to create a "real" hotfix addon. This can be removed once those
+    // tests are fixed (but keeping it doesn't hurt either)
     if (this._extensionsPrefs.get("hotfix.id", null) == addon.id) {
       this._log.debug(addon.id + " not syncable: is a hotfix.");
       return false;
     }
 
-    // We provide a back door to skip the repository checking of an add-on.
-    // This is utilized by the tests to make testing easier. Users could enable
-    // this, but it would sacrifice security.
-    if (Svc.Prefs.get("addons.ignoreRepositoryChecking", false)) {
+    // If the AddonRepository's cache isn't enabled (which it typically isn't
+    // in tests), getCachedAddonByID always returns null - so skip the check
+    // in that case.
+    if (!AddonRepository.cacheEnabled) {
       return true;
     }
 
     let cb = Async.makeSyncCallback();
     AddonRepository.getCachedAddonByID(addon.id, cb);
     let result = Async.waitForSyncCallback(cb);
 
     if (!result) {
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -16,16 +16,23 @@ Cu.import("resource://services-sync/util
 Cu.import("resource://services-common/logmanager.js");
 Cu.import("resource://services-common/async.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Status",
                                   "resource://services-sync/status.js");
 XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
                                   "resource://gre/modules/AddonManager.jsm");
 
+// Get the value for an interval that's stored in preferences. To save users
+// from themselves (and us from them!) the minimum time they can specify
+// is 60s.
+function getThrottledIntervalPreference(prefName) {
+  return Math.max(Svc.Prefs.get(prefName), 60) * 1000;
+}
+
 this.SyncScheduler = function SyncScheduler(service) {
   this.service = service;
   this.init();
 }
 SyncScheduler.prototype = {
   _log: Log.repository.getLogger("Sync.SyncScheduler"),
 
   _fatalLoginStatus: [LOGIN_FAILED_NO_USERNAME,
@@ -43,22 +50,22 @@ SyncScheduler.prototype = {
     this._log.trace("Setting SyncScheduler policy values to defaults.");
 
     let service = Cc["@mozilla.org/weave/service;1"]
                     .getService(Ci.nsISupports)
                     .wrappedJSObject;
 
     let part = service.fxAccountsEnabled ? "fxa" : "sync11";
     let prefSDInterval = "scheduler." + part + ".singleDeviceInterval";
-    this.singleDeviceInterval = Svc.Prefs.get(prefSDInterval) * 1000;
+    this.singleDeviceInterval = getThrottledIntervalPreference(prefSDInterval);
 
-    this.idleInterval         = Svc.Prefs.get("scheduler.idleInterval")         * 1000;
-    this.activeInterval       = Svc.Prefs.get("scheduler.activeInterval")       * 1000;
-    this.immediateInterval    = Svc.Prefs.get("scheduler.immediateInterval")    * 1000;
-    this.eolInterval          = Svc.Prefs.get("scheduler.eolInterval")          * 1000;
+    this.idleInterval         = getThrottledIntervalPreference("scheduler.idleInterval");
+    this.activeInterval       = getThrottledIntervalPreference("scheduler.activeInterval");
+    this.immediateInterval    = getThrottledIntervalPreference("scheduler.immediateInterval");
+    this.eolInterval          = getThrottledIntervalPreference("scheduler.eolInterval");
 
     // A user is non-idle on startup by default.
     this.idle = false;
 
     this.hasIncomingItems = false;
 
     this.clearSyncTriggers();
   },
--- a/services/sync/services-sync.js
+++ b/services/sync/services-sync.js
@@ -33,19 +33,16 @@ pref("services.sync.engine.tabs", true);
 pref("services.sync.engine.tabs.filteredUrls", "^(about:.*|chrome://weave/.*|wyciwyg:.*|file:.*|blob:.*)$");
 
 pref("services.sync.jpake.serverURL", "https://setup.services.mozilla.com/");
 pref("services.sync.jpake.pollInterval", 1000);
 pref("services.sync.jpake.firstMsgMaxTries", 300); // 5 minutes
 pref("services.sync.jpake.lastMsgMaxTries", 300);  // 5 minutes
 pref("services.sync.jpake.maxTries", 10);
 
-// Allow add-ons to be synced from non-trusted sources.
-pref("services.sync.addons.ignoreRepositoryChecking", false);
-
 // If true, add-on sync ignores changes to the user-enabled flag. This
 // allows people to have the same set of add-ons installed across all
 // profiles while maintaining different enabled states.
 pref("services.sync.addons.ignoreUserEnabledChanges", false);
 
 // Comma-delimited list of hostnames to trust for add-on install.
 pref("services.sync.addons.trustedSourceHostnames", "addons.mozilla.org");
 
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -1,14 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-common/async.js");
 Cu.import("resource://testing-common/services/common/utils.js");
 Cu.import("resource://testing-common/PlacesTestUtils.jsm");
+Cu.import("resource://services-sync/util.js");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, 'SyncPingSchema', function() {
   let ns = {};
   Cu.import("resource://gre/modules/FileUtils.jsm", ns);
   let stream = Cc["@mozilla.org/network/file-input-stream;1"]
                .createInstance(Ci.nsIFileInputStream);
   let jsonReader = Cc["@mozilla.org/dom/json;1"]
@@ -385,8 +386,17 @@ function sync_engine_and_validate_telem(
     }
     if (caughtError) {
       Svc.Obs.notify("weave:service:sync:error", caughtError);
     } else {
       Svc.Obs.notify("weave:service:sync:finish");
     }
   });
 }
+
+// Avoid an issue where `client.name2` containing unicode characters causes
+// a number of tests to fail, due to them assuming that we do not need to utf-8
+// encode or decode data sent through the mocked server (see bug 1268912).
+Utils.getDefaultDeviceName = function() {
+  return "Test device name";
+};
+
+
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/systemaddon-search.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<searchresults total_results="1">
+  <addon id="5618">
+  <name>System Add-on Test</name>
+  <type id="1">Extension</type>
+  <guid>system1@tests.mozilla.org</guid>
+  <slug>addon11</slug>
+  <version>1.0</version>
+
+  <compatible_applications><application>
+      <name>Firefox</name>
+      <application_id>1</application_id>
+      <min_version>3.6</min_version>
+      <max_version>*</max_version>
+      <appID>xpcshell@tests.mozilla.org</appID>
+    </application></compatible_applications>
+  <all_compatible_os><os>ALL</os></all_compatible_os>
+
+  <install os="ALL" size="999">http://127.0.0.1:8888/system.xpi</install>
+    <created epoch="1252903662">
+      2009-09-14T04:47:42Z
+    </created>
+    <last_updated epoch="1315255329">
+      2011-09-05T20:42:09Z
+    </last_updated>
+    </addon>
+</searchresults>
--- a/services/sync/tests/unit/test_addon_utils.js
+++ b/services/sync/tests/unit/test_addon_utils.js
@@ -98,18 +98,16 @@ add_test(function test_ignore_untrusted_
 });
 
 add_test(function test_source_uri_rewrite() {
   _("Ensure that a 'src=api' query string is rewritten to 'src=sync'");
 
   // This tests for conformance with bug 708134 so server-side metrics aren't
   // skewed.
 
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
-
   // We resort to monkeypatching because of the API design.
   let oldFunction = AddonUtils.__proto__.installAddonFromSearchResult;
 
   let installCalled = false;
   AddonUtils.__proto__.installAddonFromSearchResult =
     function testInstallAddon(addon, metadata, cb) {
 
     do_check_eq(SERVER_ADDRESS + "/require.xpi?src=sync",
@@ -134,11 +132,10 @@ add_test(function test_source_uri_rewrit
     requireSecureURI: false,
   }
   AddonUtils.installAddons([installOptions], installCallback);
 
   installCallback.wait();
   do_check_true(installCalled);
   AddonUtils.__proto__.installAddonFromSearchResult = oldFunction;
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
   server.stop(run_next_test);
 });
--- a/services/sync/tests/unit/test_addons_engine.js
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -11,16 +11,17 @@ Cu.import("resource://services-sync/addo
 Cu.import("resource://services-sync/engines/addons.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://testing-common/services/sync/utils.js");
 
 var prefs = new Preferences();
 prefs.set("extensions.getAddons.get.url",
           "http://localhost:8888/search/guid:%IDS%");
+prefs.set("extensions.install.requireSecureOrigin", false);
 
 loadAddonTestFunctions();
 startupManager();
 
 var engineManager = Service.engineManager;
 
 engineManager.register(AddonsEngine);
 var engine = engineManager.get("addons");
@@ -30,18 +31,16 @@ var tracker = engine._tracker;
 function advance_test() {
   reconciler._addons = {};
   reconciler._changes = [];
 
   let cb = Async.makeSpinningCallback();
   reconciler.saveState(null, cb);
   cb.wait();
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
-
   run_next_test();
 }
 
 // This is a basic sanity test for the unit test itself. If this breaks, the
 // add-ons API likely changed upstream.
 add_test(function test_addon_install() {
   _("Ensure basic add-on APIs work as expected.");
 
@@ -99,17 +98,16 @@ add_test(function test_get_changed_ids()
   do_check_eq("object", typeof(changes));
   do_check_eq(1, Object.keys(changes).length);
   do_check_true(guid1 in changes);
   do_check_eq(changeTime, changes[guid1]);
 
   tracker.clearChangedIDs();
 
   _("Ensure reconciler changes are populated.");
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
   let addon = installAddon("test_bootstrap1_1");
   tracker.clearChangedIDs(); // Just in case.
   changes = engine.getChangedIDs();
   do_check_eq("object", typeof(changes));
   do_check_eq(1, Object.keys(changes).length);
   do_check_true(addon.syncGUID in changes);
   _("Change time: " + changeTime + ", addon change: " + changes[addon.syncGUID]);
   do_check_true(changes[addon.syncGUID] >= changeTime);
@@ -146,19 +144,16 @@ add_test(function test_get_changed_ids()
 });
 
 add_test(function test_disabled_install_semantics() {
   _("Ensure that syncing a disabled add-on preserves proper state.");
 
   // This is essentially a test for bug 712542, which snuck into the original
   // add-on sync drop. It ensures that when an add-on is installed that the
   // disabled state and incoming syncGUID is preserved, even on the next sync.
-
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
-
   const USER       = "foo";
   const PASSWORD   = "password";
   const PASSPHRASE = "abcdeabcdeabcdeabcdeabcdea";
   const ADDON_ID   = "addon1@tests.mozilla.org";
 
   let server = new SyncServer();
   server.start();
   new SyncTestingInfrastructure(server.server, USER, PASSWORD, PASSPHRASE);
--- a/services/sync/tests/unit/test_addons_store.js
+++ b/services/sync/tests/unit/test_addons_store.js
@@ -5,23 +5,43 @@
 
 Cu.import("resource://gre/modules/Log.jsm");
 Cu.import("resource://gre/modules/Preferences.jsm");
 Cu.import("resource://services-sync/addonutils.js");
 Cu.import("resource://services-sync/engines/addons.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://gre/modules/FileUtils.jsm");
 
 const HTTP_PORT = 8888;
 
 var prefs = new Preferences();
 
 prefs.set("extensions.getAddons.get.url", "http://localhost:8888/search/guid:%IDS%");
+prefs.set("extensions.install.requireSecureOrigin", false);
+
+const SYSTEM_ADDON_ID = "system1@tests.mozilla.org";
+let systemAddonFile;
+
+// The system add-on must be installed before AddonManager is started.
+function loadSystemAddon() {
+  let addonFilename = SYSTEM_ADDON_ID + ".xpi";
+  const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true);
+  do_get_file(ExtensionsTestPath("/data/system_addons/system1_1.xpi")).copyTo(distroDir, addonFilename);
+  systemAddonFile = FileUtils.File(distroDir.path);
+  systemAddonFile.append(addonFilename);
+  systemAddonFile.lastModifiedTime = Date.now();
+  // As we're not running in application, we need to setup the features directory
+  // used by system add-ons.
+  registerDirectory("XREAppFeat", distroDir);
+}
+
 loadAddonTestFunctions();
+loadSystemAddon();
 startupManager();
 
 Service.engineManager.register(AddonsEngine);
 var engine     = Service.engineManager.get("addons");
 var tracker    = engine._tracker;
 var store      = engine._store;
 var reconciler = engine._reconciler;
 
@@ -52,29 +72,34 @@ function createAndStartHTTPServer(port) 
 
     server.registerFile("/search/guid:bootstrap1%40tests.mozilla.org",
                         do_get_file("bootstrap1-search.xml"));
     server.registerFile("/bootstrap1.xpi", do_get_file(bootstrap1XPI));
 
     server.registerFile("/search/guid:missing-xpi%40tests.mozilla.org",
                         do_get_file("missing-xpi-search.xml"));
 
+    server.registerFile("/search/guid:system1%40tests.mozilla.org",
+                        do_get_file("systemaddon-search.xml"));
+    server.registerFile("/system.xpi", systemAddonFile);
+
     server.start(port);
 
     return server;
   } catch (ex) {
     _("Got exception starting HTTP server on port " + port);
     _("Error: " + Log.exceptionStr(ex));
     do_throw(ex);
   }
 }
 
 function run_test() {
   initTestLogging("Trace");
   Log.repository.getLogger("Sync.Engine.Addons").level = Log.Level.Trace;
+  Log.repository.getLogger("Sync.Tracker.Addons").level = Log.Level.Trace;
   Log.repository.getLogger("Sync.AddonsRepository").level =
     Log.Level.Trace;
 
   reconciler.startListening();
 
   // Don't flush to disk in the middle of an event listener!
   // This causes test hangs on WinXP.
   reconciler._shouldPersist = false;
@@ -189,41 +214,44 @@ add_test(function test_apply_uninstall()
   do_check_eq(null, addon);
 
   run_next_test();
 });
 
 add_test(function test_addon_syncability() {
   _("Ensure isAddonSyncable functions properly.");
 
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
   Svc.Prefs.set("addons.trustedSourceHostnames",
                 "addons.mozilla.org,other.example.com");
 
   do_check_false(store.isAddonSyncable(null));
 
   let addon = installAddon("test_bootstrap1_1");
   do_check_true(store.isAddonSyncable(addon));
 
   let dummy = {};
-  const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall"];
+  const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall", "isSyncable"];
   for (let k of KEYS) {
     dummy[k] = addon[k];
   }
 
   do_check_true(store.isAddonSyncable(dummy));
 
   dummy.type = "UNSUPPORTED";
   do_check_false(store.isAddonSyncable(dummy));
   dummy.type = addon.type;
 
   dummy.scope = 0;
   do_check_false(store.isAddonSyncable(dummy));
   dummy.scope = addon.scope;
 
+  dummy.isSyncable = false;
+  do_check_false(store.isAddonSyncable(dummy));
+  dummy.isSyncable = addon.isSyncable;
+
   dummy.foreignInstall = true;
   do_check_false(store.isAddonSyncable(dummy));
   dummy.foreignInstall = false;
 
   uninstallAddon(addon);
 
   do_check_false(store.isSourceURITrusted(null));
 
@@ -263,27 +291,25 @@ add_test(function test_addon_syncability
   Svc.Prefs.reset("addons.trustedSourceHostnames");
 
   run_next_test();
 });
 
 add_test(function test_ignore_hotfixes() {
   _("Ensure that hotfix extensions are ignored.");
 
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
-
   // A hotfix extension is one that has the id the same as the
   // extensions.hotfix.id pref.
   let prefs = new Preferences("extensions.");
 
   let addon = installAddon("test_bootstrap1_1");
   do_check_true(store.isAddonSyncable(addon));
 
   let dummy = {};
-  const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall"];
+  const KEYS = ["id", "syncGUID", "type", "scope", "foreignInstall", "isSyncable"];
   for (let k of KEYS) {
     dummy[k] = addon[k];
   }
 
   // Basic sanity check.
   do_check_true(store.isAddonSyncable(dummy));
 
   prefs.set("hotfix.id", dummy.id);
@@ -296,28 +322,25 @@ add_test(function test_ignore_hotfixes()
   // Need to delete pref before changing type.
   prefSvc.deleteBranch("hotfix.id");
   prefSvc.setIntPref("hotfix.id", 0xdeadbeef);
 
   do_check_true(store.isAddonSyncable(dummy));
 
   uninstallAddon(addon);
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
   prefs.reset("hotfix.id");
 
   run_next_test();
 });
 
 
 add_test(function test_get_all_ids() {
   _("Ensures that getAllIDs() returns an appropriate set.");
 
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
-
   _("Installing two addons.");
   let addon1 = installAddon("test_install1");
   let addon2 = installAddon("test_bootstrap1_1");
 
   _("Ensure they're syncable.");
   do_check_true(store.isAddonSyncable(addon1));
   do_check_true(store.isAddonSyncable(addon2));
 
@@ -326,17 +349,16 @@ add_test(function test_get_all_ids() {
   do_check_eq("object", typeof(ids));
   do_check_eq(2, Object.keys(ids).length);
   do_check_true(addon1.syncGUID in ids);
   do_check_true(addon2.syncGUID in ids);
 
   addon1.install.cancel();
   uninstallAddon(addon2);
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
   run_next_test();
 });
 
 add_test(function test_change_item_id() {
   _("Ensures that changeItemID() works properly.");
 
   let addon = installAddon("test_bootstrap1_1");
 
@@ -352,19 +374,16 @@ add_test(function test_change_item_id() 
   uninstallAddon(newAddon);
 
   run_next_test();
 });
 
 add_test(function test_create() {
   _("Ensure creating/installing an add-on from a record works.");
 
-  // Set this so that getInstallFromSearchResult doesn't end up
-  // failing the install due to an insecure source URI scheme.
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
   let server = createAndStartHTTPServer(HTTP_PORT);
 
   let addon = installAddon("test_bootstrap1_1");
   let id = addon.id;
   uninstallAddon(addon);
 
   let guid = Utils.makeGUID();
   let record = createRecordForThisApp(guid, id, true, false);
@@ -374,17 +393,16 @@ add_test(function test_create() {
 
   let newAddon = getAddonFromAddonManagerByID(id);
   do_check_neq(null, newAddon);
   do_check_eq(guid, newAddon.syncGUID);
   do_check_false(newAddon.userDisabled);
 
   uninstallAddon(newAddon);
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
   server.stop(run_next_test);
 });
 
 add_test(function test_create_missing_search() {
   _("Ensures that failed add-on searches are handled gracefully.");
 
   let server = createAndStartHTTPServer(HTTP_PORT);
 
@@ -411,66 +429,110 @@ add_test(function test_create_bad_instal
   // The handler returns a search result but the XPI will 404.
   const id = "missing-xpi@tests.mozilla.org";
   let guid = Utils.makeGUID();
   let record = createRecordForThisApp(guid, id, true, false);
 
   let failed = store.applyIncomingBatch([record]);
   // This addon had no source URI so was skipped - but it's not treated as
   // failure.
-  do_check_eq(0, failed.length);
+  // XXX - this test isn't testing what we thought it was. Previously the addon
+  // was not being installed due to requireSecureURL checking *before* we'd
+  // attempted to get the XPI.
+  // With requireSecureURL disabled we do see a download failure, but the addon
+  // *does* get added to |failed|.
+  // FTR: onDownloadFailed() is called with ERROR_NETWORK_FAILURE, so it's going
+  // to be tricky to distinguish a 404 from other transient network errors
+  // where we do want the addon to end up in |failed|.
+  // This is being tracked in bug 1284778.
+  //do_check_eq(0, failed.length);
 
   let addon = getAddonFromAddonManagerByID(id);
   do_check_eq(null, addon);
 
   server.stop(run_next_test);
 });
 
+add_test(function test_ignore_system() {
+  _("Ensure we ignore system addons");
+  // Our system addon should not appear in getAllIDs
+  engine._refreshReconcilerState();
+  let num = 0;
+  for (let guid in store.getAllIDs()) {
+    num += 1;
+    let addon = reconciler.getAddonStateFromSyncGUID(guid);
+    do_check_neq(addon.id, SYSTEM_ADDON_ID);
+  }
+  do_check_true(num > 1, "should have seen at least one.")
+  run_next_test();
+});
+
+add_test(function test_incoming_system() {
+  _("Ensure we handle incoming records that refer to a system addon");
+  // eg, loop initially had a normal addon but it was then "promoted" to be a
+  // system addon but wanted to keep the same ID. The server record exists due
+  // to this.
+
+  // before we start, ensure the system addon isn't disabled.
+  do_check_false(getAddonFromAddonManagerByID(SYSTEM_ADDON_ID).userDisabled);
+
+  // Now simulate an incoming record with the same ID as the system addon,
+  // but flagged as disabled - it should not be applied.
+  let server = createAndStartHTTPServer(HTTP_PORT);
+  // We make the incoming record flag the system addon as disabled - it should
+  // be ignored.
+  let guid = Utils.makeGUID();
+  let record = createRecordForThisApp(guid, SYSTEM_ADDON_ID, false, false);
+
+  let failed = store.applyIncomingBatch([record]);
+  do_check_eq(0, failed.length);
+
+  // The system addon should still not be userDisabled.
+  do_check_false(getAddonFromAddonManagerByID(SYSTEM_ADDON_ID).userDisabled);
+
+  server.stop(run_next_test);
+});
+
 add_test(function test_wipe() {
   _("Ensures that wiping causes add-ons to be uninstalled.");
 
   let addon1 = installAddon("test_bootstrap1_1");
 
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
   store.wipe();
 
   let addon = getAddonFromAddonManagerByID(addon1.id);
   do_check_eq(null, addon);
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
-
   run_next_test();
 });
 
 add_test(function test_wipe_and_install() {
   _("Ensure wipe followed by install works.");
 
   // This tests the reset sync flow where remote data is replaced by local. The
   // receiving client will see a wipe followed by a record which should undo
   // the wipe.
   let installed = installAddon("test_bootstrap1_1");
 
   let record = createRecordForThisApp(installed.syncGUID, installed.id, true,
                                       false);
 
-  Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
   store.wipe();
 
   let deleted = getAddonFromAddonManagerByID(installed.id);
   do_check_null(deleted);
 
   // Re-applying the record can require re-fetching the XPI.
   let server = createAndStartHTTPServer(HTTP_PORT);
 
   store.applyIncoming(record);
 
   let fetched = getAddonFromAddonManagerByID(record.addonID);
   do_check_true(!!fetched);
 
-  Svc.Prefs.reset("addons.ignoreRepositoryChecking");
   server.stop(run_next_test);
 });
 
 add_test(function cleanup() {
   // There's an xpcom-shutdown hook for this, but let's give this a shot.
   reconciler.stopListening();
   run_next_test();
 });
--- a/services/sync/tests/unit/test_addons_tracker.js
+++ b/services/sync/tests/unit/test_addons_tracker.js
@@ -6,17 +6,16 @@
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://services-sync/engines/addons.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/util.js");
 
 loadAddonTestFunctions();
 startupManager();
-Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
 Svc.Prefs.set("engine.addons", true);
 
 Service.engineManager.register(AddonsEngine);
 var engine     = Service.engineManager.get("addons");
 var reconciler = engine._reconciler;
 var store      = engine._store;
 var tracker    = engine._tracker;
 
--- a/services/sync/tests/unit/test_syncscheduler.js
+++ b/services/sync/tests/unit/test_syncscheduler.js
@@ -143,26 +143,37 @@ add_test(function test_prefAttributes() 
   do_check_eq(scheduler.idleInterval,
               Svc.Prefs.get("scheduler.idleInterval") * 1000);
   do_check_eq(scheduler.activeInterval,
               Svc.Prefs.get("scheduler.activeInterval") * 1000);
   do_check_eq(scheduler.immediateInterval,
               Svc.Prefs.get("scheduler.immediateInterval") * 1000);
 
   _("Custom values for prefs will take effect after a restart.");
-  Svc.Prefs.set("scheduler.sync11.singleDeviceInterval", 42);
-  Svc.Prefs.set("scheduler.idleInterval", 23);
-  Svc.Prefs.set("scheduler.activeInterval", 18);
+  Svc.Prefs.set("scheduler.sync11.singleDeviceInterval", 420);
+  Svc.Prefs.set("scheduler.idleInterval", 230);
+  Svc.Prefs.set("scheduler.activeInterval", 180);
   Svc.Prefs.set("scheduler.immediateInterval", 31415);
   scheduler.setDefaults();
-  do_check_eq(scheduler.idleInterval, 23000);
-  do_check_eq(scheduler.singleDeviceInterval, 42000);
-  do_check_eq(scheduler.activeInterval, 18000);
+  do_check_eq(scheduler.idleInterval, 230000);
+  do_check_eq(scheduler.singleDeviceInterval, 420000);
+  do_check_eq(scheduler.activeInterval, 180000);
   do_check_eq(scheduler.immediateInterval, 31415000);
 
+  _("Custom values for interval prefs can't be less than 60 seconds.");
+  Svc.Prefs.set("scheduler.sync11.singleDeviceInterval", 42);
+  Svc.Prefs.set("scheduler.idleInterval", 50);
+  Svc.Prefs.set("scheduler.activeInterval", 50);
+  Svc.Prefs.set("scheduler.immediateInterval", 10);
+  scheduler.setDefaults();
+  do_check_eq(scheduler.idleInterval, 60000);
+  do_check_eq(scheduler.singleDeviceInterval, 60000);
+  do_check_eq(scheduler.activeInterval, 60000);
+  do_check_eq(scheduler.immediateInterval, 60000);
+
   Svc.Prefs.resetBranch("");
   scheduler.setDefaults();
   run_next_test();
 });
 
 add_identity_test(this, function* test_updateClientMode() {
   _("Test updateClientMode adjusts scheduling attributes based on # of clients appropriately");
   do_check_eq(scheduler.syncThreshold, SINGLE_USER_THRESHOLD);
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -7,16 +7,17 @@ support-files =
   addon1-search.xml
   bootstrap1-search.xml
   fake_login_manager.js
   missing-sourceuri.xml
   missing-xpi-search.xml
   places_v10_from_v11.sqlite
   rewrite-search.xml
   sync_ping_schema.json
+  systemaddon-search.xml
   !/services/common/tests/unit/head_helpers.js
   !/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
 
 # The manifest is roughly ordered from low-level to high-level. When making
 # systemic sweeping changes, this makes it easier to identify errors closer to
 # the source.
 
 # Ensure we can import everything.
--- a/testing/mochitest/runrobocop.py
+++ b/testing/mochitest/runrobocop.py
@@ -222,16 +222,19 @@ class RobocopTestRunner(MochitestDesktop
         self.options.extraPrefs.append('browser.search.suggest.enabled=true')
         self.options.extraPrefs.append('browser.search.suggest.prompted=true')
         self.options.extraPrefs.append('layout.css.devPixelsPerPx=1.0')
         self.options.extraPrefs.append('browser.chrome.dynamictoolbar=false')
         self.options.extraPrefs.append('browser.snippets.enabled=false')
         self.options.extraPrefs.append('browser.casting.enabled=true')
         self.options.extraPrefs.append('extensions.autoupdate.enabled=false')
 
+        # Override the telemetry init delay for integration testing.
+        self.options.extraPrefs.append('toolkit.telemetry.initDelay=1')
+
         self.options.extensionsToExclude.extend([
             'mochikit@mozilla.org',
             'worker-test@mozilla.org.xpi',
             'workerbootstrap-test@mozilla.org.xpi',
             'indexedDB-test@mozilla.org.xpi',
         ])
 
         manifest = MochitestDesktop.buildProfile(self, self.options)
--- a/testing/tps/tps/testrunner.py
+++ b/testing/tps/tps/testrunner.py
@@ -60,20 +60,22 @@ class TPSTestRunner(object):
         'browser.dom.window.dump.enabled': True,
         'browser.sessionstore.resume_from_crash': False,
         'browser.shell.checkDefaultBrowser': False,
         'browser.tabs.warnOnClose': False,
         'browser.warnOnQuit': False,
         # Allow installing extensions dropped into the profile folder
         'extensions.autoDisableScopes': 10,
         'extensions.getAddons.get.url': 'http://127.0.0.1:4567/addons/api/%IDS%.xml',
+        # Our pretend addons server doesn't support metadata...
+        'extensions.getAddons.cache.enabled': False,
+        'extensions.install.requireSecureOrigin': False,
         'extensions.update.enabled': False,
         # Don't open a dialog to show available add-on updates
         'extensions.update.notifyUser': False,
-        'services.sync.addons.ignoreRepositoryChecking': True,
         'services.sync.firstSync': 'notReady',
         'services.sync.lastversion': '1.0',
         'toolkit.startup.max_resumed_crashes': -1,
         # hrm - not sure what the release/beta channels will do?
         'xpinstall.signatures.required': False,
     }
 
     debug_preferences = {
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -626,17 +626,24 @@ DocumentManager = {
       }
       return null;
     };
 
     let promises = Array.from(this.enumerateWindows(global.docShell), executeInWin)
                         .filter(promise => promise);
 
     if (!promises.length) {
-      return Promise.reject({message: `No matching window`});
+      let details = {};
+      for (let key of ["all_frames", "frame_id", "matches_about_blank", "matchesHost"]) {
+        if (key in options) {
+          details[key] = options[key];
+        }
+      }
+
+      return Promise.reject({message: `No window matching ${JSON.stringify(details)}`});
     }
     if (options.all_frames) {
       return Promise.all(promises);
     }
     if (promises.length > 1) {
       return Promise.reject({message: `Internal error: Script matched multiple windows`});
     }
     return promises[0];
--- a/toolkit/components/search/nsSearchService.js
+++ b/toolkit/components/search/nsSearchService.js
@@ -2300,35 +2300,35 @@ Engine.prototype = {
     if (!this._shortName)
       return false;
 
     // An engine is a default one if we initially loaded it from the application
     // or distribution directory.
     if (/^(?:jar:)?(?:\[app\]|\[distribution\])/.test(this._loadPath))
       return true;
 
-    // If we are in the xpcshell test case, we'll accept as a 'default' engine
-    // anything that has been registered at resource://search-plugins/ even if
-    // the file doesn't come from the application folder.
-    // If not, skip costly additional checks.
-    if (!gEnvironment.get("XPCSHELL_TEST_PROFILE_DIR"))
+    // If we are using a non-default locale or in the xpcshell test case,
+    // we'll accept as a 'default' engine anything that has been registered at
+    // resource://search-plugins/ even if the file doesn't come from the
+    // application folder.  If not, skip costly additional checks.
+    if (!Services.prefs.prefHasUserValue(LOCALE_PREF) &&
+        !gEnvironment.get("XPCSHELL_TEST_PROFILE_DIR"))
       return false;
 
     // Some xpcshell tests use the search service without registering
     // resource://search-plugins/.
     if (!Services.io.getProtocolHandler("resource")
                  .QueryInterface(Ci.nsIResProtocolHandler)
                  .hasSubstitution("search-plugins"))
       return false;
 
     let uri = makeURI(APP_SEARCH_PREFIX + this._shortName + ".xml");
     if (this.getAnonymizedLoadPath(null, uri) == this._loadPath) {
       // This isn't a real default engine, but it's very close.
-      LOG("_isDefault, pretending " + this._loadPath +
-          " is a default engine for testing purposes");
+      LOG("_isDefault, pretending " + this._loadPath + " is a default engine");
       return true;
     }
 
     return false;
   },
 
   get _hasUpdates() {
     // Whether or not the engine has an update URL
@@ -2435,16 +2435,17 @@ Engine.prototype = {
     if (!aResponseType) {
       aResponseType = URLTYPE_SEARCH_HTML;
     }
 
     if (aResponseType == URLTYPE_SEARCH_HTML &&
         Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).getBoolPref("reset.enabled") &&
         this.name == Services.search.currentEngine.name &&
         !this._isDefault &&
+        this.name != Services.search.originalDefaultEngine.name &&
         (!this.getAttr("loadPathHash") ||
          this.getAttr("loadPathHash") != getVerificationHash(this._loadPath)) &&
         !this._isWhiteListed) {
       let url = "about:searchreset";
       let data = [];
       if (aData)
         data.push("data=" + encodeURIComponent(aData));
       if (aPurpose)
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -3399,17 +3399,17 @@
     "kind": "exponential",
     "low": 1000,
     "high": 150000,
     "n_buckets": 20,
     "releaseChannelCollection": "opt-out",
     "description": "PLACES: Number of unique pages"
   },
   "PLACES_MOST_RECENT_EXPIRED_VISIT_DAYS": {
-    "alert_emails": ["mbonardo@mozilla.com"],
+    "alert_emails": ["firefox-dev@mozilla.org"],
     "expires_in_version": "never",
     "kind": "linear",
     "low": 30,
     "high": 730,
     "n_buckets": 12,
     "description": "PLACES: the most recent expired visit in days"
   },
   "PLACES_BOOKMARKS_COUNT": {
@@ -4536,25 +4536,25 @@
     "description": "Number of toplevel location changes in tablet and desktop mode (only used on win10 where tablet mode is available)"
   },
   "FX_TOUCH_USED": {
     "expires_in_version": "46",
     "kind": "count",
     "description": "Windows only. Counts occurrences of touch events"
   },
   "FX_URLBAR_SELECTED_RESULT_INDEX": {
-    "alert_emails": ["mbonardo@mozilla.com"],
-    "expires_in_version": "50",
+    "alert_emails": ["firefox-dev@mozilla.org"],
+    "expires_in_version": "55",
     "kind": "enumerated",
     "n_values": 17,
     "bug_numbers": [775825],
     "description": "Firefox: The index of the selected result in the URL bar popup"
   },
   "FX_URLBAR_SELECTED_RESULT_TYPE": {
-    "alert_emails": ["mbonardo@mozilla.com"],
+    "alert_emails": ["firefox-dev@mozilla.org"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 14,
     "bug_numbers": [775825],
     "description": "Firefox: The type of the selected result in the URL bar popup. See nsBrowserGlue.js::_handleURLBarTelemetry for the result types."
   },
   "INNERWINDOWS_WITH_MUTATION_LISTENERS": {
     "expires_in_version": "never",
@@ -8268,16 +8268,36 @@
     "expires_in_version": "55",
     "description": "Percentage of total time spent playing video while element is hidden. Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,0<h<=240', 'AV,h>2180'; and 'All' will accumulate all percentages. This is reported whenever an HTML Media Element is suspended or resumed, such as when the page is unloaded.",
     "keyed": true,
     "kind": "linear",
     "high": 100,
     "n_buckets": 50,
     "bug_numbers": [1287987]
   },
+  "VIDEO_INTER_KEYFRAME_AVERAGE_MS" : {
+    "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"],
+    "expires_in_version": "55",
+    "description": "Average interval between video keyframes in played videos, in milliseconds. Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,0<h<=240', 'AV,h>2180'; and 'All' will accumulate all percentages. This is reported whenever an HTML Media Element is suspended or resumed, such as when the page is unloaded.",
+    "keyed": true,
+    "kind": "exponential",
+    "high": 60000,
+    "n_buckets": 100,
+    "bug_numbers": [1289668]
+  },
+  "VIDEO_INTER_KEYFRAME_MAX_MS" : {
+    "alert_emails": ["ajones@mozilla.com", "gsquelart@mozilla.com"],
+    "expires_in_version": "55",
+    "description": "Maximum interval between video keyframes in played videos, in milliseconds. Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,0<h<=240', 'AV,h>2180'; and 'All' will accumulate all percentages. This is reported whenever an HTML Media Element is suspended or resumed, such as when the page is unloaded.",
+    "keyed": true,
+    "kind": "exponential",
+    "high": 60000,
+    "n_buckets": 100,
+    "bug_numbers": [1289668]
+  },
   "VIDEO_UNLOAD_STATE": {
     "alert_emails": ["ajones@mozilla.com"],
     "expires_in_version": "55",
     "kind": "enumerated",
     "n_values": 5,
     "description": "HTML Media Element state when unloading. ended = 0, paused = 1, stalled = 2, seeking = 3, other = 4",
     "bug_numbers": [1261955, 1261955]
   },
@@ -8303,115 +8323,91 @@
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 20,
     "description": "Sanitize: Total time it takes to sanitize (ms)"
   },
   "FX_SANITIZE_CACHE": {
     "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "50",
+    "expires_in_version": "never",
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 20,
     "description": "Sanitize: Time it takes to sanitize the cache (ms)"
   },
-  "FX_SANITIZE_COOKIES": {
-    "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "50",
-    "kind": "exponential",
-    "high": 30000,
-    "n_buckets": 20,
-    "description": "Sanitize: Time it takes to sanitize cookies (ms)"
-  },
   "FX_SANITIZE_COOKIES_2": {
     "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "50",
+    "expires_in_version": "never",
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 20,
     "description": "Sanitize: Time it takes to sanitize firefox cookies (ms). A subset of FX_SANITIZE_COOKIES."
   },
-  "FX_SANITIZE_PLUGINS": {
-    "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "50",
-    "kind": "exponential",
-    "high": 30000,
-    "n_buckets": 20,
-    "description": "Sanitize: Time it takes to sanitize plugin cookies (ms). A subset of FX_SANITIZE_COOKIES."
-  },
   "FX_SANITIZE_LOADED_FLASH": {
     "alert_emails": ["firefox-dev@mozilla.org"],
     "bug_numbers": [1251469],
-    "expires_in_version": "50",
+    "expires_in_version": "55",
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 20,
     "description": "Sanitize: Time it takes to sanitize Flash when it's already loaded (ms). A subset of FX_SANITIZE_PLUGINS."
   },
   "FX_SANITIZE_UNLOADED_FLASH": {
     "alert_emails": ["firefox-dev@mozilla.org"],
     "bug_numbers": [1251469],
-    "expires_in_version": "50",
+    "expires_in_version": "55",
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 20,
     "description": "Sanitize: Time it takes to sanitize Flash when it's not yet loaded (ms). A subset of FX_SANITIZE_PLUGINS."
   },
-  "FX_SANITIZE_OFFLINEAPPS": {
-    "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "50",
-    "kind": "exponential",
-    "high": 30000,
-    "n_buckets": 20,
-    "description": "Sanitize: Time it takes to sanitize stored offline app data (ms)"
-  },
   "FX_SANITIZE_HISTORY": {
     "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "50",
+    "expires_in_version": "never",
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 20,
     "description": "Sanitize: Time it takes to sanitize history (ms)"
   },
   "FX_SANITIZE_FORMDATA": {
     "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "50",
+    "expires_in_version": "never",
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 20,
     "description": "Sanitize: Time it takes to sanitize stored form data (ms)"
   },
   "FX_SANITIZE_DOWNLOADS": {
     "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "50",
+    "expires_in_version": "never",
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 20,
     "description": "Sanitize: Time it takes to sanitize recent downloads (ms)"
   },
   "FX_SANITIZE_SESSIONS": {
     "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "50",
+    "expires_in_version": "never",
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 20,
     "description": "Sanitize: Time it takes to sanitize saved sessions (ms)"
   },
   "FX_SANITIZE_SITESETTINGS": {
     "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "50",
+    "expires_in_version": "never",
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 20,
     "description": "Sanitize: Time it takes to sanitize site-specific settings (ms)"
   },
   "FX_SANITIZE_OPENWINDOWS": {
     "alert_emails": ["firefox-dev@mozilla.org"],
-    "expires_in_version": "50",
+    "expires_in_version": "never",
     "kind": "exponential",
     "high": 30000,
     "n_buckets": 20,
     "description": "Sanitize: Time it takes to sanitize the open windows list (ms)"
   },
   "PWMGR_BLOCKLIST_NUM_SITES": {
     "expires_in_version": "never",
     "kind": "exponential",
--- a/toolkit/content/widgets/findbar.xml
+++ b/toolkit/content/widgets/findbar.xml
@@ -537,16 +537,18 @@
         <parameter name="aHighlight"/>
         <parameter name="aFromPrefObserver"/>
         <body><![CDATA[
           if (aHighlight === this._highlightAll) {
             return;
           }
           this._highlightAll = aHighlight;
 
+          this.browser.finder.onHighlightAllChange(aHighlight);
+
           if (!this._dispatchFindEvent("highlightallchange")) {
             return;
           }
 
           this._setHighlightAll(aHighlight, aFromPrefObserver);
 
           let word = this._findField.value;
           // Bug 429723. Don't attempt to highlight ""
--- a/toolkit/library/Makefile.in
+++ b/toolkit/library/Makefile.in
@@ -8,18 +8,11 @@ include $(topsrcdir)/config/config.mk
 
 ifeq (WINNT_1,$(OS_TARGET)_$(MOZ_PROFILE_USE))
 # Wrap linker to measure peak virtual memory usage.
 EXPAND_LIBS_EXEC := $(PYTHON) $(topsrcdir)/config/link.py linker-vsize
 endif
 
 include $(topsrcdir)/config/rules.mk
 
-ifdef COMPILE_ENVIRONMENT
-target:: $(FINAL_TARGET)/dependentlibs.list
-endif
-
-$(FINAL_TARGET)/dependentlibs.list: $(topsrcdir)/toolkit/library/dependentlibs.py $(SHARED_LIBRARY) $(wildcard $(if $(wildcard $(FINAL_TARGET)/dependentlibs.list),$(addprefix $(FINAL_TARGET)/,$(shell cat $(FINAL_TARGET)/dependentlibs.list))))
-	$(PYTHON) $< $(SHARED_LIBRARY) -L $(FINAL_TARGET) $(if $(TOOLCHAIN_PREFIX),$(addprefix -p ,$(TOOLCHAIN_PREFIX))) > $@
-
 .PHONY: gtestxul
 gtestxul:
 	$(MAKE) -C $(DEPTH) toolkit/library/gtest/target LINK_GTEST=1
--- a/toolkit/library/dependentlibs.py
+++ b/toolkit/library/dependentlibs.py
@@ -1,31 +1,28 @@
 # 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/.
 
 '''Given a library, dependentlibs.py prints the list of libraries it depends
 upon that are in the same directory, followed by the library itself.
 '''
 
-from optparse import OptionParser
 import os
 import re
-import fnmatch
 import subprocess
 import sys
+import mozpack.path as mozpath
 from mozpack.executables import (
     get_type,
     ELF,
     MACHO,
 )
 from buildconfig import substs
 
-TOOLCHAIN_PREFIX = ''
-
 def dependentlibs_dumpbin(lib):
     '''Returns the list of dependencies declared in the given DLL'''
     try:
         proc = subprocess.Popen(['dumpbin', '-dependents', lib], stdout = subprocess.PIPE)
     except OSError:
         # dumpbin is missing, probably mingw compilation. Try using objdump.
         return dependentlibs_mingw_objdump(lib)
     deps = []
@@ -49,17 +46,17 @@ def dependentlibs_mingw_objdump(lib):
         match = re.match('\tDLL Name: (\S+)', line)
         if match:
             deps.append(match.group(1))
     proc.wait()
     return deps
 
 def dependentlibs_readelf(lib):
     '''Returns the list of dependencies declared in the given ELF .so'''
-    proc = subprocess.Popen([TOOLCHAIN_PREFIX + 'readelf', '-d', lib], stdout = subprocess.PIPE)
+    proc = subprocess.Popen([substs.get('TOOLCHAIN_PREFIX', '') + 'readelf', '-d', lib], stdout = subprocess.PIPE)
     deps = []
     for line in proc.stdout:
         # Each line has the following format:
         #  tag (TYPE)          value
         # Looking for NEEDED type entries
         tmp = line.split(' ', 3)
         if len(tmp) > 3 and tmp[2] == '(NEEDED)':
             # NEEDED lines look like:
@@ -103,38 +100,36 @@ def dependentlibs(lib, libpaths, func):
         for dir in libpaths:
             deppath = os.path.join(dir, dep)
             if os.path.exists(deppath):
                 deps.extend([d for d in dependentlibs(deppath, libpaths, func) if not d in deps])
                 # Black list the ICU data DLL because preloading it at startup
                 # leads to startup performance problems because of its excessive
                 # size (around 10MB).
                 if not dep.startswith("icu"):
-                    deps.append(dep)
+                    deps.append(deppath)
                 break
 
     return deps
 
-def main():
-    parser = OptionParser()
-    parser.add_option("-L", dest="libpaths", action="append", metavar="PATH", help="Add the given path to the library search path")
-    parser.add_option("-p", dest="toolchain_prefix", metavar="PREFIX", help="Use the given prefix to readelf")
-    (options, args) = parser.parse_args()
-    if options.toolchain_prefix:
-        global TOOLCHAIN_PREFIX
-        TOOLCHAIN_PREFIX = options.toolchain_prefix
-    lib = args[0]
+def gen_list(output, lib):
+    libpaths = [os.path.join(substs['DIST'], 'bin')]
     binary_type = get_type(lib)
     if binary_type == ELF:
         func = dependentlibs_readelf
     elif binary_type == MACHO:
         func = dependentlibs_otool
     else:
         ext = os.path.splitext(lib)[1]
         assert(ext == '.dll')
         func = dependentlibs_dumpbin
-    if not options.libpaths:
-        options.libpaths = [os.path.dirname(lib)]
 
-    print '\n'.join(dependentlibs(lib, options.libpaths, func) + [lib])
+    deps = dependentlibs(lib, libpaths, func)
+    deps.append(mozpath.join(libpaths[0], lib))
+    dependentlibs_output = [mozpath.basename(f) for f in deps]
+    output.write('\n'.join(dependentlibs_output) + '\n')
+    return set(deps)
+
+def main():
+    gen_list(sys.stdout, sys.argv[1])
 
 if __name__ == '__main__':
     main()
--- a/toolkit/library/moz.build
+++ b/toolkit/library/moz.build
@@ -358,10 +358,26 @@ if CONFIG['OS_ARCH'] == 'WINNT':
         ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     OS_LIBS += [
         'usp10',
         'oleaut32',
     ]
 
+if CONFIG['COMPILE_ENVIRONMENT']:
+    if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('cocoa', 'uikit'):
+        full_libname = SHARED_LIBRARY_NAME
+    else:
+        full_libname = '%s%s%s' % (
+            CONFIG['DLL_PREFIX'],
+            LIBRARY_NAME,
+            CONFIG['DLL_SUFFIX']
+        )
+    GENERATED_FILES += ['dependentlibs.list']
+    GENERATED_FILES['dependentlibs.list'].script = 'dependentlibs.py:gen_list'
+    GENERATED_FILES['dependentlibs.list'].inputs = [
+        '!%s' % full_libname,
+    ]
+    FINAL_TARGET_FILES += ['!dependentlibs.list']
+
 # This needs to be last
 USE_LIBS += ['StaticXULComponentsEnd']
--- a/toolkit/modules/Finder.jsm
+++ b/toolkit/modules/Finder.jsm
@@ -316,16 +316,21 @@ Finder.prototype = {
     this.highlighter.highlight(false);
   },
 
   onModalHighlightChange(useModalHighlight) {
     if (this._highlighter)
       this._highlighter.onModalHighlightChange(useModalHighlight);
   },
 
+  onHighlightAllChange(highlightAll) {
+    if (this._highlighter)
+