merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Thu, 26 Nov 2015 11:57:05 +0100
changeset 274192 c321d84038519dcf1670d59fd2c5c00ad8a85a55
parent 274160 6beb4e02f810bbcecdd12f73de0008fb7f5dbfb5 (current diff)
parent 274191 23eb26d374c6bb374cf7f2b23a77a7147f7a5371 (diff)
child 274194 6924a22877b3ca56160aead6c59434824c23774c
push id29724
push usercbook@mozilla.com
push dateThu, 26 Nov 2015 10:57:21 +0000
treeherdermozilla-central@c321d8403851 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone45.0a1
first release with
nightly linux32
c321d8403851 / 45.0a1 / 20151126030226 / files
nightly linux64
c321d8403851 / 45.0a1 / 20151126030226 / files
nightly mac
c321d8403851 / 45.0a1 / 20151126030226 / files
nightly win32
c321d8403851 / 45.0a1 / 20151126030226 / files
nightly win64
c321d8403851 / 45.0a1 / 20151126030226 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
devtools/client/debugger/views/sources-view.js
devtools/client/shared/redux/reducers.js
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -169,16 +169,17 @@
 @RESPATH@/components/docshell.xpt
 @RESPATH@/components/dom.xpt
 @RESPATH@/components/dom_activities.xpt
 @RESPATH@/components/dom_apps.xpt
 @RESPATH@/components/dom_newapps.xpt
 @RESPATH@/components/dom_audiochannel.xpt
 @RESPATH@/components/dom_base.xpt
 @RESPATH@/components/dom_system.xpt
+@RESPATH@/components/dom_workers.xpt
 #ifdef MOZ_WIDGET_GONK
 @RESPATH@/components/dom_wifi.xpt
 @RESPATH@/components/dom_system_gonk.xpt
 #endif
 #ifdef MOZ_B2G_RIL
 @RESPATH@/components/dom_wappush.xpt
 @RESPATH@/components/dom_mobileconnection.xpt
 #endif
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1473,24 +1473,24 @@ pref("network.disable.ipc.security", tru
 
 // CustomizableUI debug logging.
 pref("browser.uiCustomization.debug", false);
 
 // CustomizableUI state of the browser's user interface
 pref("browser.uiCustomization.state", "");
 
 // The remote content URL shown for FxA signup. Must use HTTPS.
-pref("identity.fxaccounts.remote.signup.uri", "https://accounts.firefox.com/signup?service=sync&context=fx_desktop_v1");
+pref("identity.fxaccounts.remote.signup.uri", "https://accounts.firefox.com/signup?service=sync&context=fx_desktop_v2");
 
 // The URL where remote content that forces re-authentication for Firefox Accounts
 // should be fetched.  Must use HTTPS.
-pref("identity.fxaccounts.remote.force_auth.uri", "https://accounts.firefox.com/force_auth?service=sync&context=fx_desktop_v1");
+pref("identity.fxaccounts.remote.force_auth.uri", "https://accounts.firefox.com/force_auth?service=sync&context=fx_desktop_v2");
 
 // The remote content URL shown for signin in. Must use HTTPS.
-pref("identity.fxaccounts.remote.signin.uri", "https://accounts.firefox.com/signin?service=sync&context=fx_desktop_v1");
+pref("identity.fxaccounts.remote.signin.uri", "https://accounts.firefox.com/signin?service=sync&context=fx_desktop_v2");
 
 // The remote content URL where FxAccountsWebChannel messages originate.
 pref("identity.fxaccounts.remote.webchannel.uri", "https://accounts.firefox.com/");
 
 // The URL we take the user to when they opt to "manage" their Firefox Account.
 // Note that this will always need to be in the same TLD as the
 // "identity.fxaccounts.remote.signup.uri" pref.
 pref("identity.fxaccounts.settings.uri", "https://accounts.firefox.com/settings");
--- a/browser/components/distribution.js
+++ b/browser/components/distribution.js
@@ -197,16 +197,41 @@ DistributionCustomizer.prototype = {
         if (item.description) {
           let bmId = yield PlacesUtils.promiseItemId(bm.guid);
           PlacesUtils.annotations.setItemAnnotation(bmId,
                                                     "bookmarkProperties/description",
                                                     item.description, 0,
                                                     PlacesUtils.annotations.EXPIRE_NEVER);
         }
 
+        if (item.icon && item.iconData) {
+          try {
+            let faviconURI = this._makeURI(item.icon);
+            PlacesUtils.favicons.replaceFaviconDataFromDataURL(
+              faviconURI, item.iconData, 0,
+              Services.scriptSecurityManager.getSystemPrincipal());
+
+            PlacesUtils.favicons.setAndFetchFaviconForPage(
+              this._makeURI(item.link), faviconURI, false,
+              PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, null,
+              Services.scriptSecurityManager.getSystemPrincipal());
+          } catch(e) {
+            Cu.reportError(e);
+          }
+        }
+
+        if (item.keyword) {
+          try {
+            yield PlacesUtils.keywords.insert({ keyword: item.keyword,
+                                                url: item.link });
+          } catch(e) {
+            Cu.reportError(e);
+          }
+        }
+
         break;
       }
     }
   }),
 
   _customizationsApplied: false,
   applyCustomizations: function DIST_applyCustomizations() {
     this._customizationsApplied = true;
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -1,105 +1,112 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
                                   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+
+const INTEGER = /^[1-9]\d*$/;
+
 var {
   EventManager,
 } = ExtensionUtils;
 
 // This file provides some useful code for the |tabs| and |windows|
 // modules. All of the code is installed on |global|, which is a scope
 // shared among the different ext-*.js scripts.
 
 
 // Manages icon details for toolbar buttons in the |pageAction| and
 // |browserAction| APIs.
 global.IconDetails = {
-  // Accepted icon sizes.
-  SIZES: ["19", "38"],
-
   // Normalizes the various acceptable input formats into an object
-  // with two properties, "19" and "38", containing icon URLs.
+  // with icon size as key and icon URL as value.
+  //
+  // If a context is specified (function is called from an extension):
+  // Throws an error if an invalid icon size was provided or the
+  // extension is not allowed to load the specified resources.
+  //
+  // If no context is specified, instead of throwing an error, this
+  // function simply logs a warning message.
   normalize(details, extension, context=null, localize=false) {
     let result = {};
 
-    if (details.imageData) {
-      let imageData = details.imageData;
+    try {
+      if (details.imageData) {
+        let imageData = details.imageData;
 
-      if (imageData instanceof Cu.getGlobalForObject(imageData).ImageData) {
-        imageData = {"19": imageData};
-      }
+        if (imageData instanceof Cu.getGlobalForObject(imageData).ImageData) {
+          imageData = {"19": imageData};
+        }
 
-      for (let size of this.SIZES) {
-        if (size in imageData) {
+        for (let size of Object.keys(imageData)) {
+          if (!INTEGER.test(size)) {
+            throw new Error(`Invalid icon size ${size}, must be an integer`);
+          }
+
           result[size] = this.convertImageDataToPNG(imageData[size], context);
         }
       }
-    }
+
+      if (details.path) {
+        let path = details.path;
+        if (typeof path != "object") {
+          path = {"19": path};
+        }
 
-    if (details.path) {
-      let path = details.path;
-      if (typeof path != "object") {
-        path = {"19": path};
-      }
+        let baseURI = context ? context.uri : extension.baseURI;
 
-      let baseURI = context ? context.uri : extension.baseURI;
+        for (let size of Object.keys(path)) {
+          if (!INTEGER.test(size)) {
+            throw new Error(`Invalid icon size ${size}, must be an integer`);
+          }
 
-      for (let size of this.SIZES) {
-        if (size in path) {
           let url = path[size];
           if (localize) {
             url = extension.localize(url);
           }
 
           url = baseURI.resolve(path[size]);
 
           // The Chrome documentation specifies these parameters as
           // relative paths. We currently accept absolute URLs as well,
           // which means we need to check that the extension is allowed
-          // to load them.
-          try {
-            Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
-              extension.principal, url,
-              Services.scriptSecurityManager.DISALLOW_SCRIPT);
-          } catch (e) {
-            if (context) {
-              throw e;
-            }
-            // If there's no context, it's because we're handling this
-            // as a manifest directive. Log a warning rather than
-            // raising an error, but don't accept the URL in any case.
-            extension.manifestError(`Access to URL '${url}' denied`);
-            continue;
-          }
+          // to load them. This will throw an error if it's not allowed.
+          Services.scriptSecurityManager.checkLoadURIStrWithPrincipal(
+            extension.principal, url,
+            Services.scriptSecurityManager.DISALLOW_SCRIPT);
 
           result[size] = url;
         }
       }
+    } catch (e) {
+      // Function is called from extension code, delegate error.
+      if (context) {
+        throw e;
+      }
+      // If there's no context, it's because we're handling this
+      // as a manifest directive. Log a warning rather than
+      // raising an error.
+      extension.manifestError(`Invalid icon data: ${e}`);
     }
 
     return result;
   },
 
   // Returns the appropriate icon URL for the given icons object and the
   // screen resolution of the given window.
   getURL(icons, window, extension) {
     const DEFAULT = "chrome://browser/content/extension.svg";
 
-    // Use the higher resolution image if we're doing any up-scaling
-    // for high resolution monitors.
-    let res = window.devicePixelRatio;
-    let size = res > 1 ? "38" : "19";
-
-    return icons[size] || icons["19"] || icons["38"] || DEFAULT;
+    return AddonManager.getPreferredIconURL({icons: icons}, 18, window) || DEFAULT;
   },
 
   convertImageDataToPNG(imageData, context) {
     let document = context.contentWindow.document;
     let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
     canvas.width = imageData.width;
     canvas.height = imageData.height;
     canvas.getContext("2d").putImageData(imageData, 0, 0);
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -96,16 +96,39 @@ add_task(function* testDetailsObjects() 
           "1": browser.runtime.getURL("data/a.png"),
           "2": imageData.red.url, } },
       { details: {
           "path": { "38": "a.png" },
           "imageData": imageData.red.imageData, },
         resolutions: {
           "1": imageData.red.url,
           "2": browser.runtime.getURL("data/a.png"), } },
+
+      // Various resolutions
+      { details: { "path": { "18": "a.png", "32": "a-x2.png" } },
+        resolutions: {
+          "1": browser.runtime.getURL("data/a.png"),
+          "2": browser.runtime.getURL("data/a-x2.png"), } },
+      { details: { "path": { "16": "16.png", "100": "100.png" } },
+        resolutions: {
+          "1": browser.runtime.getURL("data/100.png"),
+          "2": browser.runtime.getURL("data/100.png"), } },
+      { details: { "path": { "2": "2.png"} },
+        resolutions: {
+          "1": browser.runtime.getURL("data/2.png"),
+          "2": browser.runtime.getURL("data/2.png"), } },
+      { details: { "path": {
+        "6": "6.png",
+        "18": "18.png",
+        "32": "32.png",
+        "48": "48.png",
+        "128": "128.png" } },
+        resolutions: {
+          "1": browser.runtime.getURL("data/18.png"),
+          "2": browser.runtime.getURL("data/48.png"), } },
     ];
 
     // Allow serializing ImageData objects for logging.
     ImageData.prototype.toJSON = () => "<ImageData>";
 
     var tabId;
 
     browser.test.onMessage.addListener((msg, test) => {
@@ -192,16 +215,72 @@ add_task(function* testDetailsObjects() 
 
     let pageActionImage = document.getElementById(pageActionId);
     is(pageActionImage.src, imageURL, "page action has the correct image");
   }
 
   yield extension.unload();
 });
 
+// Test that an error is thrown when providing invalid icon sizes
+add_task(function *testInvalidIconSizes() {
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "browser_action": {},
+      "page_action": {},
+    },
+
+    background: function () {
+      browser.tabs.query({ active: true, currentWindow: true }, tabs => {
+        var tabId = tabs[0].id;
+
+        for (var api of ["pageAction", "browserAction"]) {
+          // helper function to run setIcon and check if it fails
+          let assertSetIconThrows = function(detail, error, message) {
+            try {
+              detail.tabId = tabId;
+              browser[api].setIcon(detail);
+
+              browser.test.fail("Expected an error on invalid icon size.");
+              browser.test.notifyFail("setIcon with invalid icon size");
+              return;
+            } catch (e) {
+              browser.test.succeed("setIcon with invalid icon size");
+            }
+          }
+
+          // test invalid icon size inputs
+          for (var type of ["path", "imageData"]) {
+            assertSetIconThrows({ [type]: { "abcdef": "test.png" } });
+            assertSetIconThrows({ [type]: { "48px": "test.png" } });
+            assertSetIconThrows({ [type]: { "20.5": "test.png" } });
+            assertSetIconThrows({ [type]: { "5.0": "test.png" } });
+            assertSetIconThrows({ [type]: { "-300": "test.png" } });
+            assertSetIconThrows({ [type]: {
+              "abc": "test.png",
+              "5": "test.png"
+            }});
+          }
+
+          assertSetIconThrows({ imageData: { "abcdef": "test.png" }, path: {"5": "test.png"} });
+          assertSetIconThrows({ path: { "abcdef": "test.png" }, imageData: {"5": "test.png"} });
+        }
+
+        browser.test.notifyPass("setIcon with invalid icon size");
+      });
+    }
+  });
+
+  yield Promise.all([extension.startup(), extension.awaitFinish("setIcon with invalid icon size")]);
+
+  yield extension.unload();
+});
+
+
 // Test that default icon details in the manifest.json file are handled
 // correctly.
 add_task(function *testDefaultDetails() {
   // TODO: Test localized variants.
   let icons = [
     "foo/bar.png",
     "/foo/bar.png",
     { "19": "foo/bar.png" },
@@ -289,32 +368,32 @@ add_task(function* testSecureURLsDenied(
 
         browser.test.notifyPass("setIcon security tests");
       });
     },
   });
 
   yield extension.startup();
 
-  yield extension.awaitFinish();
+  yield extension.awaitFinish("setIcon security tests");
   yield extension.unload();
 
 
   // Test URLs included in the manifest.
 
   let urls = ["chrome://browser/content/browser.xul",
               "javascript:true"];
 
   let matchURLForbidden = url => ({
-    message: new RegExp(`Loading extension.*Access to.*'${url}' denied`),
+    message: new RegExp(`Loading extension.*Invalid icon data: NS_ERROR_DOM_BAD_URI`),
   });
 
+  // Because the underlying method throws an error on invalid data,
+  // only the first invalid URL of each component will be logged.
   let messages = [matchURLForbidden(urls[0]),
-                  matchURLForbidden(urls[1]),
-                  matchURLForbidden(urls[0]),
                   matchURLForbidden(urls[1])];
 
   let waitForConsole = new Promise(resolve => {
     // Not necessary in browser-chrome tests, but monitorConsole gripes
     // if we don't call it.
     SimpleTest.waitForExplicitFinish();
 
     SimpleTest.monitorConsole(resolve, messages);
@@ -325,18 +404,18 @@ add_task(function* testSecureURLsDenied(
       "browser_action": {
         "default_icon": {
           "19": urls[0],
           "38": urls[1],
         },
       },
       "page_action": {
         "default_icon": {
-          "19": urls[0],
-          "38": urls[1],
+          "19": urls[1],
+          "38": urls[0],
         },
       },
     },
   });
 
   yield extension.startup();
   yield extension.unload();
 
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -435,17 +435,26 @@ loop.panel = (function(_, mozL10n) {
     },
 
     handleClickEntry: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
         roomToken: this.props.room.roomToken
       }));
-      this.closeWindow();
+
+      // Open url if needed.
+      loop.request("getSelectedTabMetadata").then(function(metadata) {
+        var contextURL = this.props.room.decryptedContext.urls &&
+          this.props.room.decryptedContext.urls[0].location;
+        if (contextURL && metadata.url !== contextURL) {
+          loop.request("OpenURL", contextURL);
+        }
+        this.closeWindow();
+      }.bind(this));
     },
 
     handleClick: function(e) {
       e.preventDefault();
       e.stopPropagation();
 
       this.setState({
         eventPosY: e.pageY
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -435,17 +435,26 @@ loop.panel = (function(_, mozL10n) {
     },
 
     handleClickEntry: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
         roomToken: this.props.room.roomToken
       }));
-      this.closeWindow();
+
+      // Open url if needed.
+      loop.request("getSelectedTabMetadata").then(function(metadata) {
+        var contextURL = this.props.room.decryptedContext.urls &&
+          this.props.room.decryptedContext.urls[0].location;
+        if (contextURL && metadata.url !== contextURL) {
+          loop.request("OpenURL", contextURL);
+        }
+        this.closeWindow();
+      }.bind(this));
     },
 
     handleClick: function(e) {
       e.preventDefault();
       e.stopPropagation();
 
       this.setState({
         eventPosY: e.pageY
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -77,17 +77,20 @@ describe("loop.panel", function() {
       GetUserProfile: function() { return null; }
     });
 
     roomName = "First Room Name";
     roomData = {
       roomToken: "QzBbvGmIZWU",
       roomUrl: "http://sample/QzBbvGmIZWU",
       decryptedContext: {
-        roomName: roomName
+        roomName: roomName,
+        urls: [{
+          location: "http://testurl.com"
+        }]
       },
       maxSize: 2,
         participants: [{
         displayName: "Alexis",
           account: "alexis@example.com",
           roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
       }, {
         displayName: "Adam",
@@ -660,22 +663,33 @@ describe("loop.panel", function() {
 
         TestUtils.Simulate.click(node, fakeEvent);
 
         expect(view.state.showMenu).to.eql(!prevState);
       });
     });
 
     describe("Copy button", function() {
-      var roomEntry;
+      var roomEntry, openURLStub;
 
       beforeEach(function() {
         // Stub to prevent warnings where no stores are set up to handle the
         // actions we are testing.
         sandbox.stub(dispatcher, "dispatch");
+        openURLStub = sinon.stub();
+
+        LoopMochaUtils.stubLoopRequest({
+          GetSelectedTabMetadata: function() {
+            return {
+              url: "http://invalid.com",
+              description: "fakeSite"
+            };
+          },
+          OpenURL: openURLStub
+        });
 
         roomEntry = mountRoomEntry({
           deleteRoom: sandbox.stub(),
           isOpenedRoom: false,
           room: new loop.store.Room(roomData)
         });
       });
 
@@ -712,16 +726,42 @@ describe("loop.panel", function() {
             isOpenedRoom: true,
             room: new loop.store.Room(roomData)
           });
 
           TestUtils.Simulate.click(roomEntry.refs.roomEntry.getDOMNode());
 
           sinon.assert.notCalled(dispatcher.dispatch);
         });
+
+        it("should open a new tab with the room context if it is not the same as the currently open tab", function() {
+          TestUtils.Simulate.click(roomEntry.refs.roomEntry.getDOMNode());
+          sinon.assert.calledOnce(openURLStub);
+          sinon.assert.calledWithExactly(openURLStub, "http://testurl.com");
+        });
+
+        it("should not open a new tab if the context is the same as the currently open tab", function() {
+          LoopMochaUtils.stubLoopRequest({
+            GetSelectedTabMetadata: function() {
+              return {
+                url: "http://testurl.com",
+                description: "fakeSite"
+              };
+            }
+          });
+
+          roomEntry = mountRoomEntry({
+            deleteRoom: sandbox.stub(),
+            isOpenedRoom: false,
+            room: new loop.store.Room(roomData)
+          });
+
+          TestUtils.Simulate.click(roomEntry.refs.roomEntry.getDOMNode());
+          sinon.assert.notCalled(openURLStub);
+        });
       });
     });
 
     describe("Context Indicator", function() {
       var roomEntry;
 
       function mountEntryForContext() {
         return mountRoomEntry({
--- a/browser/components/migration/EdgeProfileMigrator.js
+++ b/browser/components/migration/EdgeProfileMigrator.js
@@ -2,24 +2,26 @@
  * 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/. */
 
 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource:///modules/MigrationUtils.jsm");
 Cu.import("resource:///modules/MSMigrationUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
                                   "resource://gre/modules/PlacesUtils.jsm");
 
 const kEdgeRegistryRoot = "SOFTWARE\\Classes\\Local Settings\\Software\\" +
   "Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\" +
   "microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge";
+const kEdgeReadingListPath = "AC\\MicrosoftEdge\\User\\Default\\DataStore\\Data\\";
 
 function EdgeTypedURLMigrator() {
 }
 
 EdgeTypedURLMigrator.prototype = {
   type: MigrationUtils.resourceTypes.HISTORY,
 
   get _typedURLs() {
@@ -74,26 +76,133 @@ EdgeTypedURLMigrator.prototype = {
       handleError: function() {},
       handleCompletion: function() {
         aCallback(this._success);
       }
     });
   },
 }
 
+function EdgeReadingListMigrator() {
+}
+
+EdgeReadingListMigrator.prototype = {
+  type: MigrationUtils.resourceTypes.BOOKMARKS,
+
+  get exists() {
+    return !!MSMigrationUtils.getEdgeLocalDataFolder();
+  },
+
+  migrate(callback) {
+    this._migrateReadingList(PlacesUtils.bookmarks.menuGuid).then(
+      () => callback(true),
+      ex => {
+        Cu.reportError(ex);
+        callback(false);
+      }
+    );
+  },
+
+  _migrateReadingList: Task.async(function*(parentGuid) {
+    let edgeDir = MSMigrationUtils.getEdgeLocalDataFolder();
+    if (!edgeDir) {
+      return;
+    }
+    this._readingListExtractor = Cc["@mozilla.org/profile/migrator/edgereadinglistextractor;1"].
+                                 createInstance(Ci.nsIEdgeReadingListExtractor);
+    edgeDir.appendRelativePath(kEdgeReadingListPath);
+    let errorProduced = null;
+    if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
+      let expectedDir = edgeDir.clone();
+      expectedDir.appendRelativePath("nouser1\\120712-0049");
+      if (expectedDir.exists() && expectedDir.isReadable() && expectedDir.isDirectory()) {
+        yield this._migrateReadingListDB(expectedDir, parentGuid).catch(ex => {
+          if (!errorProduced)
+            errorProduced = ex;
+        });
+      } else {
+        let getSubdirs = someDir => {
+          let subdirs = someDir.directoryEntries;
+          let rv = [];
+          while (subdirs.hasMoreElements()) {
+            let subdir = subdirs.getNext().QueryInterface(Ci.nsIFile);
+            if (subdir.isDirectory() && subdir.isReadable()) {
+              rv.push(subdir);
+            }
+          }
+          return rv;
+        };
+        let dirs = getSubdirs(edgeDir).map(getSubdirs);
+        for (let dir of dirs) {
+          yield this._migrateReadingListDB(dir, parentGuid).catch(ex => {
+            if (!errorProduced)
+              errorProduced = ex;
+          });
+        }
+      }
+    }
+    if (errorProduced) {
+      throw errorProduced;
+    }
+  }),
+  _migrateReadingListDB: Task.async(function*(dbFile, parentGuid) {
+    dbFile.appendRelativePath("DBStore\\spartan.edb");
+
+    if (!dbFile.exists() || !dbFile.isReadable() || !dbFile.isFile()) {
+      return;
+    }
+    let readingListItems;
+    try {
+      readingListItems = this._readingListExtractor.extract(dbFile.path);
+    } catch (ex) {
+      Cu.reportError("Failed to extract Edge reading list information from " +
+                     "the database at " + dbFile.path + " due to the following error: " + ex);
+      // Deliberately make this fail so we expose failure in the UI:
+      throw ex;
+      return;
+    }
+    if (!readingListItems.length) {
+      return;
+    }
+    let destFolderGuid = yield this._ensureReadingListFolder(parentGuid);
+    for (let i = 0; i < readingListItems.length; i++) {
+      let readingListItem = readingListItems.queryElementAt(i, Ci.nsIPropertyBag2);
+      let url = readingListItem.get("uri");
+      let title = readingListItem.get("title");
+      let time = readingListItem.get("time");
+      // time is a PRTime, which is microseconds (since unix epoch), or null.
+      // We need milliseconds for the date constructor, so divide by 1000:
+      let dateAdded = time ? new Date(time / 1000) : new Date();
+      yield PlacesUtils.bookmarks.insert({
+        parentGuid: destFolderGuid, url: url, title, dateAdded
+      });
+    }
+  }),
+
+  _ensureReadingListFolder: Task.async(function*(parentGuid) {
+    if (!this.__readingListFolderGuid) {
+      let folderTitle = MigrationUtils.getLocalizedString("importedEdgeReadingList");
+      let folderSpec = {type: PlacesUtils.bookmarks.TYPE_FOLDER, parentGuid, title: folderTitle};
+      this.__readingListFolderGuid = (yield PlacesUtils.bookmarks.insert(folderSpec)).guid;
+    }
+    return this.__readingListFolderGuid;
+  }),
+};
+
 function EdgeProfileMigrator() {
 }
 
 EdgeProfileMigrator.prototype = Object.create(MigratorPrototype);
 
 EdgeProfileMigrator.prototype.getResources = function() {
   let resources = [
     MSMigrationUtils.getBookmarksMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE),
     MSMigrationUtils.getCookiesMigrator(MSMigrationUtils.MIGRATION_TYPE_EDGE),
     new EdgeTypedURLMigrator(),
+    new EdgeReadingListMigrator(),
   ];
   let windowsVaultFormPasswordsMigrator =
     MSMigrationUtils.getWindowsVaultFormPasswordsMigrator();
   windowsVaultFormPasswordsMigrator.name = "EdgeVaultFormPasswords";
   resources.push(windowsVaultFormPasswordsMigrator);
   return resources.filter(r => r.exists);
 };
 
--- a/browser/components/migration/MSMigrationUtils.jsm
+++ b/browser/components/migration/MSMigrationUtils.jsm
@@ -21,17 +21,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
                                   "resource://gre/modules/WindowsRegistry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ctypes",
                                   "resource://gre/modules/ctypes.jsm");
 
 const EDGE_COOKIE_PATH_OPTIONS = ["", "#!001\\", "#!002\\"];
 const EDGE_COOKIES_SUFFIX = "MicrosoftEdge\\Cookies";
 const EDGE_FAVORITES = "AC\\MicrosoftEdge\\User\\Default\\Favorites";
-const EDGE_READINGLIST = "AC\\MicrosoftEdge\\User\\Default\\DataStore\\Data\\";
 const FREE_CLOSE_FAILED = 0;
 const INTERNET_EXPLORER_EDGE_GUID = [0x3CCD5499,
                                      0x4B1087A8,
                                      0x886015A2,
                                      0x553BDD88];
 const RESULT_SUCCESS = 0;
 const VAULT_ENUMERATE_ALL_ITEMS = 512;
 const WEB_CREDENTIALS_VAULT_ID = [0x4BF4C442,
@@ -371,30 +370,27 @@ Bookmarks.prototype = {
     return Task.spawn(function* () {
       // Import to the bookmarks menu.
       let folderGuid = PlacesUtils.bookmarks.menuGuid;
       if (!MigrationUtils.isStartupMigration) {
         folderGuid =
           yield MigrationUtils.createImportedBookmarksFolder(this.importedAppLabel, folderGuid);
       }
       yield this._migrateFolder(this._favoritesFolder, folderGuid);
-
-      if (this._migrationType == MSMigrationUtils.MIGRATION_TYPE_EDGE) {
-        yield this._migrateEdgeReadingList(PlacesUtils.bookmarks.menuGuid);
-      }
     }.bind(this)).then(() => aCallback(true),
                         e => { Cu.reportError(e); aCallback(false) });
   },
 
   _migrateFolder: Task.async(function* (aSourceFolder, aDestFolderGuid) {
     // TODO (bug 741993): the favorites order is stored in the Registry, at
     // HCU\Software\Microsoft\Windows\CurrentVersion\Explorer\MenuOrder\Favorites
     // for IE, and in a similar location for Edge.
     // Until we support it, bookmarks are imported in alphabetical order.
     let entries = aSourceFolder.directoryEntries;
+    let succeeded = true;
     while (entries.hasMoreElements()) {
       let entry = entries.getNext().QueryInterface(Ci.nsIFile);
       try {
         // Make sure that entry.path == entry.target to not follow .lnk folder
         // shortcuts which could lead to infinite cycles.
         // Don't use isSymlink(), since it would throw for invalid
         // lnk files pointing to URLs or to unresolvable paths.
         if (entry.path == entry.target && entry.isDirectory()) {
@@ -434,92 +430,24 @@ Bookmarks.prototype = {
 
             yield PlacesUtils.bookmarks.insert({
               parentGuid: aDestFolderGuid, url: uri, title
             });
           }
         }
       } catch (ex) {
         Components.utils.reportError("Unable to import " + this.importedAppLabel + " favorite (" + entry.leafName + "): " + ex);
+        succeeded = false;
       }
     }
+    if (!succeeded) {
+      throw new Error("Failed to import all bookmarks correctly.");
+    }
   }),
 
-  _migrateEdgeReadingList: Task.async(function*(parentGuid) {
-    let edgeDir = getEdgeLocalDataFolder();
-    if (!edgeDir) {
-      return;
-    }
-
-    this._readingListExtractor = Cc["@mozilla.org/profile/migrator/edgereadinglistextractor;1"].
-                                 createInstance(Ci.nsIEdgeReadingListExtractor);
-    edgeDir.appendRelativePath(EDGE_READINGLIST);
-    if (edgeDir.exists() && edgeDir.isReadable() && edgeDir.isDirectory()) {
-      let expectedDir = edgeDir.clone();
-      expectedDir.appendRelativePath("nouser1\\120712-0049");
-      if (expectedDir.exists() && expectedDir.isReadable() && expectedDir.isDirectory()) {
-        yield this._migrateEdgeReadingListDB(expectedDir, parentGuid);
-      } else {
-        let getSubdirs = someDir => {
-          let subdirs = someDir.directoryEntries;
-          let rv = [];
-          while (subdirs.hasMoreElements()) {
-            let subdir = subdirs.getNext().QueryInterface(Ci.nsIFile);
-            if (subdir.isDirectory() && subdir.isReadable()) {
-              rv.push(subdir);
-            }
-          }
-          return rv;
-        };
-        let dirs = getSubdirs(edgeDir).map(getSubdirs);
-        for (let dir of dirs) {
-          yield this._migrateEdgeReadingListDB(dir, parentGuid);
-        }
-      }
-    }
-  }),
-  _migrateEdgeReadingListDB: Task.async(function*(dbFile, parentGuid) {
-    dbFile.appendRelativePath("DBStore\\spartan.edb");
-    if (!dbFile.exists() || !dbFile.isReadable() || !dbFile.isFile()) {
-      return;
-    }
-    let readingListItems;
-    try {
-      readingListItems = this._readingListExtractor.extract(dbFile.path);
-    } catch (ex) {
-      Cu.reportError("Failed to extract Edge reading list information from " +
-                     "the database at " + dbPath + " due to the following error: " + ex);
-      return;
-    }
-    if (!readingListItems.length) {
-      return;
-    }
-    let destFolderGuid = yield this._ensureEdgeReadingListFolder(parentGuid);
-    for (let i = 0; i < readingListItems.length; i++) {
-      let readingListItem = readingListItems.queryElementAt(i, Ci.nsIPropertyBag2);
-      let url = readingListItem.get("uri");
-      let title = readingListItem.get("title");
-      let time = readingListItem.get("time");
-      // time is a PRTime, which is microseconds (since unix epoch), or null.
-      // We need milliseconds for the date constructor, so divide by 1000:
-      let dateAdded = time ? new Date(time / 1000) : new Date();
-      yield PlacesUtils.bookmarks.insert({
-        parentGuid: destFolderGuid, url: url, title, dateAdded
-      });
-    }
-  }),
-
-  _ensureEdgeReadingListFolder: Task.async(function*(parentGuid) {
-    if (!this.__edgeReadingListFolderGuid) {
-      let folderTitle = MigrationUtils.getLocalizedString("importedEdgeReadingList");
-      let folderSpec = {type: PlacesUtils.bookmarks.TYPE_FOLDER, parentGuid, title: folderTitle};
-      this.__edgeReadingListFolderGuid = (yield PlacesUtils.bookmarks.insert(folderSpec)).guid;
-    }
-    return this.__edgeReadingListFolderGuid;
-  }),
 };
 
 function Cookies(migrationType) {
   this._migrationType = migrationType;
 }
 
 Cookies.prototype = {
   type: MigrationUtils.resourceTypes.COOKIES,
@@ -943,9 +871,10 @@ var MSMigrationUtils = {
   },
   getCookiesMigrator(migrationType = this.MIGRATION_TYPE_IE) {
     return new Cookies(migrationType);
   },
   getWindowsVaultFormPasswordsMigrator() {
     return new WindowsVaultFormPasswords();
   },
   getTypedURLs,
+  getEdgeLocalDataFolder,
 };
--- a/browser/components/migration/nsEdgeReadingListExtractor.cpp
+++ b/browser/components/migration/nsEdgeReadingListExtractor.cpp
@@ -192,13 +192,19 @@ nsEdgeReadingListExtractor::ConvertJETEr
       return NS_ERROR_FILE_IS_LOCKED;
     case JET_errPermissionDenied:
     case JET_errAccessDenied:
       return NS_ERROR_FILE_ACCESS_DENIED;
     case JET_errInvalidFilename:
       return NS_ERROR_FILE_INVALID_PATH;
     case JET_errFileNotFound:
       return NS_ERROR_FILE_NOT_FOUND;
+    case JET_errDatabaseDirtyShutdown:
+      return NS_ERROR_FILE_CORRUPTED;
     default:
+      nsCOMPtr<nsIConsoleService> consoleService = do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+      wchar_t* msg = new wchar_t[80];
+      swprintf(msg, 80, MOZ_UTF16("Unexpected JET error from ESE database: %ld"), aError);
+      consoleService->LogStringMessage(msg);
       return NS_ERROR_FAILURE;
   }
 }
 
--- a/browser/components/places/tests/unit/distribution.ini
+++ b/browser/components/places/tests/unit/distribution.ini
@@ -3,19 +3,25 @@
 
 [Global]
 id=516444
 version=1.0
 about=Test distribution file
 
 [BookmarksToolbar]
 item.1.title=Toolbar Link Before
-item.1.link=http://mozilla.com/
+item.1.link=https://example.org/toolbar/before/
+item.1.keyword=e:t:b
+item.1.icon=https://example.org/favicon.png
+item.1.iconData=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==
 item.2.type=default
 item.3.title=Toolbar Link After
-item.3.link=http://mozilla.com/
+item.3.link=https://example.org/toolbar/after/
+item.3.keyword=e:t:a
 
 [BookmarksMenu]
 item.1.title=Menu Link Before
-item.1.link=http://mozilla.com/
+item.1.link=https://example.org/menu/before/
+item.1.icon=https://example.org/favicon.png
+item.1.iconData=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==
 item.2.type=default
 item.3.title=Menu Link After
-item.3.link=http://mozilla.com/
\ No newline at end of file
+item.3.link=https://example.org/menu/after/
--- a/browser/components/places/tests/unit/head_bookmarks.js
+++ b/browser/components/places/tests/unit/head_bookmarks.js
@@ -114,8 +114,39 @@ function rebuildSmartBookmarks() {
   });
   Cc["@mozilla.org/browser/browserglue;1"]
     .getService(Ci.nsIObserver)
     .observe(null, "browser-glue-test", "smart-bookmarks-init");
   return promiseTopicObserved("test-smart-bookmarks-done").then(() => {
     Services.console.unregisterListener(consoleListener);
   });
 }
+
+const SINGLE_TRY_TIMEOUT = 100;
+const NUMBER_OF_TRIES = 30;
+
+/**
+ * Similar to waitForConditionPromise, but poll for an asynchronous value
+ * every SINGLE_TRY_TIMEOUT ms, for no more than tryCount times.
+ *
+ * @param promiseFn
+ *        A function to generate a promise, which resolves to the expected
+ *        asynchronous value.
+ * @param timeoutMsg
+ *        The reason to reject the returned promise with.
+ * @param [optional] tryCount
+ *        Maximum times to try before rejecting the returned promise with
+ *        timeoutMsg, defaults to NUMBER_OF_TRIES.
+ * @return {Promise}
+ * @resolves to the asynchronous value being polled.
+ * @rejects if the asynchronous value is not available after tryCount attempts.
+ */
+var waitForResolvedPromise = Task.async(function* (promiseFn, timeoutMsg, tryCount=NUMBER_OF_TRIES) {
+  let tries = 0;
+  do {
+    try {
+      let value = yield promiseFn();
+      return value;
+    } catch (ex) {}
+    yield new Promise(resolve => do_timeout(SINGLE_TRY_TIMEOUT, resolve));
+  } while (++tries <= tryCount);
+  throw(timeoutMsg);
+});
--- a/browser/components/places/tests/unit/test_browserGlue_distribution.js
+++ b/browser/components/places/tests/unit/test_browserGlue_distribution.js
@@ -71,23 +71,51 @@ add_task(function* () {
   Assert.equal(menuItem.title, "Menu Link Before");
 
   menuItem = yield PlacesUtils.bookmarks.fetch({
     parentGuid: PlacesUtils.bookmarks.menuGuid,
     index: 1 + DEFAULT_BOOKMARKS_ON_MENU
   });
   Assert.equal(menuItem.title, "Menu Link After");
 
+  // Check no favicon or keyword exists for this bookmark
+  yield Assert.rejects(waitForResolvedPromise(() => {
+    return PlacesUtils.promiseFaviconData(menuItem.url.href);
+  }, "Favicon not found", 10), /Favicon\snot\sfound/, "Favicon not found");
+
+  let keywordItem = yield PlacesUtils.keywords.fetch({
+    url: menuItem.url.href
+  });
+  Assert.strictEqual(keywordItem, null);
+
   // Check the custom bookmarks exist on toolbar.
   let toolbarItem = yield PlacesUtils.bookmarks.fetch({
     parentGuid: PlacesUtils.bookmarks.toolbarGuid,
     index: 0
   });
   Assert.equal(toolbarItem.title, "Toolbar Link Before");
 
+  // Check the custom favicon and keyword exist for this bookmark
+  let faviconItem = yield waitForResolvedPromise(() => {
+    return PlacesUtils.promiseFaviconData(toolbarItem.url.href);
+  }, "Favicon not found", 10);
+  Assert.equal(faviconItem.uri.spec, "https://example.org/favicon.png");
+  Assert.greater(faviconItem.dataLen, 0);
+  Assert.equal(faviconItem.mimeType, "image/png");
+
+  let base64Icon = "data:image/png;base64," +
+      base64EncodeString(String.fromCharCode.apply(String, faviconItem.data));
+  Assert.equal(base64Icon, SMALLPNG_DATA_URI.spec);
+
+  keywordItem = yield PlacesUtils.keywords.fetch({
+    url: toolbarItem.url.href
+  });
+  Assert.notStrictEqual(keywordItem, null);
+  Assert.equal(keywordItem.keyword, "e:t:b");
+
   toolbarItem = yield PlacesUtils.bookmarks.fetch({
     parentGuid: PlacesUtils.bookmarks.toolbarGuid,
     index: 1 + DEFAULT_BOOKMARKS_ON_TOOLBAR
   });
   Assert.equal(toolbarItem.title, "Toolbar Link After");
 
   // Check the bmprocessed pref has been created.
   Assert.ok(Services.prefs.getBoolPref(PREF_BMPROCESSED));
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -1245,32 +1245,45 @@ var SessionStoreInternal = {
       // Store the window's close date to figure out when each individual tab
       // was closed. This timestamp should allow re-arranging data based on how
       // recently something was closed.
       winData.closedAt = Date.now();
 
       // we don't want to save the busy state
       delete winData.busy;
 
+      // When closing windows one after the other until Firefox quits, we
+      // will move those closed in series back to the "open windows" bucket
+      // before writing to disk. If however there is only a single window
+      // with tabs we deem not worth saving then we might end up with a
+      // random closed or even a pop-up window re-opened. To prevent that
+      // we explicitly allow saving an "empty" window state.
+      let isLastWindow =
+        Object.keys(this._windows).length == 1 &&
+        !this._closedWindows.some(win => win._shouldRestore || false);
+
+      // clear this window from the list, since it has definitely been closed.
+      delete this._windows[aWindow.__SSi];
+
       // Now we have to figure out if this window is worth saving in the _closedWindows
       // Object.
       //
       // We're about to flush the tabs from this window, but it's possible that we
       // might never hear back from the content process(es) in time before the user
       // chooses to restore the closed window. So we do the following:
       //
       // 1) Use the tab state cache to determine synchronously if the window is
       //    worth stashing in _closedWindows.
       // 2) Flush the window.
       // 3) When the flush is complete, revisit our decision to store the window
       //    in _closedWindows, and add/remove as necessary.
       if (!winData.isPrivate) {
         // Remove any open private tabs the window may contain.
         PrivacyFilter.filterPrivateTabs(winData);
-        this.maybeSaveClosedWindow(winData);
+        this.maybeSaveClosedWindow(winData, isLastWindow);
       }
 
       // The tabbrowser binding will go away once the window is closed,
       // so we'll hold a reference to the browsers in the closure here.
       let browsers = tabbrowser.browsers;
 
       TabStateFlusher.flushWindow(aWindow).then(() => {
         // At this point, aWindow is closed! You should probably not try to
@@ -1287,38 +1300,35 @@ var SessionStoreInternal = {
         }
 
         // Save non-private windows if they have at
         // least one saveable tab or are the last window.
         if (!winData.isPrivate) {
           // It's possible that a tab switched its privacy state at some point
           // before our flush, so we need to filter again.
           PrivacyFilter.filterPrivateTabs(winData);
-          this.maybeSaveClosedWindow(winData);
+          this.maybeSaveClosedWindow(winData, isLastWindow);
         }
 
-        // clear this window from the list
-        delete this._windows[aWindow.__SSi];
         // Update the tabs data now that we've got the most
         // recent information.
         this.cleanUpWindow(aWindow, winData);
 
         // save the state without this window to disk
         this.saveStateDelayed();
       });
     } else {
       this.cleanUpWindow(aWindow, winData);
     }
 
     for (let i = 0; i < tabbrowser.tabs.length; i++) {
       this.onTabRemove(aWindow, tabbrowser.tabs[i], true);
     }
   },
 
-
   /**
    * Clean up the message listeners on a window that has finally
    * gone away. Call this once you're sure you don't want to hear
    * from any of this windows tabs from here forward.
    *
    * @param aWindow
    *        The browser window we're cleaning up.
    * @param winData
@@ -1340,32 +1350,29 @@ var SessionStoreInternal = {
    * Decides whether or not a closed window should be put into the
    * _closedWindows Object. This might be called multiple times per
    * window, and will do the right thing of moving the window data
    * in or out of _closedWindows if the winData indicates that our
    * need for saving it has changed.
    *
    * @param winData
    *        The data for the closed window that we might save.
+   * @param isLastWindow
+   *        Whether or not the window being closed is the last
+   *        browser window. Callers of this function should pass
+   *        in the value of SessionStoreInternal.atLastWindow for
+   *        this argument, and pass in the same value if they happen
+   *        to call this method again asynchronously (for example, after
+   *        a window flush).
    */
-  maybeSaveClosedWindow(winData) {
+  maybeSaveClosedWindow(winData, isLastWindow) {
     if (RunState.isRunning) {
       // Determine whether the window has any tabs worth saving.
       let hasSaveableTabs = winData.tabs.some(this._shouldSaveTabState);
 
-      // When closing windows one after the other until Firefox quits, we
-      // will move those closed in series back to the "open windows" bucket
-      // before writing to disk. If however there is only a single window
-      // with tabs we deem not worth saving then we might end up with a
-      // random closed or even a pop-up window re-opened. To prevent that
-      // we explicitly allow saving an "empty" window state.
-      let isLastWindow =
-        Object.keys(this._windows).length == 1 &&
-        !this._closedWindows.some(win => win._shouldRestore || false);
-
       // Note that we might already have this window stored in
       // _closedWindows from a previous call to this function.
       let winIndex = this._closedWindows.indexOf(winData);
       let alreadyStored = (winIndex != -1);
       let shouldStore = (hasSaveableTabs || isLastWindow);
 
       if (shouldStore && !alreadyStored) {
         let index = this._closedWindows.findIndex(win => {
--- a/browser/components/sessionstore/test/browser.ini
+++ b/browser/components/sessionstore/test/browser.ini
@@ -212,8 +212,9 @@ skip-if = true
 # Disabled on OS X:
 [browser_625016.js]
 skip-if = os == "mac"
 
 [browser_911547.js]
 [browser_send_async_message_oom.js]
 [browser_multiple_navigateAndRestore.js]
 run-if = e10s
+[browser_async_window_flushing.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/sessionstore/test/browser_async_window_flushing.js
@@ -0,0 +1,38 @@
+/**
+ * Tests that when we close a window, it is immediately removed from the
+ * _windows array.
+ */
+add_task(function* test_synchronously_remove_window_state() {
+  // Depending on previous tests, we might already have some closed
+  // windows stored. We'll use its length to determine whether or not
+  // the window was added or not.
+  let state = JSON.parse(ss.getBrowserState());
+  ok(state, "Make sure we can get the state");
+  let initialWindows = state.windows.length;
+
+  // Open a new window and send the first tab somewhere
+  // interesting.
+  let newWin = yield BrowserTestUtils.openNewBrowserWindow();
+  let browser = newWin.gBrowser.selectedBrowser;
+  browser.loadURI("http://example.com");
+  yield BrowserTestUtils.browserLoaded(browser);
+  yield TabStateFlusher.flush(browser);
+
+  state = JSON.parse(ss.getBrowserState());
+  is(state.windows.length, initialWindows + 1,
+     "The new window to be in the state");
+
+  // Now close the window, and make sure that the window was removed
+  // from the windows list from the SessionState. We're specifically
+  // testing the case where the window is _not_ removed in between
+  // the close-initiated flush request and the flush response.
+  let windowClosed = BrowserTestUtils.windowClosed(newWin);
+  newWin.close();
+
+  state = JSON.parse(ss.getBrowserState());
+  is(state.windows.length, initialWindows,
+     "The new window should have been removed from the state");
+
+  // Wait for our window to go away
+  yield windowClosed;
+});
--- a/devtools/client/aboutdebugging/aboutdebugging.css
+++ b/devtools/client/aboutdebugging/aboutdebugging.css
@@ -7,17 +7,18 @@ html, body {
   width: 100%;
 }
 
 h2, h3, h4 {
   margin-bottom: 5px;
 }
 
 button {
-  width: 100px;
+  padding-left: 20px;
+  padding-right: 20px;
 }
 
 #body {
   display: flex;
   flex-direction: row;
 }
 
 /* Category tabs */
@@ -75,8 +76,17 @@ label {
 
 .inverted-icons .target-icon {
   filter: invert(30%);
 }
 
 .target-details {
   flex: 1;
 }
+
+.addon-controls {
+  display: flex;
+  flex-direction: row;
+}
+
+.addon-options {
+  flex: 1;
+}
--- a/devtools/client/aboutdebugging/aboutdebugging.js
+++ b/devtools/client/aboutdebugging/aboutdebugging.js
@@ -3,32 +3,41 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* eslint-env browser */
 /* global AddonsComponent, DebuggerClient, DebuggerServer, React,
    RuntimesComponent, WorkersComponent */
 
 "use strict";
 
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 const { loader } = Components.utils.import(
   "resource://devtools/shared/Loader.jsm", {});
 
 loader.lazyRequireGetter(this, "AddonsComponent",
   "devtools/client/aboutdebugging/components/addons", true);
 loader.lazyRequireGetter(this, "DebuggerClient",
   "devtools/shared/client/main", true);
 loader.lazyRequireGetter(this, "DebuggerServer",
   "devtools/server/main", true);
 loader.lazyRequireGetter(this, "Telemetry",
   "devtools/client/shared/telemetry");
 loader.lazyRequireGetter(this, "WorkersComponent",
   "devtools/client/aboutdebugging/components/workers", true);
 loader.lazyRequireGetter(this, "Services");
 
+loader.lazyImporter(this, "AddonManager",
+  "resource://gre/modules/AddonManager.jsm");
+
+const Strings = Services.strings.createBundle(
+  "chrome://devtools/locale/aboutdebugging.properties");
+
 var AboutDebugging = {
+  _prefListeners: [],
+
   _categories: null,
   get categories() {
     // If needed, initialize the list of available categories.
     if (!this._categories) {
       let elements = document.querySelectorAll(".category");
       this._categories = Array.map(elements, element => {
         let value = element.getAttribute("value");
         element.addEventListener("click", this.showTab.bind(this, value));
@@ -67,43 +76,79 @@ var AboutDebugging = {
 
     // Link checkboxes to prefs.
     let elements = document.querySelectorAll("input[type=checkbox][data-pref]");
     Array.map(elements, element => {
       let pref = element.dataset.pref;
       let updatePref = () => {
         Services.prefs.setBoolPref(pref, element.checked);
       };
+      element.addEventListener("change", updatePref, false);
       let updateCheckbox = () => {
         element.checked = Services.prefs.getBoolPref(pref);
       };
-      element.addEventListener("change", updatePref, false);
       Services.prefs.addObserver(pref, updateCheckbox, false);
+      this._prefListeners.push([pref, updateCheckbox]);
       updateCheckbox();
     });
 
+    // Link buttons to their associated actions.
+    let loadAddonButton = document.getElementById("load-addon-from-file");
+    loadAddonButton.addEventListener("click", this.loadAddonFromFile);
+
     if (!DebuggerServer.initialized) {
       DebuggerServer.init();
       DebuggerServer.addBrowserActors();
     }
     DebuggerServer.allowChromeProcess = true;
     let client = new DebuggerClient(DebuggerServer.connectPipe());
 
     client.connect(() => {
       React.render(React.createElement(AddonsComponent, { client }),
         document.querySelector("#addons"));
       React.render(React.createElement(WorkersComponent, { client }),
         document.querySelector("#workers"));
     });
   },
 
+  loadAddonFromFile() {
+    let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+    fp.init(window,
+      Strings.GetStringFromName("selectAddonFromFile"),
+      Ci.nsIFilePicker.modeOpen);
+    let res = fp.show();
+    if (res == Ci.nsIFilePicker.returnCancel || !fp.file) {
+      return;
+    }
+    let file = fp.file;
+    // AddonManager.installTemporaryAddon accepts either
+    // addon directory or final xpi file.
+    if (!file.isDirectory() && !file.leafName.endsWith(".xpi")) {
+      file = file.parent;
+    }
+    try {
+      AddonManager.installTemporaryAddon(file);
+    } catch(e) {
+      alert("Error while installing the addon:\n" + e.message + "\n");
+      throw e;
+    }
+  },
+
   destroy() {
     let telemetry = this._telemetry;
     telemetry.toolClosed("aboutdebugging");
     telemetry.destroy();
+
+    this._prefListeners.forEach(([pref, listener]) => {
+      Services.prefs.removeObserver(pref, listener);
+    });
+    this._prefListeners = [];
+
+    React.unmountComponentAtNode(document.querySelector("#addons"));
+    React.unmountComponentAtNode(document.querySelector("#workers"));
   },
 };
 
 window.addEventListener("DOMContentLoaded", function load() {
   window.removeEventListener("DOMContentLoaded", load);
   AboutDebugging.init();
 });
 
--- a/devtools/client/aboutdebugging/aboutdebugging.xhtml
+++ b/devtools/client/aboutdebugging/aboutdebugging.xhtml
@@ -29,18 +29,23 @@
         <div class="category-name">&aboutDebugging.workers;</div>
       </div>
     </div>
     <div class="main-content">
       <div id="tab-addons" class="tab active">
         <div class="header">
           <h1 class="header-name">&aboutDebugging.addons;</h1>
         </div>
-        <input id="enable-addon-debugging" type="checkbox" data-pref="devtools.chrome.enabled"/>
-        <label for="enable-addon-debugging" title="&aboutDebugging.addonDebugging.tooltip;">&aboutDebugging.addonDebugging.label;</label>
+        <div class="addon-controls">
+          <div class="addon-options">
+            <input id="enable-addon-debugging" type="checkbox" data-pref="devtools.chrome.enabled"/>
+            <label for="enable-addon-debugging" title="&aboutDebugging.addonDebugging.tooltip;">&aboutDebugging.addonDebugging.label;</label>
+          </div>
+          <button id="load-addon-from-file">&aboutDebugging.loadTemporaryAddon;</button>
+        </div>
         <div id="addons"></div>
       </div>
       <div id="tab-workers" class="tab">
         <div class="header">
           <h1 class="header-name">&aboutDebugging.workers;</h1>
         </div>
         <input id="enable-worker-debugging" type="checkbox" data-pref="devtools.debugger.workers"/>
         <label for="enable-worker-debugging" title="&options.enableWorkers.tooltip;">&options.enableWorkers.label;</label>
copy from devtools/client/locales/en-US/aboutdebugging.properties
copy to devtools/client/aboutdebugging/moz.build
--- a/devtools/client/locales/en-US/aboutdebugging.properties
+++ b/devtools/client/aboutdebugging/moz.build
@@ -1,12 +1,13 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
 # 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/.
 
-debug = Debug
+DIRS += [
+    'components',
+]
 
-extensions = Extensions
-serviceWorkers = Service Workers
-sharedWorkers = Shared Workers
-otherWorkers = Other Workers
-
-nothing = Nothing yet.
+BROWSER_CHROME_MANIFESTS += [
+    'test/browser.ini'
+]
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/unpacked/bootstrap.js
@@ -0,0 +1,7 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+function startup() {
+  Services.obs.notifyObservers(null, "test-devtools", null);
+}
+function shutdown() {}
+function install() {}
+function uninstall() {}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/addons/unpacked/install.rdf
@@ -0,0 +1,26 @@
+<?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/.
+-->
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+  <Description about="urn:mozilla:install-manifest"
+               em:id="test-devtools@mozilla.org"
+               em:name="test-devtools"
+               em:version="1.0"
+               em:type="2"
+               em:creator="Mozilla">
+
+    <em:bootstrap>true</em:bootstrap>
+    <em:targetApplication>
+      <Description>
+        <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+        <em:minVersion>44.0a1</em:minVersion>
+        <em:maxVersion>*</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+  </Description>
+</RDF>
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+tags = devtools
+subsuite = devtools
+support-files =
+  head.js
+  addons/unpacked/bootstrap.js
+  addons/unpacked/install.rdf
+
+[browser_addons_install.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/browser_addons_install.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+var {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+
+const ADDON_ID = "test-devtools@mozilla.org";
+const ADDON_NAME = "test-devtools";
+
+add_task(function *() {
+  let { tab, document } = yield openAboutDebugging("addons");
+
+  // Mock the file picker to select a test addon
+  let MockFilePicker = SpecialPowers.MockFilePicker;
+  MockFilePicker.init(null);
+  let file = get_supports_file("addons/unpacked/install.rdf");
+  MockFilePicker.returnFiles = [file.file];
+
+  // Wait for a message sent by the addon's bootstrap.js file
+  let promise = new Promise(done => {
+    Services.obs.addObserver(function listener() {
+      Services.obs.removeObserver(listener, "test-devtools", false);
+      ok(true, "Addon installed and running its bootstrap.js file");
+      done();
+    }, "test-devtools", false);
+  });
+  // Trigger the file picker by clicking on the button
+  document.getElementById("load-addon-from-file").click();
+
+  // Wait for the addon execution
+  yield promise;
+
+  // Check that the addon appears in the UI
+  let names = [...document.querySelectorAll("#addons .target-name")];
+  names = names.map(element => element.textContent);
+  ok(names.includes(ADDON_NAME), "The addon name appears in the list of addons: " + names);
+
+  // Now uninstall this addon
+  yield new Promise(done => {
+    AddonManager.getAddonByID(ADDON_ID, addon => {
+      let listener = {
+        onUninstalled: function(aUninstalledAddon) {
+          if (aUninstalledAddon != addon) {
+            return;
+          }
+          AddonManager.removeAddonListener(listener);
+          done();
+        }
+      };
+      AddonManager.addAddonListener(listener);
+      addon.uninstall();
+    });
+  });
+
+  // Ensure that the UI removes the addon from the list
+  names = [...document.querySelectorAll("#addons .target-name")];
+  names = names.map(element => element.textContent);
+  ok(!names.includes(ADDON_NAME), "After uninstall, the addon name disappears from the list of addons: " + names);
+
+  yield closeAboutDebugging(tab);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging/test/head.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {utils: Cu, classes: Cc, interfaces: Ci} = Components;
+
+const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+const Services = require("Services");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+DevToolsUtils.testing = true;
+
+const CHROME_ROOT = gTestPath.substr(0, gTestPath.lastIndexOf("/") + 1);
+
+registerCleanupFunction(() => {
+  DevToolsUtils.testing = false;
+});
+
+function openAboutDebugging() {
+  info("opening about:debugging");
+  return addTab("about:debugging").then(tab => {
+    let browser = tab.linkedBrowser;
+    return {
+      tab,
+      document: browser.contentDocument,
+      window: browser.contentWindow
+    };
+  });
+}
+
+function closeAboutDebugging(tab) {
+  info("Closing about:debugging");
+  return removeTab(tab);
+}
+
+function addTab(aUrl, aWindow) {
+  info("Adding tab: " + aUrl);
+
+  return new Promise(done => {
+    let targetWindow = aWindow || window;
+    let targetBrowser = targetWindow.gBrowser;
+
+    targetWindow.focus();
+    let tab = targetBrowser.selectedTab = targetBrowser.addTab(aUrl);
+    let linkedBrowser = tab.linkedBrowser;
+
+    linkedBrowser.addEventListener("load", function onLoad() {
+      linkedBrowser.removeEventListener("load", onLoad, true);
+      info("Tab added and finished loading: " + aUrl);
+      done(tab);
+    }, true);
+  });
+}
+
+function removeTab(aTab, aWindow) {
+  info("Removing tab.");
+
+  return new Promise(done => {
+    let targetWindow = aWindow || window;
+    let targetBrowser = targetWindow.gBrowser;
+    let tabContainer = targetBrowser.tabContainer;
+
+    tabContainer.addEventListener("TabClose", function onClose(aEvent) {
+      tabContainer.removeEventListener("TabClose", onClose, false);
+      info("Tab removed and finished closing.");
+      done();
+    }, false);
+
+    targetBrowser.removeTab(aTab);
+  });
+}
+
+function get_supports_file(path) {
+  let cr = Cc["@mozilla.org/chrome/chrome-registry;1"].
+  getService(Ci.nsIChromeRegistry);
+  let fileurl = cr.convertChromeURL(Services.io.newURI(CHROME_ROOT + path, null, null));
+  return fileurl.QueryInterface(Ci.nsIFileURL);
+}
--- a/devtools/client/locales/en-US/aboutdebugging.dtd
+++ b/devtools/client/locales/en-US/aboutdebugging.dtd
@@ -1,9 +1,10 @@
 <!-- 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 aboutDebugging.title                      "about:debugging">
 <!ENTITY aboutDebugging.addons                     "Add-ons">
 <!ENTITY aboutDebugging.addonDebugging.label       "Enable add-on debugging">
 <!ENTITY aboutDebugging.addonDebugging.tooltip     "Turning this on will allow you to debug add-ons and various other parts of the browser chrome">
+<!ENTITY aboutDebugging.loadTemporaryAddon         "Load Temporary Add-on">
 <!ENTITY aboutDebugging.workers                    "Workers">
--- a/devtools/client/locales/en-US/aboutdebugging.properties
+++ b/devtools/client/locales/en-US/aboutdebugging.properties
@@ -1,12 +1,13 @@
 # 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/.
 
 debug = Debug
 
 extensions = Extensions
+selectAddonFromFile = Select Add-on Directory or XPI File
 serviceWorkers = Service Workers
 sharedWorkers = Shared Workers
 otherWorkers = Other Workers
 
 nothing = Nothing yet.
--- a/devtools/client/moz.build
+++ b/devtools/client/moz.build
@@ -2,17 +2,17 @@
 # vim: set filetype=python:
 # 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('../templates.mozbuild')
 
 DIRS += [
-    'aboutdebugging/components',
+    'aboutdebugging',
     'animationinspector',
     'canvasdebugger',
     'commandline',
     'debugger',
     'eyedropper',
     'fontinspector',
     'framework',
     'inspector',
--- a/devtools/server/actors/webconsole.js
+++ b/devtools/server/actors/webconsole.js
@@ -153,18 +153,22 @@ WebConsoleActor.prototype =
    * @type boolean
    */
   get _parentIsContentActor() {
     return "ContentActor" in DebuggerServer &&
             this.parentActor instanceof DebuggerServer.ContentActor;
   },
 
   /**
-   * The window we work with.
-   * @type nsIDOMWindow
+   * The window or sandbox we work with.
+   * Note that even if it is named `window` it refers to the current
+   * global we are debugging, which can be a Sandbox for addons
+   * or browser content toolbox.
+   *
+   * @type nsIDOMWindow or Sandbox
    */
   get window() {
     if (this.parentActor.isRootActor) {
       return this._getWindowForBrowserConsole();
     }
     return this.parentActor.window;
   },
 
@@ -731,17 +735,18 @@ WebConsoleActor.prototype =
     while (types.length > 0) {
       let type = types.shift();
       switch (type) {
         case "ConsoleAPI": {
           if (!this.consoleAPIListener) {
             break;
           }
 
-          let requestStartTime = this.window ?
+          // See `window` definition. It isn't always a DOM Window.
+          let requestStartTime = this.window && this.window.performance ?
             this.window.performance.timing.requestStart : 0;
 
           let cache = this.consoleAPIListener
                       .getCachedMessages(!this.parentActor.isRootActor);
           cache.forEach((aMessage) => {
             // Filter out messages that came from a ServiceWorker but happened
             // before the page was requested.
             if (aMessage.innerID === "ServiceWorker" &&
--- a/devtools/shared/heapsnapshot/HeapSnapshot.cpp
+++ b/devtools/shared/heapsnapshot/HeapSnapshot.cpp
@@ -52,30 +52,17 @@ using ::google::protobuf::io::ArrayInput
 using ::google::protobuf::io::CodedInputStream;
 using ::google::protobuf::io::GzipInputStream;
 using ::google::protobuf::io::ZeroCopyInputStream;
 
 using JS::ubi::AtomOrTwoByteChars;
 
 /*** Cycle Collection Boilerplate *****************************************************************/
 
-NS_IMPL_CYCLE_COLLECTION_CLASS(HeapSnapshot)
-
-NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(HeapSnapshot)
-  NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
-NS_IMPL_CYCLE_COLLECTION_UNLINK_END
-
-NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(HeapSnapshot)
-  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
-  NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS
-NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
-
-NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(HeapSnapshot)
-  NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
-NS_IMPL_CYCLE_COLLECTION_TRACE_END
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(HeapSnapshot, mParent)
 
 NS_IMPL_CYCLE_COLLECTING_ADDREF(HeapSnapshot)
 NS_IMPL_CYCLE_COLLECTING_RELEASE(HeapSnapshot)
 
 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(HeapSnapshot)
   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
   NS_INTERFACE_MAP_ENTRY(nsISupports)
 NS_INTERFACE_MAP_END
--- a/dom/media/MediaManager.cpp
+++ b/dom/media/MediaManager.cpp
@@ -93,16 +93,33 @@ struct nsIMediaDevice::COMTypeInfo<mozil
 };
 const nsIID nsIMediaDevice::COMTypeInfo<mozilla::VideoDevice, void>::kIID = NS_IMEDIADEVICE_IID;
 template<>
 struct nsIMediaDevice::COMTypeInfo<mozilla::AudioDevice, void> {
   static const nsIID kIID;
 };
 const nsIID nsIMediaDevice::COMTypeInfo<mozilla::AudioDevice, void>::kIID = NS_IMEDIADEVICE_IID;
 
+namespace {
+already_AddRefed<nsIAsyncShutdownClient> GetShutdownPhase() {
+  nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdown();
+  MOZ_RELEASE_ASSERT(svc);
+
+  nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase;
+  nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(shutdownPhase));
+  if (!shutdownPhase) {
+    // We are probably in a content process.
+    rv = svc->GetContentChildShutdown(getter_AddRefs(shutdownPhase));
+  }
+  MOZ_RELEASE_ASSERT(shutdownPhase);
+  MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+  return shutdownPhase.forget();
+}
+}
+
 namespace mozilla {
 
 #ifdef LOG
 #undef LOG
 #endif
 
 LogModule*
 GetMediaManagerLog()
@@ -1555,44 +1572,38 @@ MediaManager::Get() {
       prefs->AddObserver("media.navigator.video.default_width", sSingleton, false);
       prefs->AddObserver("media.navigator.video.default_height", sSingleton, false);
       prefs->AddObserver("media.navigator.video.default_fps", sSingleton, false);
       prefs->AddObserver("media.navigator.video.default_minfps", sSingleton, false);
     }
 
     // Prepare async shutdown
 
-    nsCOMPtr<nsIAsyncShutdownClient> profileBeforeChange;
-    {
-      nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdown();
-      MOZ_RELEASE_ASSERT(svc);
-      nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(profileBeforeChange));
-      MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
-    }
+    nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetShutdownPhase();
 
     class Blocker : public media::ShutdownBlocker
     {
     public:
       Blocker()
       : media::ShutdownBlocker(NS_LITERAL_STRING(
           "Media shutdown: blocking on media thread")) {}
 
-      NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient* aProfileBeforeChange) override
+      NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient*) override
       {
         MOZ_RELEASE_ASSERT(MediaManager::GetIfExists());
         MediaManager::GetIfExists()->Shutdown();
         return NS_OK;
       }
     };
 
     sSingleton->mShutdownBlocker = new Blocker();
-    nsresult rv = profileBeforeChange->AddBlocker(sSingleton->mShutdownBlocker,
-                                                  NS_LITERAL_STRING(__FILE__),
-                                                  __LINE__,
-                                                  NS_LITERAL_STRING("Media shutdown"));
+    nsresult rv = shutdownPhase->AddBlocker(sSingleton->mShutdownBlocker,
+                                            NS_LITERAL_STRING(__FILE__),
+                                            __LINE__,
+                                            NS_LITERAL_STRING("Media shutdown"));
     MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
 #ifdef MOZ_B2G
     // Init MediaPermissionManager before sending out any permission requests.
     (void) MediaPermissionManager::GetInstance();
 #endif //MOZ_B2G
   }
   return sSingleton;
 }
@@ -2649,24 +2660,18 @@ MediaManager::Shutdown()
       media::NewRunnableFrom([this, that]() mutable {
     LOG(("MediaManager shutdown lambda running, releasing MediaManager singleton and thread"));
     if (mMediaThread) {
       mMediaThread->Stop();
     }
 
     // Remove async shutdown blocker
 
-    nsCOMPtr<nsIAsyncShutdownClient> profileBeforeChange;
-    {
-      nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdown();
-      MOZ_RELEASE_ASSERT(svc);
-      nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(profileBeforeChange));
-      MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
-    }
-    profileBeforeChange->RemoveBlocker(sSingleton->mShutdownBlocker);
+    nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetShutdownPhase();
+    shutdownPhase->RemoveBlocker(sSingleton->mShutdownBlocker);
 
     // we hold a ref to 'that' which is the same as sSingleton
     sSingleton = nullptr;
 
     return NS_OK;
   })));
 }
 
--- a/editor/composer/nsComposeTxtSrvFilter.cpp
+++ b/editor/composer/nsComposeTxtSrvFilter.cpp
@@ -40,16 +40,17 @@ nsComposeTxtSrvFilter::Skip(nsIDOMNode* 
         if (!*_retval) {
           *_retval = content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
                                           nsGkAtoms::mozsignature, eCaseMatters);
         }
       }
     } else if (content->IsAnyOfHTMLElements(nsGkAtoms::script,
                                             nsGkAtoms::textarea,
                                             nsGkAtoms::select,
+                                            nsGkAtoms::style,
                                             nsGkAtoms::map)) {
       *_retval = true;
     } else if (content->IsHTMLElement(nsGkAtoms::table)) {
       if (mIsForMail) {
         *_retval =
           content->AttrValueIs(kNameSpaceID_None, nsGkAtoms::_class,
                                NS_LITERAL_STRING("moz-email-headers-table"),
                                eCaseMatters);
--- a/editor/composer/test/chrome.ini
+++ b/editor/composer/test/chrome.ini
@@ -6,8 +6,9 @@ skip-if = buildapp == 'b2g' || os == 'an
 [test_bug434998.xul]
 [test_bug678842.html]
 [test_bug697981.html]
 [test_bug717433.html]
 [test_bug1204147.html]
 [test_bug1200533.html]
 [test_bug1205983.html]
 [test_bug1209414.html]
+[test_bug1219928.html]
new file mode 100644
--- /dev/null
+++ b/editor/composer/test/test_bug1219928.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1219928
+-->
+<head>
+  <title>Test for Bug 1219928</title>
+  <script type="text/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1219928">Mozilla Bug 1219928</a>
+<p id="display"></p>
+
+<div contenteditable id="en-US" lang="en-US">
+<p>And here a missspelled word</p>
+<style>
+<!-- and here another onnee in a style comment -->
+</style>
+</div>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1219928 **/
+/* Very simple test to check that <style> blocks are skipped in the spell check */
+
+var spellchecker;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.waitForFocus(function() {
+  Components.utils.import("resource://gre/modules/AsyncSpellCheckTestHelper.jsm");
+
+  var elem = document.getElementById('en-US');
+  elem.focus();
+
+  onSpellCheck(elem, function () {
+    var Ci = Components.interfaces;
+    var editingSession = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                               .getInterface(Ci.nsIWebNavigation)
+                               .QueryInterface(Ci.nsIInterfaceRequestor)
+                               .getInterface(Ci.nsIEditingSession);
+    var editor = editingSession.getEditorForWindow(window);
+    var selcon = editor.selectionController;
+    var sel = selcon.getSelection(selcon.SELECTION_SPELLCHECK);
+
+    is(sel.toString(), "missspelled", "one misspelled word expected: missspelled");
+
+    spellchecker = Components.classes['@mozilla.org/editor/editorspellchecker;1'].createInstance(Components.interfaces.nsIEditorSpellCheck);
+    var filterContractId = "@mozilla.org/editor/txtsrvfilter;1";
+    spellchecker.setFilter(Components.classes[filterContractId].createInstance(Components.interfaces.nsITextServicesFilter));
+    spellchecker.InitSpellChecker(editor, false, spellCheckStarted);
+  });
+});
+
+function spellCheckStarted() {
+  var misspelledWord = spellchecker.GetNextMisspelledWord();
+  is(misspelledWord, "missspelled", "first misspelled word expected: missspelled");
+
+  // Without the fix, the next misspelled word was 'onnee', so we check that we don't get it.
+  misspelledWord = spellchecker.GetNextMisspelledWord();
+  isnot(misspelledWord, "onnee", "second misspelled word should not be: onnee");
+
+  spellchecker = "";
+
+  SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -78,16 +78,29 @@ using namespace mozilla::layout;
 static mozilla::LazyLogModule sRefreshDriverLog("nsRefreshDriver");
 #define LOG(...) MOZ_LOG(sRefreshDriverLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
 
 #define DEFAULT_THROTTLED_FRAME_RATE 1
 #define DEFAULT_RECOMPUTE_VISIBILITY_INTERVAL_MS 1000
 // after 10 minutes, stop firing off inactive timers
 #define DEFAULT_INACTIVE_TIMER_DISABLE_SECONDS 600
 
+namespace {
+  // `true` if we are currently in jank-critical mode.
+  //
+  // In jank-critical mode, any iteration of the event loop that takes
+  // more than 16ms to compute will cause an ongoing animation to miss
+  // frames.
+  //
+  // For simplicity, the current implementation assumes that we are in
+  // jank-critical mode if and only if at least one vsync driver has
+  // at least one observer.
+  static uint64_t sActiveVsyncTimers = 0;
+}
+
 namespace mozilla {
 
 /*
  * The base class for all global refresh driver timers.  It takes care
  * of managing the list of refresh drivers attached to them and
  * provides interfaces for querying/setting the rate and actually
  * running a timer 'Tick'.  Subclasses must implement StartTimer(),
  * StopTimer(), and ScheduleNextTick() -- the first two just
@@ -498,34 +511,45 @@ private:
     // Detach current vsync timer from this VsyncObserver. The observer will no
     // longer tick this timer.
     mVsyncObserver->Shutdown();
     mVsyncObserver = nullptr;
   }
 
   virtual void StartTimer() override
   {
+    // Protect updates to `sActiveVsyncTimers`.
+    MOZ_ASSERT(NS_IsMainThread());
+
     mLastFireEpoch = JS_Now();
     mLastFireTime = TimeStamp::Now();
 
     if (XRE_IsParentProcess()) {
       mVsyncDispatcher->SetParentRefreshTimer(mVsyncObserver);
     } else {
       Unused << mVsyncChild->SendObserve();
       mVsyncObserver->OnTimerStart();
     }
+
+    ++sActiveVsyncTimers;
   }
 
   virtual void StopTimer() override
   {
+    // Protect updates to `sActiveVsyncTimers`.
+    MOZ_ASSERT(NS_IsMainThread());
+
     if (XRE_IsParentProcess()) {
       mVsyncDispatcher->SetParentRefreshTimer(nullptr);
     } else {
       Unused << mVsyncChild->SendUnobserve();
     }
+
+    MOZ_ASSERT(sActiveVsyncTimers > 0);
+    --sActiveVsyncTimers;
   }
 
   virtual void ScheduleNextTick(TimeStamp aNowTime) override
   {
     // Do nothing since we just wait for the next vsync from
     // RefreshDriverVsyncObserver.
   }
 
@@ -2123,9 +2147,16 @@ nsRefreshDriver::CancelPendingEvents(nsI
 {
   for (auto i : Reversed(MakeRange(mPendingEvents.Length()))) {
     if (mPendingEvents[i].mTarget->OwnerDoc() == aDocument) {
       mPendingEvents.RemoveElementAt(i);
     }
   }
 }
 
+/* static */ bool
+nsRefreshDriver::IsJankCritical()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  return sActiveVsyncTimers > 0;
+}
+
 #undef LOG
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -425,11 +425,22 @@ private:
   void BeginRefreshingImages(RequestTable& aEntries,
                              mozilla::TimeStamp aDesired);
 
   friend class mozilla::RefreshDriverTimer;
 
   // turn on or turn off high precision based on various factors
   void ConfigureHighPrecision();
   void SetHighPrecisionTimersEnabled(bool aEnable);
+
+  // `true` if we are currently in jank-critical mode.
+  //
+  // In jank-critical mode, any iteration of the event loop that takes
+  // more than 16ms to compute will cause an ongoing animation to miss
+  // frames.
+  //
+  // For simplicity, the current implementation assumes that we are
+  // in jank-critical mode if and only if the vsync driver has at least
+  // one observer.
+  static bool IsJankCritical();
 };
 
 #endif /* !defined(nsRefreshDriver_h_) */
--- a/layout/generic/nsContainerFrame.cpp
+++ b/layout/generic/nsContainerFrame.cpp
@@ -589,18 +589,18 @@ GetPresContextContainerWidget(nsPresCont
 }
 
 static bool
 IsTopLevelWidget(nsIWidget* aWidget)
 {
   nsWindowType windowType = aWidget->WindowType();
   return windowType == eWindowType_toplevel ||
          windowType == eWindowType_dialog ||
+         windowType == eWindowType_popup ||
          windowType == eWindowType_sheet;
-  // popups aren't toplevel so they're not handled here
 }
 
 void
 nsContainerFrame::SyncWindowProperties(nsPresContext*       aPresContext,
                                        nsIFrame*            aFrame,
                                        nsView*              aView,
                                        nsRenderingContext*  aRC,
                                        uint32_t             aFlags)
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -554,16 +554,17 @@ pref("ui.dragThresholdX", 25);
 pref("ui.dragThresholdY", 25);
 
 pref("layers.acceleration.disabled", false);
 pref("layers.offmainthreadcomposition.enabled", true);
 pref("layers.async-video.enabled", true);
 #ifdef MOZ_ANDROID_APZ
 pref("layers.async-pan-zoom.enabled", true);
 pref("apz.axis_lock.mode", 1);
+pref("apz.fling_stop_on_tap_threshold", "0.08");
 #endif
 pref("apz.allow_zooming", true);
 pref("layers.progressive-paint", true);
 pref("layers.low-precision-buffer", true);
 pref("layers.low-precision-resolution", "0.25");
 pref("layers.low-precision-opacity", "1.0");
 // We want to limit layers for two reasons:
 // 1) We can't scroll smoothly if we have to many draw calls
--- a/mobile/android/b2gdroid/installer/package-manifest.in
+++ b/mobile/android/b2gdroid/installer/package-manifest.in
@@ -159,16 +159,17 @@
 @BINPATH@/components/dom_security.xpt
 @BINPATH@/components/dom_settings.xpt
 @BINPATH@/components/dom_permissionsettings.xpt
 @BINPATH@/components/dom_sidebar.xpt
 @BINPATH@/components/dom_mobilemessage.xpt
 @BINPATH@/components/dom_storage.xpt
 @BINPATH@/components/dom_stylesheets.xpt
 @BINPATH@/components/dom_system.xpt
+@BINPATH@/components/dom_workers.xpt
 @BINPATH@/components/dom_threads.xpt
 @BINPATH@/components/dom_traversal.xpt
 @BINPATH@/components/dom_tv.xpt
 @BINPATH@/components/dom_views.xpt
 #ifdef MOZ_WEBSPEECH
 @BINPATH@/components/dom_webspeechrecognition.xpt
 #endif
 @BINPATH@/components/dom_xbl.xpt
--- a/python/mozboot/mozboot/android.py
+++ b/python/mozboot/mozboot/android.py
@@ -4,17 +4,17 @@
 
 # If we add unicode_literals, Python 2.6.1 (required for OS X 10.6) breaks.
 from __future__ import print_function
 
 import errno
 import os
 import stat
 import subprocess
-
+import sys
 
 # These are the platform and build-tools versions for building
 # mobile/android, respectively. Try to keep these in synch with the
 # build system and Mozilla's automation.
 ANDROID_TARGET_SDK = '23'
 ANDROID_BUILD_TOOLS_VERSION = '23.0.1'
 
 # These are the "Android packages" needed for building Firefox for Android.
@@ -204,17 +204,16 @@ def ensure_android_packages(android_tool
     '''
     Use the given android tool (like 'android') to install required Android
     packages.
     '''
 
     if not packages:
         packages = ANDROID_PACKAGES
 
-
     # Bug 1171232: The |android| tool behaviour has changed; we no longer can
     # see what packages are installed easily.  Force installing everything until
     # we find a way to actually see the missing packages.
     missing = packages
     if not missing:
         print(NOT_INSTALLING_ANDROID_PACKAGES % ', '.join(packages))
         return
 
@@ -231,16 +230,19 @@ def ensure_android_packages(android_tool
     failing = []
     if failing:
         raise Exception(MISSING_ANDROID_PACKAGES % (', '.join(missing), ', '.join(failing)))
 
 
 def suggest_mozconfig(sdk_path=None, ndk_path=None):
     print(MOBILE_ANDROID_MOZCONFIG_TEMPLATE % (sdk_path, ndk_path))
 
-def android_ndk_url(os_name, ver='r10e') :
-    from sys import maxsize
-    base_url = 'https://dl.google.com/android/ndk/android-ndk-'
+
+def android_ndk_url(os_name, ver='r10e'):
+    # Produce a URL like 'https://dl.google.com/android/ndk/android-ndk-r10e-linux-x86_64.bin'.
+    base_url = 'https://dl.google.com/android/ndk/android-ndk'
 
-    if maxsize > 2**32 :
-	return (base_url+ver+'-'+os_name+'-x86_64.bin')
-    else :
-	return (base_url+ver+'-'+os_name+'-x86.bin')
\ No newline at end of file
+    if sys.maxsize > 2**32:
+        arch = 'x86_64'
+    else:
+        arch = 'x86'
+
+    return '%s-%s-%s-%s.bin' % (base_url, ver, os_name, arch)
--- a/python/mozboot/mozboot/archlinux.py
+++ b/python/mozboot/mozboot/archlinux.py
@@ -108,17 +108,17 @@ class ArchlinuxBootstrapper(BaseBootstra
 
         # 2. The user may have an external Android SDK (in which case we save
         # them a lengthy download), or they may have already completed the
         # download. We unpack to ~/.mozbuild/{android-sdk-linux, android-ndk-r10e}.
         mozbuild_path = os.environ.get('MOZBUILD_STATE_PATH', os.path.expanduser(os.path.join('~', '.mozbuild')))
         self.sdk_path = os.environ.get('ANDROID_SDK_HOME', os.path.join(mozbuild_path, 'android-sdk-linux'))
         self.ndk_path = os.environ.get('ANDROID_NDK_HOME', os.path.join(mozbuild_path, 'android-ndk-r10e'))
         self.sdk_url = 'https://dl.google.com/android/android-sdk_r24.0.1-linux.tgz'
-	self.ndk_url = android_ndk_url('linux')
+        self.ndk_url = android.android_ndk_url('linux')
 
         android.ensure_android_sdk_and_ndk(path=mozbuild_path,
                                            sdk_path=self.sdk_path, sdk_url=self.sdk_url,
                                            ndk_path=self.ndk_path, ndk_url=self.ndk_url)
         android_tool = os.path.join(self.sdk_path, 'tools', 'android')
         android.ensure_android_packages(android_tool=android_tool)
 
     def suggest_mobile_android_mozconfig(self):
--- a/python/mozboot/mozboot/debian.py
+++ b/python/mozboot/mozboot/debian.py
@@ -123,17 +123,17 @@ class DebianBootstrapper(BaseBootstrappe
 
         # 2. The user may have an external Android SDK (in which case we save
         # them a lengthy download), or they may have already completed the
         # download. We unpack to ~/.mozbuild/{android-sdk-linux, android-ndk-r10e}.
         mozbuild_path = os.environ.get('MOZBUILD_STATE_PATH', os.path.expanduser(os.path.join('~', '.mozbuild')))
         self.sdk_path = os.environ.get('ANDROID_SDK_HOME', os.path.join(mozbuild_path, 'android-sdk-linux'))
         self.ndk_path = os.environ.get('ANDROID_NDK_HOME', os.path.join(mozbuild_path, 'android-ndk-r10e'))
         self.sdk_url = 'https://dl.google.com/android/android-sdk_r24.0.1-linux.tgz'
-        self.ndk_url = android_ndk_url('linux')
+        self.ndk_url = android.android_ndk_url('linux')
 
         android.ensure_android_sdk_and_ndk(path=mozbuild_path,
                                            sdk_path=self.sdk_path, sdk_url=self.sdk_url,
                                            ndk_path=self.ndk_path, ndk_url=self.ndk_url)
 
         # 3. We expect the |android| tool to at
         # ~/.mozbuild/android-sdk-linux/tools/android.
         android_tool = os.path.join(self.sdk_path, 'tools', 'android')
--- a/python/mozboot/mozboot/osx.py
+++ b/python/mozboot/mozboot/osx.py
@@ -350,30 +350,30 @@ class OSXBootstrapper(BaseBootstrapper):
         # them a lengthy download), or they may have already completed the
         # download. We unpack to ~/.mozbuild/{android-sdk-linux, android-ndk-r10e}.
         mozbuild_path = os.environ.get('MOZBUILD_STATE_PATH', os.path.expanduser(os.path.join('~', '.mozbuild')))
         self.sdk_path = os.environ.get('ANDROID_SDK_HOME', os.path.join(mozbuild_path, 'android-sdk-macosx'))
         self.ndk_path = os.environ.get('ANDROID_NDK_HOME', os.path.join(mozbuild_path, 'android-ndk-r10e'))
         self.sdk_url = 'https://dl.google.com/android/android-sdk_r24.0.1-macosx.zip'
         is_64bits = sys.maxsize > 2**32
         if is_64bits:
-            self.ndk_url = android_ndk_url('darwin')
+            self.ndk_url = android.android_ndk_url('darwin')
         else:
             raise Exception('You need a 64-bit version of Mac OS X to build Firefox for Android.')
 
         android.ensure_android_sdk_and_ndk(path=mozbuild_path,
                                            sdk_path=self.sdk_path, sdk_url=self.sdk_url,
                                            ndk_path=self.ndk_path, ndk_url=self.ndk_url)
 
         # 3. We expect the |android| tool to at
         # ~/.mozbuild/android-sdk-macosx/tools/android.
         android_tool = os.path.join(self.sdk_path, 'tools', 'android')
         android.ensure_android_packages(android_tool=android_tool)
 
-    def suggest_mobile_android_mozconfig(self):
+    def suggest_homebrew_mobile_android_mozconfig(self):
         import android
         android.suggest_mozconfig(sdk_path=self.sdk_path,
                                   ndk_path=self.ndk_path)
 
     def _ensure_macports_packages(self, packages):
         self.port = self.which('port')
         assert self.port is not None
 
--- a/services/fxaccounts/FxAccountsWebChannel.jsm
+++ b/services/fxaccounts/FxAccountsWebChannel.jsm
@@ -17,16 +17,18 @@ Cu.import("resource://gre/modules/XPCOMU
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "WebChannel",
                                   "resource://gre/modules/WebChannel.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
                                   "resource://gre/modules/FxAccounts.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Weave",
+                                  "resource://services-sync/main.js");
 
 const COMMAND_PROFILE_CHANGE       = "profile:change";
 const COMMAND_CAN_LINK_ACCOUNT     = "fxaccounts:can_link_account";
 const COMMAND_LOGIN                = "fxaccounts:login";
 const COMMAND_LOGOUT               = "fxaccounts:logout";
 const COMMAND_DELETE               = "fxaccounts:delete";
 
 const PREF_LAST_FXA_USER           = "identity.fxaccounts.lastSignedInUserHash";
@@ -201,31 +203,44 @@ this.FxAccountsWebChannelHelpers.prototy
    *
    * Save a bit into prefs that is read on verification to see whether
    * to show the list of data types that can be saved.
    */
   setShowCustomizeSyncPref(showCustomizeSyncPref) {
     Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, showCustomizeSyncPref);
   },
 
-  getShowCustomizeSyncPref(showCustomizeSyncPref) {
+  getShowCustomizeSyncPref() {
     return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION);
   },
 
   /**
    * stores sync login info it in the fxaccounts service
    *
    * @param accountData the user's account data and credentials
    */
   login(accountData) {
     if (accountData.customizeSync) {
       this.setShowCustomizeSyncPref(true);
       delete accountData.customizeSync;
     }
 
+    if (accountData.declinedSyncEngines) {
+      let declinedSyncEngines = accountData.declinedSyncEngines;
+      log.debug("Received declined engines", declinedSyncEngines);
+      Weave.Service.engineManager.setDeclined(declinedSyncEngines);
+      declinedSyncEngines.forEach(engine => {
+        Services.prefs.setBoolPref("services.sync.engine." + engine, false);
+      });
+
+      // if we got declinedSyncEngines that means we do not need to show the customize screen.
+      this.setShowCustomizeSyncPref(false);
+      delete accountData.declinedSyncEngines;
+    }
+
     // the user has already been shown the "can link account"
     // screen. No need to keep this data around.
     delete accountData.verifiedCanLinkAccount;
 
     // Remember who it was so we can log out next time.
     this.setPreviousAccountNameHashPref(accountData.email);
 
     // A sync-specific hack - we want to ensure sync has been initialized
@@ -234,17 +249,17 @@ this.FxAccountsWebChannelHelpers.prototy
               .getService(Ci.nsISupports)
               .wrappedJSObject;
     return xps.whenLoaded().then(() => {
       return this._fxAccounts.setSignedInUser(accountData);
     });
   },
 
   /**
-   * logoust the fxaccounts service
+   * logout the fxaccounts service
    *
    * @param the uid of the account which have been logged out
    */
   logout(uid) {
     return fxAccounts.getSignedInUser().then(userData => {
       if (userData.uid === uid) {
         return fxAccounts.signOut();
       }
--- a/services/fxaccounts/tests/xpcshell/test_web_channel.js
+++ b/services/fxaccounts/tests/xpcshell/test_web_channel.js
@@ -235,16 +235,58 @@ add_test(function test_helpers_login_wit
 
   helpers.login({
     email: 'testuser@testuser.com',
     verifiedCanLinkAccount: true,
     customizeSync: true
   });
 });
 
+add_test(function test_helpers_login_with_customize_sync_and_declined_engines() {
+  let helpers = new FxAccountsWebChannelHelpers({
+    fxAccounts: {
+      setSignedInUser: function(accountData) {
+        // ensure fxAccounts is informed of the new user being signed in.
+        do_check_eq(accountData.email, 'testuser@testuser.com');
+
+        // customizeSync should be stripped in the data.
+        do_check_false('customizeSync' in accountData);
+        do_check_false('declinedSyncEngines' in accountData);
+        do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), false);
+        do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true);
+        do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true);
+        do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true);
+        do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), false);
+        do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true);
+
+        // the customizeSync pref should be disabled
+        do_check_false(helpers.getShowCustomizeSyncPref());
+
+        run_next_test();
+      }
+    }
+  });
+
+  // the customize sync pref should be overwritten
+  helpers.setShowCustomizeSyncPref(true);
+
+  do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), true);
+  do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true);
+  do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true);
+  do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true);
+  do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), true);
+  do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true);
+  helpers.login({
+    email: 'testuser@testuser.com',
+    verifiedCanLinkAccount: true,
+    customizeSync: true,
+    declinedSyncEngines: ['addons', 'prefs']
+  });
+});
+
 function run_test() {
   run_next_test();
 }
 
 function makeObserver(aObserveTopic, aObserveFunc) {
   let callback = function (aSubject, aTopic, aData) {
     log.debug("observed " + aTopic + " " + aData);
     if (aTopic == aObserveTopic) {
--- a/services/healthreport/healthreport-prefs.js
+++ b/services/healthreport/healthreport-prefs.js
@@ -29,10 +29,10 @@ pref("datareporting.healthreport.service
     "healthreport-js-provider-default"
 #elif MOZ_UPDATE_CHANNEL == default
     "healthreport-js-provider-default"
 #else
     "healthreport-js-provider-default,healthreport-js-provider-@MOZ_UPDATE_CHANNEL@"
 #endif
     );
 
-pref("datareporting.healthreport.about.reportUrl", "https://fhr.cdn.mozilla.net/%LOCALE%/");
+pref("datareporting.healthreport.about.reportUrl", "https://fhr.cdn.mozilla.net/%LOCALE%/v4/");
 pref("datareporting.healthreport.about.reportUrlUnified", "https://fhr.cdn.mozilla.net/%LOCALE%/v4/");
--- a/toolkit/components/asyncshutdown/AsyncShutdown.jsm
+++ b/toolkit/components/asyncshutdown/AsyncShutdown.jsm
@@ -61,16 +61,22 @@ Object.defineProperty(this, "gCrashRepor
       return this.gCrashReporter = reporter;
     } catch (ex) {
       return this.gCrashReporter = null;
     }
   },
   configurable: true
 });
 
+// `true` if this is a content process, `false` otherwise.
+// It would be nicer to go through `Services.appInfo`, but some tests need to be
+// able to replace that field with a custom implementation before it is first
+// called.
+const isContent = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+
 // Display timeout warnings after 10 seconds
 const DELAY_WARNING_MS = 10 * 1000;
 
 
 // Crash the process if shutdown is really too long
 // (allowing for sleep).
 const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout";
 var DELAY_CRASH_MS = 60 * 1000; // One minute
@@ -974,18 +980,29 @@ Barrier.prototype = Object.freeze({
 
 
 
 // List of well-known phases
 // Ideally, phases should be registered from the component that decides
 // when they start/stop. For compatibility with existing startup/shutdown
 // mechanisms, we register a few phases here.
 
-this.AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown");
-this.AsyncShutdown.profileBeforeChange = getPhase("profile-before-change");
-this.AsyncShutdown.placesClosingInternalConnection = getPhase("places-will-close-connection");
-this.AsyncShutdown.sendTelemetry = getPhase("profile-before-change2");
+// Parent process
+if (!isContent) {
+  this.AsyncShutdown.profileChangeTeardown = getPhase("profile-change-teardown");
+  this.AsyncShutdown.profileBeforeChange = getPhase("profile-before-change");
+  this.AsyncShutdown.placesClosingInternalConnection = getPhase("places-will-close-connection");
+  this.AsyncShutdown.sendTelemetry = getPhase("profile-before-change2");
+}
+
+
+// Content process
+if (isContent) {
+  this.AsyncShutdown.contentChildShutdown = getPhase("content-child-shutdown");
+}
+
+// All processes
 this.AsyncShutdown.webWorkersShutdown = getPhase("web-workers-shutdown");
 this.AsyncShutdown.xpcomThreadsShutdown = getPhase("xpcom-threads-shutdown");
 
 this.AsyncShutdown.Barrier = Barrier;
 
 Object.freeze(this.AsyncShutdown);
--- a/toolkit/components/asyncshutdown/nsAsyncShutdown.js
+++ b/toolkit/components/asyncshutdown/nsAsyncShutdown.js
@@ -221,27 +221,35 @@ nsAsyncShutdownBarrier.prototype = {
   QueryInterface :  XPCOMUtils.generateQI([Ci.nsIAsyncShutdownBarrier]),
   classID:          Components.ID("{29a0e8b5-9111-4c09-a0eb-76cd02bf20fa}"),
 };
 
 function nsAsyncShutdownService() {
   // Cache for the getters
 
   for (let _k of
-   ["profileBeforeChange",
+   [// Parent process
+    "profileBeforeChange",
     "profileChangeTeardown",
     "sendTelemetry",
+
+    // Child processes
+    "contentChildShutdown",
+
+    // All processes
     "webWorkersShutdown",
-    "xpcomThreadsShutdown"]) {
+    "xpcomThreadsShutdown",
+    ]) {
     let k = _k;
     Object.defineProperty(this, k, {
       configurable: true,
       get: function() {
         delete this[k];
-        let result = new nsAsyncShutdownClient(AsyncShutdown[k]);
+        let wrapped = AsyncShutdown[k]; // May be undefined, if we're on the wrong process.
+        let result = wrapped ? new nsAsyncShutdownClient(wrapped) : undefined;
         Object.defineProperty(this, k, {
           value: result
         });
         return result;
       }
     });
   }
 
@@ -261,9 +269,8 @@ nsAsyncShutdownService.prototype = {
 };
 
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
     nsAsyncShutdownService,
     nsAsyncShutdownBarrier,
     nsAsyncShutdownClient,
 ]);
-
--- a/toolkit/components/asyncshutdown/nsIAsyncShutdown.idl
+++ b/toolkit/components/asyncshutdown/nsIAsyncShutdown.idl
@@ -155,53 +155,61 @@ interface nsIAsyncShutdownBarrier: nsISu
    * The callback always receives NS_OK.
    */
   void wait(in nsIAsyncShutdownCompletionCallback aOnReady);
 };
 
 /**
  * A service that allows registering shutdown-time dependencies.
  */
-[scriptable, uuid(8a9a0859-0404-4d50-9e76-10a4f56dfb51)]
+[scriptable, uuid(10f51032-dcc9-4732-bec0-c0b7d6596622)]
 interface nsIAsyncShutdownService: nsISupports {
   /**
    * Create a new barrier.
    *
    * By convention, the name should respect the following format:
    * "MyModuleName: Doing something while it's time"
    * e.g.
    * "OS.File: Waiting for clients to flush before shutting down"
    *
    * This attribute is uploaded as part of crash reports.
    */
   nsIAsyncShutdownBarrier makeBarrier(in AString aName);
 
-  // Barriers for global shutdown stages
+
+  // Barriers for global shutdown stages in the parent process.
 
   /**
    * Barrier for notification profile-before-change.
    */
   readonly attribute nsIAsyncShutdownClient profileBeforeChange;
 
   /**
    * Barrier for notification profile-change-teardown.
    */
   readonly attribute nsIAsyncShutdownClient profileChangeTeardown;
 
   /**
    * Barrier for notification profile-before-change2.
    */
   readonly attribute nsIAsyncShutdownClient sendTelemetry;
 
+  // Barriers for global shutdown stages in the content processes.
+
+  readonly attribute nsIAsyncShutdownClient contentChildShutdown;
+
+  // Barriers for global shutdown stages in all processes.
+
   /**
    * Barrier for notification web-workers-shutdown.
    */
   readonly attribute nsIAsyncShutdownClient webWorkersShutdown;
 
   /**
    * Barrier for notification xpcom-threads-shutdown.
    */
   readonly attribute nsIAsyncShutdownClient xpcomThreadsShutdown;
+
 };
 
 %{C++
 #define NS_ASYNCSHUTDOWNSERVICE_CONTRACTID "@mozilla.org/async-shutdown-service;1"
 %}
--- a/toolkit/components/osfile/modules/osfile_async_front.jsm
+++ b/toolkit/components/osfile/modules/osfile_async_front.jsm
@@ -1458,21 +1458,26 @@ this.OS.Path = Path;
 
 // Returns a resolved promise when all the queued operation have been completed.
 Object.defineProperty(OS.File, "queue", {
   get: function() {
     return Scheduler.queue;
   }
 });
 
+// `true` if this is a content process, `false` otherwise.
+// It would be nicer to go through `Services.appInfo`, but some tests need to be
+// able to replace that field with a custom implementation before it is first
+// called.
+const isContent = Components.classes["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+
 /**
  * Shutdown barriers, to let clients register to be informed during shutdown.
  */
 var Barriers = {
-  profileBeforeChange: new AsyncShutdown.Barrier("OS.File: Waiting for clients before profile-before-shutdown"),
   shutdown: new AsyncShutdown.Barrier("OS.File: Waiting for clients before full shutdown"),
   /**
    * Return the shutdown state of OS.File
    */
   getDetails: function() {
     let result = {
       launched: Scheduler.launched,
       shutdown: Scheduler.shutdown,
@@ -1490,30 +1495,40 @@ var Barriers = {
       if (result[key] && typeof result[key][0] == "number") {
         result[key][0] = Date(result[key][0]);
       }
     }
     return result;
   }
 };
 
-File.profileBeforeChange = Barriers.profileBeforeChange.client;
-File.shutdown = Barriers.shutdown.client;
+function setupShutdown(phaseName) {
+  Barriers[phaseName] = new AsyncShutdown.Barrier(`OS.File: Waiting for clients before ${phaseName}`),
+  File[phaseName] = Barriers[phaseName].client;
+
+  // Auto-flush OS.File during `phaseName`. This ensures that any I/O
+  // that has been queued *before* `phaseName` is properly completed.
+  // To ensure that I/O queued *during* `phaseName` change is completed,
+  // clients should register using AsyncShutdown.addBlocker.
+  AsyncShutdown[phaseName].addBlocker(
+    `OS.File: flush I/O queued before ${phaseName}`,
+    Task.async(function*() {
+      // Give clients a last chance to enqueue requests.
+      yield Barriers[phaseName].wait({crashAfterMS: null});
 
-// Auto-flush OS.File during profile-before-change. This ensures that any I/O
-// that has been queued *before* profile-before-change is properly completed.
-// To ensure that I/O queued *during* profile-before-change is completed,
-// clients should register using AsyncShutdown.addBlocker.
-AsyncShutdown.profileBeforeChange.addBlocker(
-  "OS.File: flush I/O queued before profile-before-change",
-  Task.async(function*() {
-    // Give clients a last chance to enqueue requests.
-    yield Barriers.profileBeforeChange.wait({crashAfterMS: null});
+      // Wait until all currently enqueued requests are completed.
+      yield Scheduler.queue;
+    }),
+    () => {
+      let details = Barriers.getDetails();
+      details.clients = Barriers[phaseName].state;
+      return details;
+    }
+  );
+}
 
-    // Wait until all currently enqueued requests are completed.
-    yield Scheduler.queue;
-  }),
-  () => {
-    let details = Barriers.getDetails();
-    details.clients = Barriers.profileBeforeChange.state;
-    return details;
-  }
-);
+
+if (isContent) {
+  setupShutdown("contentChildShutdown");
+} else {
+  setupShutdown("profileBeforeChange")
+}
+File.shutdown = Barriers.shutdown.client;
--- a/toolkit/modules/Troubleshoot.jsm
+++ b/toolkit/modules/Troubleshoot.jsm
@@ -80,16 +80,21 @@ const PREFS_WHITELIST = [
   "network.",
   "permissions.default.image",
   "places.",
   "plugin.",
   "plugins.",
   "print.",
   "privacy.",
   "security.",
+  "services.sync.declinedEngines",
+  "services.sync.lastPing",
+  "services.sync.lastSync",
+  "services.sync.numClients",
+  "services.sync.engine.",
   "social.enabled",
   "storage.vacuum.last.",
   "svg.",
   "toolkit.startup.recent_crashes",
   "ui.osk.enabled",
   "ui.osk.detect_physical_keyboard",
   "ui.osk.require_tablet_mode",
   "ui.osk.debug.keyboardDisplayReason",
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -79,16 +79,17 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/FileUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "CertUtils", function certUtilsLazyGetter() {
   let certUtils = {};
   Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
   return certUtils;
 });
 
+const INTEGER = /^[1-9]\d*$/;
 
 this.EXPORTED_SYMBOLS = [ "AddonManager", "AddonManagerPrivate" ];
 
 const CATEGORY_PROVIDER_MODULE = "addon-provider-module";
 
 // A list of providers to load by default
 const DEFAULT_PROVIDERS = [
   "resource://gre/modules/addons/XPIProvider.jsm",
@@ -2341,17 +2342,20 @@ var AddonManagerInternal = {
    * that is closest to the specified size.
    *
    * The optional window parameter will be used to determine
    * the screen resolution and select a more appropriate icon.
    * Calling this method with 48px on retina screens will try to
    * match an icon of size 96px.
    *
    * @param  aAddon
-   *         The addon to find an icon for
+   *         An addon object, meaning:
+   *         An object with either an icons property that is a key-value
+   *         list of icon size and icon URL, or an object having an iconURL
+   *         and icon64URL property.
    * @param  aSize
    *         Ideal icon size in pixels
    * @param  aWindow
    *         Optional window object for determining the correct scale.
    * @return {String} The absolute URL of the icon or null if the addon doesn't have icons
    */
   getPreferredIconURL: function AMI_getPreferredIconURL(aAddon, aSize, aWindow = undefined) {
     if (aWindow && aWindow.devicePixelRatio) {
@@ -2375,22 +2379,23 @@ var AddonManagerInternal = {
     // quick return if the exact size was found
     if (icons[aSize]) {
       return icons[aSize];
     }
 
     let bestSize = null;
 
     for (let size of Object.keys(icons)) {
-      size = parseInt(size, 10);
-      if (isNaN(size)) {
+      if (!INTEGER.test(size)) {
         throw Components.Exception("Invalid icon size, must be an integer",
                                    Cr.NS_ERROR_ILLEGAL_VALUE);
       }
 
+      size = parseInt(size, 10);
+
       if (!bestSize) {
         bestSize = size;
         continue;
       }
 
       if (size > aSize && bestSize > aSize) {
         // If both best size and current size are larger than the wanted size then choose
         // the one closest to the wanted size
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -236,16 +236,17 @@ const SIGNED_TYPES = new Set([
 
 // Whether add-on signing is required.
 function mustSign(aType) {
   if (!SIGNED_TYPES.has(aType))
     return false;
   return REQUIRE_SIGNING || Preferences.get(PREF_XPI_SIGNATURES_REQUIRED, false);
 }
 
+const INTEGER = /^[1-9]\d*$/;
 
 // Keep track of where we are in startup for telemetry
 // event happened during XPIDatabase.startup()
 const XPI_STARTING = "XPIStarting";
 // event happened after startup() but before the final-ui-startup event
 const XPI_BEFORE_UI_STARTUP = "BeforeFinalUIStartup";
 // event happened after final-ui-startup
 const XPI_AFTER_UI_STARTUP = "AfterFinalUIStartup";
@@ -907,18 +908,18 @@ var loadManifestFromWebManifest = Task.a
   addon.iconURL = null;
   addon.icon64URL = null;
   addon.icons = {};
 
   let icons = getOptionalProp('icons');
   if (icons) {
     // filter out invalid (non-integer) size keys
     Object.keys(icons)
+          .filter((size) => INTEGER.test(size))
           .map((size) => parseInt(size, 10))
-          .filter((size) => !isNaN(size))
           .forEach((size) => addon.icons[size] = icons[size]);
   }
 
   addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
 
   function getLocale(aLocale) {
     let result = {
       name: extension.localize(getProp("name"), aLocale),
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
@@ -82,16 +82,19 @@ add_task(function*() {
     applications: {
       gecko: {
         id: ID
       }
     },
     icons: {
       32: "icon32.png",
       banana: "bananana.png",
+      "20.5": "icon20.5.png",
+      "20.0": "also invalid",
+      "123banana": "123banana.png",
       64: "icon64.png"
     }
   }, profileDir);
 
   yield promiseRestartManager();
 
   let addon = yield promiseAddonByID(ID);
   do_check_neq(addon, null);
--- a/toolkit/themes/windows/global/alerts/alert.css
+++ b/toolkit/themes/windows/global/alerts/alert.css
@@ -5,16 +5,21 @@
 /* ===== alert.css =====================================================
   == Styles specific to the alerts dialog.
   ======================================================================= */
 
 @import url("chrome://global/skin/alerts/alert-common.css");
 
 @namespace url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
 
+#alertNotification {
+  -moz-appearance: none;
+  background: transparent;
+}
+
 #alertBox {
   border: 1px solid ThreeDShadow;
   border-radius: 1px;
   background-color: -moz-Dialog;
   color: -moz-DialogText;
 }
 
 .alertCloseButton {