merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 05 Apr 2016 16:50:25 +0200
changeset 347708 f0110e77e9f7b40ba80b3640ea7cd038398b8eee
parent 347649 dc76178b7202c86424274a391f9d3fad745fcf53 (current diff)
parent 347682 071ebc4f3ce73133d7d47ceeaf7f7049bad1b9af (diff)
child 347760 17a0ded9bb99c05c25729c306b91771483109067
push id14653
push userolivier@olivieryiptong.com
push dateTue, 05 Apr 2016 19:21:01 +0000
reviewersmerge
milestone48.0a1
merge fx-team to mozilla-central a=merge
toolkit/components/passwordmgr/test/test_bug_627616.html
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -1013,36 +1013,53 @@
       <method name="updateTitlebar">
         <body>
           <![CDATA[
             this.ownerDocument.title = this.getWindowTitleForBrowser(this.mCurrentBrowser);
           ]]>
         </body>
       </method>
 
+      <!-- Holds a unique ID for the tab change that's currently being timed.
+           Used to make sure that multiple, rapid tab switches do not try to
+           create overlapping timers. -->
+      <field name="_tabSwitchID">null</field>
+
       <method name="updateCurrentBrowser">
         <parameter name="aForceUpdate"/>
         <body>
           <![CDATA[
             var newBrowser = this.getBrowserAtIndex(this.tabContainer.selectedIndex);
             if (this.mCurrentBrowser == newBrowser && !aForceUpdate)
               return;
 
             if (!aForceUpdate) {
               TelemetryStopwatch.start("FX_TAB_SWITCH_UPDATE_MS");
               if (!gMultiProcessBrowser) {
                 // old way of measuring tab paint which is not valid with e10s.
                 // Waiting until the next MozAfterPaint ensures that we capture
                 // the time it takes to paint, upload the textures to the compositor,
                 // and then composite.
+                if (this._tabSwitchID) {
+                  TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_MS");
+                }
+
+                let tabSwitchID = Symbol();
+
                 TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_MS");
-                window.addEventListener("MozAfterPaint", function onMozAfterPaint() {
-                  TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_MS");
+                this._tabSwitchID = tabSwitchID;
+
+                let onMozAfterPaint = () => {
+                  if (this._tabSwitchID === tabSwitchID) {
+                    TelemetryStopwatch.finish("FX_TAB_SWITCH_TOTAL_MS");
+                    this._tabSwitchID = null;
+                  }
                   window.removeEventListener("MozAfterPaint", onMozAfterPaint);
-                });
+                }
+                window.addEventListener("MozAfterPaint", onMozAfterPaint);
               }
             }
 
             var oldTab = this.mCurrentTab;
 
             // Preview mode should not reset the owner
             if (!this._previewMode && !oldTab.selected)
               oldTab.owner = null;
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -281,17 +281,17 @@ const CustomizableWidgets = [
       recentlyClosedWindows.addEventListener("click", onRecentlyClosedClick);
     },
     onViewHiding: function(aEvent) {
       log.debug("History view is being hidden!");
     }
   }, {
     id: "sync-button",
     label: "remotetabs-panelmenu.label",
-    tooltiptext: "remotetabs-panelmenu.tooltiptext",
+    tooltiptext: "remotetabs-panelmenu.tooltiptext2",
     type: "view",
     viewId: "PanelUI-remotetabs",
     defaultArea: CustomizableUI.AREA_PANEL,
     deckIndices: {
       DECKINDEX_TABS: 0,
       DECKINDEX_TABSDISABLED: 1,
       DECKINDEX_FETCHING: 2,
       DECKINDEX_NOCLIENTS: 3,
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -1233,17 +1233,18 @@ BrowserGlue.prototype = {
           DefaultBrowserCheck.prompt(RecentWindow.getMostRecentBrowserWindow());
         }.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
       }
     }
 
     if (AppConstants.E10S_TESTING_ONLY) {
       E10SUINotification.checkStatus();
     }
-    if (AppConstants.platform == "win") {
+    if (AppConstants.platform == "win" ||
+        AppConstants.platform == "macosx") {
       // Handles prompting to inform about incompatibilites when accessibility
       // and e10s are active together.
       E10SAccessibilityCheck.init();
     }
   },
 
   _createExtraDefaultProfile: function () {
     if (!AppConstants.MOZ_DEV_EDITION) {
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -248,17 +248,17 @@ var gMainPane = {
 
   onGetStarted: function (aEvent) {
     const Cc = Components.classes, Ci = Components.interfaces;
     let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
                 .getService(Ci.nsIWindowMediator);
     let win = wm.getMostRecentWindow("navigator:browser");
 
     if (win) {
-      let accountsTab = win.gBrowser.addTab("about:accounts");
+      let accountsTab = win.gBrowser.addTab("about:accounts?action=signin&entrypoint=dev-edition-setup");
       win.gBrowser.selectedTab = accountsTab;
     }
   },
 #endif
 
   // HOME PAGE
 
   /*
--- a/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
+++ b/browser/locales/en-US/chrome/browser/customizableui/customizableWidgets.properties
@@ -2,17 +2,17 @@
 # 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/.
 
 history-panelmenu.label = History
 # LOCALIZATION NOTE(history-panelmenu.tooltiptext2): %S is the keyboard shortcut
 history-panelmenu.tooltiptext2 = Show your history (%S)
 
 remotetabs-panelmenu.label = Synced Tabs
-remotetabs-panelmenu.tooltiptext = Show your synced tabs from other devices
+remotetabs-panelmenu.tooltiptext2 = Show tabs from other devices
 
 privatebrowsing-button.label = New Private Window
 # LOCALIZATION NOTE(privatebrowsing-button.tooltiptext): %S is the keyboard shortcut
 privatebrowsing-button.tooltiptext = Open a new Private Browsing window (%S)
 
 save-page-button.label = Save Page
 # LOCALIZATION NOTE(save-page-button.tooltiptext3): %S is the keyboard shortcut
 save-page-button.tooltiptext3 = Save this page (%S)
--- a/devtools/client/debugger/utils.js
+++ b/devtools/client/debugger/utils.js
@@ -310,17 +310,21 @@ var SourceUtils = {
       let dir = aUrl.directory;
       if (dir) {
         return this.trimUrl(aUrl, dir.replace(/^\//, "") + aLabel, aSeq + 1);
       }
       aSeq++;
     }
     // Prepend the hostname and port number.
     if (aSeq == 4) {
-      let host = aUrl.hostPort;
+      let host;
+      try {
+        // Bug 1261860: jar: URLs throw when accessing `hostPost`
+        host = aUrl.hostPort;
+      } catch(e) {}
       if (host) {
         return this.trimUrl(aUrl, host + "/" + aLabel, aSeq + 1);
       }
       aSeq++;
     }
     // Use the whole url spec but ignoring the reference.
     if (aSeq == 5) {
       return this.trimUrl(aUrl, aUrl.specIgnoringRef, aSeq + 1);
--- a/devtools/client/framework/target.js
+++ b/devtools/client/framework/target.js
@@ -473,26 +473,28 @@ TabTarget.prototype = {
       // We have to filter message to ensure that this detach is for this tab
       if (aPacket.from == this._form.actor) {
         this.destroy();
       }
     };
     this.client.addListener("tabDetached", this._onTabDetached);
 
     this._onTabNavigated = (aType, aPacket) => {
-      // Update the title and url on tabNavigated event.
-      this._url = aPacket.url;
-      this._title = aPacket.title;
-
       let event = Object.create(null);
       event.url = aPacket.url;
       event.title = aPacket.title;
       event.nativeConsoleAPI = aPacket.nativeConsoleAPI;
       event.isFrameSwitching = aPacket.isFrameSwitching;
 
+      if (!aPacket.isFrameSwitching) {
+        // Update the title and url unless this is a frame switch.
+        this._url = aPacket.url;
+        this._title = aPacket.title;
+      }
+
       // Send any stored event payload (DOMWindow or nsIRequest) for backwards
       // compatibility with non-remotable tools.
       if (aPacket.state == "start") {
         event._navPayload = this._navRequest;
         this.emit("will-navigate", event);
         this._navRequest = null;
       } else {
         event._navPayload = this._navWindow;
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -2,16 +2,17 @@
 tags = devtools
 subsuite = devtools
 support-files =
   browser_toolbox_options_disable_js.html
   browser_toolbox_options_disable_js_iframe.html
   browser_toolbox_options_disable_cache.sjs
   browser_toolbox_sidebar_tool.xul
   browser_toolbox_window_title_changes_page.html
+  browser_toolbox_window_title_frame_select_page.html
   code_math.js
   code_ugly.js
   head.js
   shared-head.js
   shared-redux-head.js
   helper_disable_cache.js
   doc_theme.css
   doc_viewsource.html
@@ -69,13 +70,14 @@ skip-if = e10s # Bug 1069044 - destroyIn
 [browser_toolbox_view_source_01.js]
 [browser_toolbox_view_source_02.js]
 [browser_toolbox_view_source_03.js]
 [browser_toolbox_view_source_04.js]
 [browser_toolbox_window_reload_target.js]
 [browser_toolbox_window_shortcuts.js]
 skip-if = os == "mac" && os_version == "10.8" || os == "win" && os_version == "5.1" # Bug 851129 - Re-enable browser_toolbox_window_shortcuts.js test after leaks are fixed
 [browser_toolbox_window_title_changes.js]
+[browser_toolbox_window_title_frame_select.js]
 [browser_toolbox_zoom.js]
 [browser_two_tabs.js]
 skip-if = e10s && debug && os == 'win' # Bug 1231869
 # We want this test to run for mochitest-dt as well, so we include it here:
 [../../../../browser/base/content/test/general/browser_parsable_css.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select.js
@@ -0,0 +1,72 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from shared-head.js */
+
+"use strict";
+
+/**
+ * Check that the detached devtools window title is not updated when switching
+ * the selected frame.
+ */
+
+var {Toolbox} = require("devtools/client/framework/toolbox");
+const URL = URL_ROOT + "browser_toolbox_window_title_frame_select_page.html";
+const IFRAME_URL = URL_ROOT + "browser_toolbox_window_title_changes_page.html";
+
+add_task(function* () {
+  Services.prefs.setBoolPref("devtools.command-button-frames.enabled", true);
+
+  yield addTab(URL);
+  let target = TargetFactory.forTab(gBrowser.selectedTab);
+  let toolbox = yield gDevTools.showToolbox(target, null,
+    Toolbox.HostType.BOTTOM);
+
+  yield toolbox.selectTool("inspector");
+  yield toolbox.switchHost(Toolbox.HostType.WINDOW);
+
+  is(getTitle(), "Inspector - Page title",
+    "Devtools title correct after switching to detached window host");
+
+  // Verify that the frame list button is visible and populated
+  let btn = toolbox.doc.getElementById("command-button-frames");
+  let frames = Array.slice(btn.firstChild.querySelectorAll("[data-window-id]"));
+  is(frames.length, 2, "We have both frames in the list");
+
+  let topFrameBtn = frames.filter(b => b.getAttribute("label") == URL)[0];
+  let iframeBtn = frames.filter(b => b.getAttribute("label") == IFRAME_URL)[0];
+  ok(topFrameBtn, "Got top level document in the list");
+  ok(iframeBtn, "Got iframe document in the list");
+
+  // Listen to will-navigate to check if the view is empty
+  let willNavigate = toolbox.target.once("will-navigate");
+
+  // Only select the iframe after we are able to select an element from the top
+  // level document.
+  let newRoot = toolbox.getPanel("inspector").once("new-root");
+  info("Select the iframe");
+  iframeBtn.click();
+
+  yield willNavigate;
+  yield newRoot;
+
+  info("Navigation to the iframe is done, the inspector should be back up");
+  is(getTitle(), "Inspector - Page title",
+    "Devtools title was not updated after changing inspected frame");
+
+  info("Cleanup toolbox and test preferences.");
+  yield toolbox.destroy();
+  toolbox = null;
+  gBrowser.removeCurrentTab();
+  Services.prefs.clearUserPref("devtools.toolbox.host");
+  Services.prefs.clearUserPref("devtools.toolbox.selectedTool");
+  Services.prefs.clearUserPref("devtools.toolbox.sideEnabled");
+  Services.prefs.clearUserPref("devtools.command-button-frames.enabled");
+  finish();
+});
+
+function getTitle() {
+  return Services.wm.getMostRecentWindow("devtools:toolbox").document.title;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_toolbox_window_title_frame_select_page.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <title>Page title</title>
+    <!-- Any copyright is dedicated to the Public Domain.
+         http://creativecommons.org/publicdomain/zero/1.0/ -->
+    <iframe src="browser_toolbox_window_title_changes_page.html"></iframe>
+  </head>
+  <body></body>
+</html>
--- a/devtools/client/shared/browser-loader.js
+++ b/devtools/client/shared/browser-loader.js
@@ -74,17 +74,16 @@ function BrowserLoader(options) {
  */
 function BrowserLoaderBuilder({ baseURI, window, useOnlyShared }) {
   assert(!!baseURI !== !!useOnlyShared,
     "Cannot use both `baseURI` and `useOnlyShared`.");
 
   const loaderOptions = devtools.require("@loader/options");
   const dynamicPaths = {};
   const componentProxies = new Map();
-  const hotReloadEnabled = Services.prefs.getBoolPref("devtools.loader.hotreload");
 
   if (AppConstants.DEBUG || AppConstants.DEBUG_JS_MODULES) {
     dynamicPaths["devtools/client/shared/vendor/react"] =
       "resource://devtools/client/shared/vendor/react-dev";
   }
 
   const opts = {
     id: "browser-loader",
@@ -129,17 +128,17 @@ function BrowserLoaderBuilder({ baseURI,
         lazyGetter: devtools.lazyGetter,
         lazyImporter: devtools.lazyImporter,
         lazyServiceGetter: devtools.lazyServiceGetter,
         lazyRequireGetter: this.lazyRequireGetter.bind(this),
       },
     }
   };
 
-  if (hotReloadEnabled) {
+  if (Services.prefs.getBoolPref("devtools.loader.hotreload")) {
     opts.loadModuleHook = (module, require) => {
       const { uri, exports } = module;
 
       if (exports.prototype &&
           exports.prototype.isReactComponent) {
         const { createProxy, getForceUpdate } =
               require("devtools/client/shared/vendor/react-proxy");
         const React = require("devtools/client/shared/vendor/react");
@@ -153,34 +152,29 @@ function BrowserLoaderBuilder({ baseURI,
           const proxy = componentProxies.get(uri);
           const instances = proxy.update(exports);
           instances.forEach(getForceUpdate(React));
           module.exports = proxy.get();
         }
       }
       return exports;
     }
+    const watcher = devtools.require("devtools/client/shared/devtools-file-watcher");
+    let onFileChanged = (_, relativePath, path) => {
+      this.hotReloadFile(componentProxies, "resource://devtools/" + relativePath);
+    };
+    watcher.on("file-changed", onFileChanged);
+    window.addEventListener("unload", () => {
+      watcher.off("file-changed", onFileChanged);
+    });
   }
 
   const mainModule = loaders.Module(baseURI, joinURI(baseURI, "main.js"));
   this.loader = loaders.Loader(opts);
   this.require = loaders.Require(this.loader, mainModule);
-
-  if (hotReloadEnabled) {
-    const watcher = devtools.require("devtools/client/shared/file-watcher");
-    const onFileChanged = (_, relativePath) => {
-      this.hotReloadFile(window, componentProxies,
-                         "resource://devtools/" + relativePath);
-    };
-    watcher.on("file-changed", onFileChanged);
-
-    window.addEventListener("unload", () => {
-      watcher.off("file-changed", onFileChanged);
-    });
-  }
 }
 
 BrowserLoaderBuilder.prototype = {
   /**
    * Define a getter property on the given object that requires the given
    * module. This enables delaying importing modules until the module is
    * actually used.
    *
@@ -196,17 +190,17 @@ BrowserLoaderBuilder.prototype = {
   lazyRequireGetter: function(obj, property, module, destructure) {
     devtools.lazyGetter(obj, property, () => {
       return destructure
           ? this.require(module)[property]
           : this.require(module || property);
     });
   },
 
-  hotReloadFile: function(window, componentProxies, fileURI) {
+  hotReloadFile: function(componentProxies, fileURI) {
     if (fileURI.match(/\.js$/)) {
       // Test for React proxy components
       const proxy = componentProxies.get(fileURI);
       if (proxy) {
         // Remove the old module and re-require the new one; the require
         // hook in the loader will take care of the rest
         delete this.loader.modules[fileURI];
         clearCache();
--- a/devtools/client/shared/css-reload.js
+++ b/devtools/client/shared/css-reload.js
@@ -98,17 +98,17 @@ function replaceCSSResource(window, file
     if (node.src.startsWith(fileURI)) {
       node.src = fileURI + "?s=" + randomKey;
     }
   }
 }
 
 function watchCSS(window) {
   if (Services.prefs.getBoolPref("devtools.loader.hotreload")) {
-    const watcher = require("devtools/client/shared/file-watcher");
+    const watcher = require("devtools/client/shared/devtools-file-watcher");
 
     function onFileChanged(_, relativePath) {
       if (relativePath.match(/\.css$/)) {
         if (relativePath.startsWith("client/themes")) {
           let path = relativePath.replace(/^client\/themes\//, "");
 
           // Special-case a few files that get imported from other CSS
           // files. We just manually hot reload the parent CSS file.
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/devtools-file-watcher.js
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+"use strict";
+
+const { Ci } = require("chrome");
+const Services = require("Services");
+const EventEmitter = require("devtools/shared/event-emitter");
+
+loader.lazyImporter(this, "OS", "resource://gre/modules/osfile.jsm");
+
+const HOTRELOAD_PREF = "devtools.loader.hotreload";
+
+function resolveResourcePath(uri) {
+  const handler = Services.io.getProtocolHandler("resource")
+        .QueryInterface(Ci.nsIResProtocolHandler);
+  const resolved = handler.resolveURI(Services.io.newURI(uri, null, null));
+  return resolved.replace(/file:\/\//, "");
+}
+
+function findSourceDir(path) {
+  if (path === "" || path === "/") {
+    return Promise.resolve(null);
+  }
+
+  return OS.File.exists(
+    OS.Path.join(path, "devtools/client/shared/file-watcher.js")
+  ).then(exists => {
+    if (exists) {
+      return path;
+    } else {
+      return findSourceDir(OS.Path.dirname(path));
+    }
+  });
+}
+
+let worker = null;
+const onPrefChange = function() {
+  // We need to figure out a src dir to watch. These are the actual
+  // files the user is working with, not the files in the obj dir. We
+  // do this by walking up the filesystem and looking for the devtools
+  // directories, and falling back to the raw path. This means none of
+  // this will work for users who store their obj dirs outside of the
+  // src dir.
+  //
+  // We take care not to mess with the `devtoolsPath` if that's what
+  // we end up using, because it might be intentionally mapped to a
+  // specific place on the filesystem for loading devtools externally.
+  //
+  // `devtoolsPath` is currently the devtools directory inside of the
+  // obj dir, and we search for `devtools/client`, so go up 2 levels
+  // to skip that devtools dir and start searching for the src dir.
+  if (Services.prefs.getBoolPref(HOTRELOAD_PREF) && !worker) {
+    const devtoolsPath = resolveResourcePath("resource://devtools")
+      .replace(/\/$/, "");
+    const searchPoint = OS.Path.dirname(OS.Path.dirname(devtoolsPath));
+    findSourceDir(searchPoint)
+      .then(srcPath => {
+        const rootPath = srcPath ? OS.Path.join(srcPath, "devtools") : devtoolsPath;
+        const watchPath = OS.Path.join(rootPath, "client");
+        const { watchFiles } = require("devtools/client/shared/file-watcher");
+        worker = watchFiles(watchPath, path => {
+          let relativePath = path.replace(rootPath + "/", "");
+          module.exports.emit("file-changed", relativePath, path);
+        });
+      });
+  } else if (worker) {
+    worker.terminate();
+    worker = null;
+  }
+}
+Services.prefs.addObserver(HOTRELOAD_PREF, {
+  observe: onPrefChange
+}, false);
+onPrefChange();
+
+EventEmitter.decorate(module.exports);
--- a/devtools/client/shared/file-watcher-worker.js
+++ b/devtools/client/shared/file-watcher-worker.js
@@ -4,27 +4,16 @@
 "use strict";
 
 /* eslint-env worker */
 /* global OS */
 importScripts("resource://gre/modules/osfile.jsm");
 
 const modifiedTimes = new Map();
 
-function findSourceDir(path) {
-  if (path === "" || path === "/") {
-    return null;
-  } else if (OS.File.exists(
-    OS.Path.join(path, "devtools/client/shared/file-watcher.js")
-  )) {
-    return path;
-  }
-  return findSourceDir(OS.Path.dirname(path));
-}
-
 function gatherFiles(path, fileRegex) {
   let files = [];
   const iterator = new OS.File.DirectoryIterator(path);
 
   try {
     for (let child in iterator) {
       // Don't descend into test directories. Saves us some time and
       // there's no reason to.
@@ -66,49 +55,28 @@ function scanFiles(files, onChangedFile)
       modifiedTimes.set(file, info.lastModificationDate.getTime());
       onChangedFile(file);
     }
   });
 }
 
 onmessage = function(event) {
   const { path, fileRegex } = event.data;
-  const devtoolsPath = event.data.devtoolsPath.replace(/\/$/, "");
 
-  // We need to figure out a src dir to watch. These are the actual
-  // files the user is working with, not the files in the obj dir. We
-  // do this by walking up the filesystem and looking for the devtools
-  // directories, and falling back to the raw path. This means none of
-  // this will work for users who store their obj dirs outside of the
-  // src dir.
-  //
-  // We take care not to mess with the `devtoolsPath` if that's what
-  // we end up using, because it might be intentionally mapped to a
-  // specific place on the filesystem for loading devtools externally.
-  //
-  // `devtoolsPath` is currently the devtools directory inside of the
-  // obj dir, and we search for `devtools/client`, so go up 2 levels
-  // to skip that devtools dir and start searching for the src dir.
-  const searchPoint = OS.Path.dirname(OS.Path.dirname(devtoolsPath));
-  const srcPath = findSourceDir(searchPoint);
-  const rootPath = srcPath ? OS.Path.join(srcPath, "devtools") : devtoolsPath;
-  const watchPath = OS.Path.join(rootPath, path.replace(/^devtools\//, ""));
-
-  const info = OS.File.stat(watchPath);
+  const info = OS.File.stat(path);
   if (!info.isDir) {
     throw new Error("Watcher expects a directory as root path");
   }
 
   // We get a list of all the files upfront, which means we don't
   // support adding new files. But you need to rebuild Firefox when
   // adding a new file anyway.
-  const files = gatherFiles(watchPath, fileRegex || /.*/);
+  const files = gatherFiles(path, fileRegex || /.*/);
 
   // Every second, scan for file changes by stat-ing each of them and
   // comparing modification time.
   setInterval(() => {
     scanFiles(files, changedFile => {
-      postMessage({ fullPath: changedFile,
-                    relativePath: changedFile.replace(rootPath + "/", "") });
+      postMessage({ path: changedFile });
     });
   }, 1000);
 };
 
--- a/devtools/client/shared/file-watcher.js
+++ b/devtools/client/shared/file-watcher.js
@@ -1,64 +1,29 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const { Ci, ChromeWorker } = require("chrome");
 const Services = require("Services");
-const EventEmitter = require("devtools/shared/event-emitter");
-
-const HOTRELOAD_PREF = "devtools.loader.hotreload";
-
-function resolveResourcePath(uri) {
-  const handler = Services.io.getProtocolHandler("resource")
-        .QueryInterface(Ci.nsIResProtocolHandler);
-  const resolved = handler.resolveURI(Services.io.newURI(uri, null, null));
-  return resolved.replace(/file:\/\//, "");
-}
 
 function watchFiles(path, onFileChanged) {
-  if (!path.startsWith("devtools/")) {
-    throw new Error("`watchFiles` expects a devtools path");
-  }
-
   const watchWorker = new ChromeWorker(
     "resource://devtools/client/shared/file-watcher-worker.js"
   );
 
   watchWorker.onmessage = event => {
     // We need to turn a local path back into a resource URI (or
     // chrome). This means that this system will only work when built
     // files are symlinked, so that these URIs actually read from
     // local sources. There might be a better way to do this.
-    const { relativePath, fullPath } = event.data;
-    onFileChanged(relativePath, fullPath);
+    const { path } = event.data;
+    onFileChanged(path);
   };
 
   watchWorker.postMessage({
-    path: path,
-    // We must do this here because we can't access the needed APIs in
-    // a worker.
-    devtoolsPath: resolveResourcePath("resource://devtools"),
-    fileRegex: /\.(js|css|svg|png)$/ });
+    path,
+    fileRegex: /\.(js|css|svg|png)$/
+  });
   return watchWorker;
 }
-
-EventEmitter.decorate(module.exports);
-
-let watchWorker;
-function onPrefChange() {
-  if (Services.prefs.getBoolPref(HOTRELOAD_PREF) && !watchWorker) {
-    watchWorker = watchFiles("devtools/client", (relativePath, fullPath) => {
-      module.exports.emit("file-changed", relativePath, fullPath);
-    });
-  } else if (watchWorker) {
-    watchWorker.terminate();
-    watchWorker = null;
-  }
-}
-
-Services.prefs.addObserver(HOTRELOAD_PREF, {
-  observe: onPrefChange
-}, false);
-
-onPrefChange();
+exports.watchFiles = watchFiles;
--- a/devtools/client/shared/inplace-editor.js
+++ b/devtools/client/shared/inplace-editor.js
@@ -377,17 +377,17 @@ InplaceEditor.prototype = {
       if (this.maxWidth) {
         style.maxWidth = this.maxWidth + "px";
         // Use position fixed to measure dimensions without any influence from
         // the container of the editor.
         style.position = "fixed";
       }
     }
 
-    copyTextStyles(this.input, this._measurement);
+    copyAllStyles(this.input, this._measurement);
     this._updateSize();
   },
 
   /**
    * Clean up the mess created by _autosize().
    */
   _stopAutosize: function() {
     if (!this._measurement) {
@@ -1362,16 +1362,27 @@ function copyTextStyles(from, to) {
   let win = from.ownerDocument.defaultView;
   let style = win.getComputedStyle(from);
   let getCssText = name => style.getPropertyCSSValue(name).cssText;
 
   to.style.fontFamily = getCssText("font-family");
   to.style.fontSize = getCssText("font-size");
   to.style.fontWeight = getCssText("font-weight");
   to.style.fontStyle = getCssText("font-style");
+}
+
+/**
+ * Copy all styles which could have an impact on the element size.
+ */
+function copyAllStyles(from, to) {
+  let win = from.ownerDocument.defaultView;
+  let style = win.getComputedStyle(from);
+  let getCssText = name => style.getPropertyCSSValue(name).cssText;
+
+  copyTextStyles(from, to);
   to.style.lineHeight = getCssText("line-height");
 
   // If box-sizing is set to border-box, box model styles also need to be
   // copied.
   let boxSizing = getCssText("box-sizing");
   if (boxSizing === "border-box") {
     to.style.boxSizing = boxSizing;
     copyBoxModelStyles(from, to);
--- a/devtools/client/shared/moz.build
+++ b/devtools/client/shared/moz.build
@@ -19,16 +19,17 @@ DevToolsModules(
     'autocomplete-popup.js',
     'browser-loader.js',
     'css-parsing-utils.js',
     'css-reload.js',
     'Curl.jsm',
     'demangle.js',
     'developer-toolbar.js',
     'devices.js',
+    'devtools-file-watcher.js',
     'DOMHelpers.jsm',
     'doorhanger.js',
     'file-watcher-worker.js',
     'file-watcher.js',
     'frame-script-utils.js',
     'getjson.js',
     'inplace-editor.js',
     'Jsbeautify.jsm',
--- a/devtools/client/shared/test/browser_inplace-editor_maxwidth.js
+++ b/devtools/client/shared/test/browser_inplace-editor_maxwidth.js
@@ -1,17 +1,16 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 var { editableField } = require("devtools/client/shared/inplace-editor");
 
-const LINE_HEIGHT = 15;
 const MAX_WIDTH = 300;
 const START_TEXT = "Start text";
 const LONG_TEXT = "I am a long text and I will not fit in a 300px container. " +
   "I expect the inplace editor to wrap.";
 
 // Test the inplace-editor behavior with a maxWidth configuration option
 // defined.
 
@@ -90,17 +89,20 @@ let testMaxWidth = Task.async(function* 
 
 /**
  * Retrieve the current number of lines displayed in the provided textarea.
  *
  * @param {DOMNode} textarea
  * @return {Number} the number of lines
  */
 function getLines(textarea) {
-  return Math.floor(textarea.clientHeight / LINE_HEIGHT);
+  let win = textarea.ownerDocument.defaultView;
+  let style = win.getComputedStyle(textarea);
+  let lineHeight = style.getPropertyCSSValue("line-height").cssText;
+  return Math.floor(textarea.clientHeight / parseFloat(lineHeight));
 }
 
 /**
  * Verify that the provided textarea has no vertical or horizontal scrollbar.
  *
  * @param {DOMNode} textarea
  */
 function checkScrollbars(textarea) {
@@ -120,15 +122,14 @@ function createInplaceEditorAndClick(opt
   info("Clicking on the inplace-editor field to turn to edit mode");
   span.click();
 }
 
 function createSpan(doc) {
   info("Creating a new span element");
   let span = doc.createElement("span");
   span.setAttribute("tabindex", "0");
-  span.style.lineHeight = LINE_HEIGHT + "px";
   span.style.fontSize = "11px";
   span.style.fontFamily = "monospace";
   span.textContent = START_TEXT;
   doc.body.appendChild(span);
   return span;
 }
--- a/devtools/docs/redux-guidelines.md
+++ b/devtools/docs/redux-guidelines.md
@@ -1,10 +1,52 @@
+### Getting data from the store
+
+To get data from the store, use `connect()`.
+
+When using connect, you'll break up your component into two parts:
+
+1. The part that displays the data (presentational component)
+
+        // todos.js
+        const Todos = React.createClass({
+          propTypes: {
+            todos: PropTypes.array.isRequired
+          }
+
+          render: function() {...}
+        })
+
+        module.exports = Todos;
+
+2. The part that gets the data from the store (container component)
+
+        // todos-container.js
+        const Todos = require("path/to/todos");
+
+        function mapStateToProps(state) {
+          return {
+            todos: state.todos
+          };
+        }
+
+        module.exports = connect(mapStateToProps)(Todos);
+
+
+`connect()` generates the container component. It wraps around the presentational component that was passed in (e.g. Todos).
+
+The `mapStateToProps` is often called a selector. That's because it selects data from the state object. When the container component is rendering, the the selector will be called. It will pick out the data that the presentational component is going to need. Redux will take this object and pass it in to the presentational component as props.
+
+With this setup, a presentational component is easy to share across different apps. It doesn't have any dependencies on the app, or any hardcoded expectations about how to get data. It just gets props that are passed to it and renders them.
+
+For more advanced use cases, you can pass additional parameters into the selector and `connect()` functions. Read about those in the [`connect()`](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) docs.
+
+---
 
 Need to answer the following questions:
 
 * How do I do I load asynchronous data?
 * How do I do optimistic updates or respond to errors from async work?
 * Do I use Immutable.js for my state?
 * What file structure should I use?
 * How do I test redux code?
 
-And more.
\ No newline at end of file
+And more.
--- a/mobile/android/base/FennecManifest_permissions.xml.in
+++ b/mobile/android/base/FennecManifest_permissions.xml.in
@@ -5,19 +5,22 @@
      they can be easily shared between the two APKs. -->
 
 #include ../services/manifests/FxAccountAndroidManifest_permissions.xml.in
 
 #ifdef MOZ_ANDROID_SEARCH_ACTIVITY
 #include ../search/manifests/SearchAndroidManifest_permissions.xml.in
 #endif
 
-#ifdef MOZ_ANDROID_GCM
+<!-- Bug 1261302: we have two new permissions to request,
+     RECEIVE_BOOT_COMPLETED and the permission for push.  We want to ask for
+     them during the same release, which should be Fennec 48.  Therefore we
+     decouple the push permission from MOZ_ANDROID_GCM to let it ride ahead
+     (potentially) of the push feature. -->
 #include GcmAndroidManifest_permissions.xml.in
-#endif
 
     <!-- A signature level permission specific to each Firefox version (Android
          package name, e.g., org.mozilla.firefox).  Use this permission to
          broadcast securely within a single Firefox version.  This needs to
          agree with GlobalConstants.PER_ANDROID_PACKAGE_PERMISSION. -->
     <permission
         android:name="@ANDROID_PACKAGE_NAME@.permission.PER_ANDROID_PACKAGE"
         android:protectionLevel="signature"/>
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -370,17 +370,17 @@ android_res_files := $(filter-out $(not_
 # suggestedsites.json. The trailing semi-colon defines an empty
 # recipe: defining no recipe at all causes Make to treat the target
 # differently, in a way that defeats our dependencies.
 res/values/strings.xml: .locales.deps ;
 res/raw/browsersearch.json: .locales.deps ;
 res/raw/suggestedsites.json: .locales.deps ;
 
 all_resources = \
-  $(abspath $(CURDIR)/AndroidManifest.xml) \
+  $(DEPTH)/mobile/android/base/AndroidManifest.xml \
   $(android_res_files) \
   $(ANDROID_GENERATED_RESFILES) \
   $(NULL)
 
 # For GeckoView, we want a zip of an Android res/ directory that
 # merges the contents of all the ANDROID_RES_DIRS.  The inner res/
 # directory must have the Android resource two-layer hierarchy.
 
@@ -491,18 +491,18 @@ endef
 
 # .aapt.deps: $(all_resources)
 $(eval $(call aapt_command,.aapt.deps,$(all_resources),gecko.ap_,generated/,./))
 
 ifdef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
 .aapt.nodeps: FORCE
 	cp $(gradle_dir)/app/intermediates/res/resources-automation-debug.ap_ gecko-nodeps.ap_
 else
-# .aapt.nodeps: $(CURDIR)/AndroidManifest.xml FORCE
-$(eval $(call aapt_command,.aapt.nodeps,$(CURDIR)/AndroidManifest.xml FORCE,gecko-nodeps.ap_,gecko-nodeps/,gecko-nodeps/))
+# .aapt.nodeps: $(DEPTH)/mobile/android/base/AndroidManifest.xml FORCE
+$(eval $(call aapt_command,.aapt.nodeps,$(DEPTH)/mobile/android/base/AndroidManifest.xml FORCE,gecko-nodeps.ap_,gecko-nodeps/,gecko-nodeps/))
 endif
 
 # Override the Java settings with some specific android settings
 include $(topsrcdir)/config/android-common.mk
 
 update-generated-wrappers:
 	@cp $(CURDIR)/jni-stubs.inc $(topsrcdir)/mozglue/android
 	@cp $(CURDIR)/GeneratedJNIWrappers.cpp $(CURDIR)/GeneratedJNIWrappers.h $(CURDIR)/GeneratedJNINatives.h $(topsrcdir)/widget/android
@@ -520,17 +520,17 @@ update-generated-wrappers:
 	$(MAKE) -C ../../../faster
 	$(MAKE) -C ../installer stage-package
 	$(MKDIR) -p $(@D)
 	rsync --update $(DIST)/fennec/$(notdir $(OMNIJAR_NAME)) $@
 	$(RM) $(DIST)/fennec/$(notdir $(OMNIJAR_NAME))
 
 # Targets built very early during a Gradle build.
 gradle-targets: $(foreach f,$(constants_PP_JAVAFILES),$(f))
-gradle-targets: $(abspath AndroidManifest.xml)
+gradle-targets: $(DEPTH)/mobile/android/base/AndroidManifest.xml
 gradle-targets: $(ANDROID_GENERATED_RESFILES)
 
 ifndef MOZILLA_OFFICIAL
 # Local developers update omni.ja during their builds.  There's a
 # chicken-and-egg problem here.
 gradle-omnijar: $(abspath $(DIST)/fennec/$(OMNIJAR_NAME))
 else
 # In automation, omni.ja is built only during packaging.
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -1765,17 +1765,16 @@ public class BrowserApp extends GeckoApp
 
         } else if ("Telemetry:Gather".equals(event)) {
             final BrowserDB db = getProfile().getDB();
             final ContentResolver cr = getContentResolver();
             Telemetry.addToHistogram("PLACES_PAGES_COUNT", db.getCount(cr, "history"));
             Telemetry.addToHistogram("FENNEC_BOOKMARKS_COUNT", db.getCount(cr, "bookmarks"));
             Telemetry.addToHistogram("FENNEC_READING_LIST_COUNT", db.getReadingListAccessor().getCount(cr));
             Telemetry.addToHistogram("BROWSER_IS_USER_DEFAULT", (isDefaultBrowser(Intent.ACTION_VIEW) ? 1 : 0));
-            Telemetry.addToHistogram("FENNEC_TABQUEUE_ENABLED", (TabQueueHelper.isTabQueueEnabled(BrowserApp.this) ? 1 : 0));
             Telemetry.addToHistogram("FENNEC_CUSTOM_HOMEPAGE", (TextUtils.isEmpty(getHomepage()) ? 0 : 1));
             final SharedPreferences prefs = GeckoSharedPrefs.forProfile(getContext());
             final boolean hasCustomHomepanels = prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY) || prefs.contains(HomeConfigPrefsBackend.PREFS_CONFIG_KEY_OLD);
             Telemetry.addToHistogram("FENNEC_HOMEPANELS_CUSTOM", hasCustomHomepanels ? 1 : 0);
 
             if (Versions.feature16Plus) {
                 Telemetry.addToHistogram("BROWSER_IS_ASSIST_DEFAULT", (isDefaultBrowser(Intent.ACTION_ASSIST) ? 1 : 0));
             }
@@ -4088,22 +4087,28 @@ public class BrowserApp extends GeckoApp
         public void refresh();
         void setOnTabChangedListener(OnTabAddedOrRemovedListener listener);
         interface OnTabAddedOrRemovedListener {
             void onTabChanged();
         }
     }
 
     @Override
-    protected StartupAction getStartupAction(final String passedURL, final String action) {
-        final boolean inGuestMode = GeckoProfile.get(this).inGuestMode();
-        if (inGuestMode) {
-            return StartupAction.GUEST;
+    protected void recordStartupActionTelemetry(final String passedURL, final String action) {
+        final TelemetryContract.Method method;
+        if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
+            // This action is also recorded via "loadurl.1" > "homescreen".
+            method = TelemetryContract.Method.HOMESCREEN;
+        } else if (passedURL == null) {
+            Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, TelemetryContract.Method.HOMESCREEN, "launcher");
+            method = TelemetryContract.Method.HOMESCREEN;
+        } else {
+            // This is action is also recorded via "loadurl.1" > "intent".
+            method = TelemetryContract.Method.INTENT;
         }
-        if (Restrictions.isRestrictedProfile(this)) {
-            return StartupAction.RESTRICTED;
+
+        if (GeckoProfile.get(this).inGuestMode()) {
+            Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, method, "guest");
+        } else if (Restrictions.isRestrictedProfile(this)) {
+            Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, method, "restricted");
         }
-        if (ACTION_HOMESCREEN_SHORTCUT.equals(action)) {
-            return StartupAction.SHORTCUT;
-        }
-        return (passedURL == null ? StartupAction.NORMAL : StartupAction.URL);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -136,25 +136,16 @@ public abstract class GeckoApp
     NativeEventListener,
     SensorEventListener,
     Tabs.OnTabsChangedListener,
     ViewTreeObserver.OnGlobalLayoutListener {
 
     private static final String LOGTAG = "GeckoApp";
     private static final int ONE_DAY_MS = 1000*60*60*24;
 
-    public enum StartupAction {
-        NORMAL,     /* normal application start */
-        URL,        /* launched with a passed URL */
-        PREFETCH,   /* launched with a passed URL that we prefetch */
-        GUEST,      /* launched in guest browsing */
-        RESTRICTED, /* launched with restricted profile */
-        SHORTCUT    /* launched from a homescreen shortcut */
-    }
-
     public static final String ACTION_ALERT_CALLBACK       = "org.mozilla.gecko.ACTION_ALERT_CALLBACK";
     public static final String ACTION_HOMESCREEN_SHORTCUT  = "org.mozilla.gecko.BOOKMARK";
     public static final String ACTION_DEBUG                = "org.mozilla.gecko.DEBUG";
     public static final String ACTION_LAUNCH_SETTINGS      = "org.mozilla.gecko.SETTINGS";
     public static final String ACTION_LOAD                 = "org.mozilla.gecko.LOAD";
     public static final String ACTION_INIT_PW              = "org.mozilla.gecko.INIT_PW";
 
     public static final String EXTRA_STATE_BUNDLE          = "stateBundle";
@@ -1532,18 +1523,17 @@ public abstract class GeckoApp
         }
 
         // If we're not restoring, move the session file so it can be read for
         // the last tabs section.
         if (!mShouldRestore) {
             getProfile().moveSessionFile();
         }
 
-        final StartupAction startupAction = getStartupAction(passedUri, action);
-        Telemetry.addToHistogram("FENNEC_GECKOAPP_STARTUP_ACTION", startupAction.ordinal());
+        recordStartupActionTelemetry(passedUri, action);
 
         // Check if launched from data reporting notification.
         if (ACTION_LAUNCH_SETTINGS.equals(action)) {
             Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class);
             // Copy extras.
             settingsIntent.putExtras(intent.getUnsafe());
             startActivity(settingsIntent);
         }
@@ -1940,18 +1930,17 @@ public abstract class GeckoApp
         } else if (ACTION_LAUNCH_SETTINGS.equals(action)) {
             // Check if launched from data reporting notification.
             Intent settingsIntent = new Intent(GeckoApp.this, GeckoPreferences.class);
             // Copy extras.
             settingsIntent.putExtras(intent.getUnsafe());
             startActivity(settingsIntent);
         }
 
-        final StartupAction startupAction = getStartupAction(passedUri, action);
-        Telemetry.addToHistogram("FENNEC_GECKOAPP_STARTUP_ACTION", startupAction.ordinal());
+        recordStartupActionTelemetry(passedUri, action);
     }
 
     /**
      * Handles getting a URI from an intent in a way that is backwards-
      * compatible with our previous implementations.
      */
     protected String getURIFromIntent(SafeIntent intent) {
         final String action = intent.getAction();
@@ -2757,19 +2746,17 @@ public abstract class GeckoApp
                                                   final EventDispatcher dispatcher,
                                                   final String osLocale,
                                                   final String appLocale,
                                                   final SessionInformation previousSession) {
         // GeckoApp does not need to record any health information - return a stub.
         return new StubbedHealthRecorder();
     }
 
-    protected StartupAction getStartupAction(final String passedURL, final String action) {
-        // Default to NORMAL here. Subclasses can handle the other types.
-        return StartupAction.NORMAL;
+    protected void recordStartupActionTelemetry(final String passedURL, final String action) {
     }
 
     @Override
     public void checkUriVisited(String uri) {
         GlobalHistory.getInstance().checkUriVisited(uri);
     }
 
     @Override
--- a/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueuePrompt.java
+++ b/mobile/android/base/java/org/mozilla/gecko/tabqueue/TabQueuePrompt.java
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.tabqueue;
 
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.Locales;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
 
 import android.annotation.TargetApi;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
 import android.provider.Settings;
@@ -44,30 +45,28 @@ public class TabQueuePrompt extends Loca
         super.onCreate(savedInstanceState);
 
         showTabQueueEnablePrompt();
     }
 
     private void showTabQueueEnablePrompt() {
         setContentView(R.layout.tab_queue_prompt);
 
-        final int numberOfTimesTabQueuePromptSeen = GeckoSharedPrefs.forApp(this).getInt(TabQueueHelper.PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, 0);
-
         final View okButton = findViewById(R.id.ok_button);
         okButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
                 onConfirmButtonPressed();
-                Telemetry.addToHistogram("FENNEC_TABQUEUE_PROMPT_ENABLE_YES", numberOfTimesTabQueuePromptSeen);
+                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "tabqueue_prompt_yes");
             }
         });
         findViewById(R.id.cancel_button).setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
-                Telemetry.addToHistogram("FENNEC_TABQUEUE_PROMPT_ENABLE_NO", numberOfTimesTabQueuePromptSeen);
+                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "tabqueue_prompt_no");
                 setResult(TabQueueHelper.TAB_QUEUE_NO);
                 finish();
             }
         });
         final View settingsButton = findViewById(R.id.settings_button);
         settingsButton.setOnClickListener(new View.OnClickListener() {
             @Override
             public void onClick(View v) {
@@ -163,18 +162,17 @@ public class TabQueuePrompt extends Loca
     @Override
     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
         if (requestCode != SETTINGS_REQUEST_CODE) {
             return;
         }
 
         if (TabQueueHelper.canDrawOverlays(this)) {
             // User granted the permission in Android's settings.
-            final int numberOfTimesTabQueuePromptSeen = GeckoSharedPrefs.forApp(this).getInt(TabQueueHelper.PREF_TAB_QUEUE_TIMES_PROMPT_SHOWN, 0);
-            Telemetry.addToHistogram("FENNEC_TABQUEUE_PROMPT_ENABLE_YES", numberOfTimesTabQueuePromptSeen);
+            Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, "tabqueue_prompt_yes");
 
             setResult(TabQueueHelper.TAB_QUEUE_YES);
             finish();
         }
     }
 
     /**
      * Slide the overlay down off the screen and destroy it.
--- a/mobile/android/chrome/content/MemoryObserver.js
+++ b/mobile/android/chrome/content/MemoryObserver.js
@@ -38,42 +38,42 @@ var MemoryObserver = {
     defaults.setIntPref("browser.sessionhistory.max_total_viewers", 0);
   },
 
   zombify: function(tab) {
     let browser = tab.browser;
     let data = browser.__SS_data;
     let extra = browser.__SS_extdata;
 
-    // Destroying the tab will stop audio playback without invoking the
-    // normal events, therefore we need to explicitly tell the Java UI
-    // to stop displaying the audio playing indicator.
-    if (tab.playingAudio) {
-      Messaging.sendRequest({
-        type: "Tab:AudioPlayingChange",
-        tabID: tab.id,
-        isAudioPlaying: false
-      });
-    }
+    // Notify any interested parties (e.g. the session store)
+    // that the original tab object is going to be destroyed
+    let evt = document.createEvent("UIEvents");
+    evt.initUIEvent("TabPreZombify", true, false, window, null);
+    browser.dispatchEvent(evt);
 
     // We need this data to correctly create and position the new browser
     // If this browser is already a zombie, fallback to the session data
     let currentURL = browser.__SS_restore ? data.entries[0].url : browser.currentURI.spec;
     let sibling = browser.nextSibling;
     let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(browser);
 
     tab.destroy();
     tab.create(currentURL, { sibling: sibling, zombifying: true, delayLoad: true, isPrivate: isPrivate });
 
     // Reattach session store data and flag this browser so it is restored on select
     browser = tab.browser;
     browser.__SS_data = data;
     browser.__SS_extdata = extra;
     browser.__SS_restore = true;
     browser.setAttribute("pending", "true");
+
+    // Notify the session store to reattach its listeners to the new tab object
+    evt = document.createEvent("UIEvents");
+    evt.initUIEvent("TabPostZombify", true, false, window, null);
+    browser.dispatchEvent(evt);
   },
 
   gc: function() {
     window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).garbageCollect();
     Cu.forceGC();
   },
 
   dumpMemoryStats: function(aLabel) {
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -3460,16 +3460,17 @@ Tab.prototype = {
     this.browser.addEventListener("DOMWindowClose", this, true);
     this.browser.addEventListener("DOMWillOpenModalDialog", this, true);
     this.browser.addEventListener("DOMAutoComplete", this, true);
     this.browser.addEventListener("blur", this, true);
     this.browser.addEventListener("scroll", this, true);
     this.browser.addEventListener("MozScrolledAreaChanged", this, true);
     this.browser.addEventListener("pageshow", this, true);
     this.browser.addEventListener("MozApplicationManifest", this, true);
+    this.browser.addEventListener("TabPreZombify", this, true);
 
     // Note that the XBL binding is untrusted
     this.browser.addEventListener("PluginBindingAttached", this, true, true);
     this.browser.addEventListener("VideoBindingAttached", this, true, true);
     this.browser.addEventListener("VideoBindingCast", this, true, true);
 
     Services.obs.addObserver(this, "before-first-paint", false);
 
@@ -3567,16 +3568,17 @@ Tab.prototype = {
     this.browser.removeEventListener("DOMWindowClose", this, true);
     this.browser.removeEventListener("DOMWillOpenModalDialog", this, true);
     this.browser.removeEventListener("DOMAutoComplete", this, true);
     this.browser.removeEventListener("blur", this, true);
     this.browser.removeEventListener("scroll", this, true);
     this.browser.removeEventListener("MozScrolledAreaChanged", this, true);
     this.browser.removeEventListener("pageshow", this, true);
     this.browser.removeEventListener("MozApplicationManifest", this, true);
+    this.browser.removeEventListener("TabPreZombify", this, true);
 
     this.browser.removeEventListener("PluginBindingAttached", this, true, true);
     this.browser.removeEventListener("VideoBindingAttached", this, true, true);
     this.browser.removeEventListener("VideoBindingCast", this, true, true);
 
     Services.obs.removeObserver(this, "before-first-paint");
 
     // Make sure the previously selected panel remains selected. The selected panel of a deck is
@@ -4090,17 +4092,18 @@ Tab.prototype = {
           type: "DOMTitleChanged",
           tabID: this.id,
           title: truncate(aEvent.target.title, MAX_TITLE_LENGTH)
         });
         break;
       }
 
       case "DOMAudioPlaybackStarted":
-      case "DOMAudioPlaybackStopped": {
+      case "DOMAudioPlaybackStopped":
+      case "TabPreZombify": {
         if (!Services.prefs.getBoolPref("browser.tabs.showAudioPlayingIcon") ||
             !aEvent.isTrusted) {
           return;
         }
 
         let browser = aEvent.originalTarget;
         if (browser != this.browser) {
           return;
--- a/mobile/android/components/SessionStore.js
+++ b/mobile/android/components/SessionStore.js
@@ -208,16 +208,26 @@ SessionStore.prototype = {
         break;
       }
       case "TabClose": {
         let browser = aEvent.target;
         this.onTabClose(window, browser, aEvent.detail);
         this.onTabRemove(window, browser);
         break;
       }
+      case "TabPreZombify": {
+        let browser = aEvent.target;
+        this.onTabRemove(window, browser, true);
+        break;
+      }
+      case "TabPostZombify": {
+        let browser = aEvent.target;
+        this.onTabAdd(window, browser, true);
+        break;
+      }
       case "TabSelect": {
         let browser = aEvent.target;
         this.onTabSelect(window, browser);
         break;
       }
       case "DOMTitleChanged": {
         let browser = aEvent.currentTarget;
 
@@ -272,32 +282,36 @@ SessionStore.prototype = {
       this._lastSaveTime = Date.now();
     }
 
     // Add tab change listeners to all already existing tabs
     let tabs = aWindow.BrowserApp.tabs;
     for (let i = 0; i < tabs.length; i++)
       this.onTabAdd(aWindow, tabs[i].browser, true);
 
-    // Notification of tab add/remove/selection
+    // Notification of tab add/remove/selection/zombification
     let browsers = aWindow.document.getElementById("browsers");
     browsers.addEventListener("TabOpen", this, true);
     browsers.addEventListener("TabClose", this, true);
     browsers.addEventListener("TabSelect", this, true);
+    browsers.addEventListener("TabPreZombify", this, true);
+    browsers.addEventListener("TabPostZombify", this, true);
   },
 
   onWindowClose: function ss_onWindowClose(aWindow) {
     // Ignore windows not tracked by SessionStore
     if (!aWindow.__SSID || !this._windows[aWindow.__SSID])
       return;
 
     let browsers = aWindow.document.getElementById("browsers");
     browsers.removeEventListener("TabOpen", this, true);
     browsers.removeEventListener("TabClose", this, true);
     browsers.removeEventListener("TabSelect", this, true);
+    browsers.removeEventListener("TabPreZombify", this, true);
+    browsers.removeEventListener("TabPostZombify", this, true);
 
     if (this._loadState == STATE_RUNNING) {
       // Update all window data for a last time
       this._collectWindowData(aWindow);
 
       // Clear this window from the list
       delete this._windows[aWindow.__SSID];
 
@@ -967,16 +981,18 @@ SessionStore.prototype = {
       let tabData = JSON.parse(aData.tabs[i]);
       let params = {
         selected: (i == aData.tabs.length - 1),
         isPrivate: tabData.isPrivate,
         desktopMode: tabData.desktopMode,
       };
 
       let tab = window.BrowserApp.addTab(tabData.entries[tabData.index - 1].url, params);
+      tab.browser.__SS_data = tabData;
+      tab.browser.__SS_extdata = tabData.extData;
       this._restoreTab(tabData, tab.browser);
     }
   },
 
   /**
    * Don't save sensitive data if the user doesn't want to
    * (distinguishes between encrypted and non-encrypted sites)
    */
@@ -986,22 +1002,28 @@ SessionStore.prototype = {
     return Services.prefs.getIntPref(pref) < (isHTTPS ? PRIVACY_ENCRYPTED : PRIVACY_FULL);
   },
 
   /**
   * Starts the restoration process for a browser. History is restored at this
   * point, but text data must be delayed until the content loads.
   */
   _restoreTab: function ss_restoreTab(aTabData, aBrowser) {
+    // aTabData shouldn't be empty here, but if it is,
+    // _restoreHistory() will crash otherwise.
+    if (!aTabData || aTabData.entries.length == 0) {
+      Cu.reportError("SessionStore.js: Error trying to restore tab with empty tabdata");
+      return;
+    }
     this._restoreHistory(aTabData, aBrowser.sessionHistory);
 
     // Restoring the text data requires waiting for the content to load. So
     // we set a flag and delay this until the "load" event.
     //this._restoreTextData(aTabData, aBrowser);
-    aBrowser.__SS_restore_data = aTabData || {};
+    aBrowser.__SS_restore_data = aTabData;
   },
 
   /**
   * Takes serialized history data and create news entries into the given
   * nsISessionHistory object.
   */
   _restoreHistory: function ss_restoreHistory(aTabData, aHistory) {
     if (aHistory.count > 0) {
@@ -1089,29 +1111,29 @@ SessionStore.prototype = {
         delete tabData.tabId;
 
         // Don't restore tab if user has closed it
         if (tab == null) {
           continue;
         }
       }
 
+      tab.browser.__SS_data = tabData;
+      tab.browser.__SS_extdata = tabData.extData;
+
       if (window.BrowserApp.selectedTab == tab) {
         this._restoreTab(tabData, tab.browser);
 
         delete tab.browser.__SS_restore;
         tab.browser.removeAttribute("pending");
       } else {
-        // Make sure the browser has its session data for the delay reload
-        tab.browser.__SS_data = tabData;
+        // Mark the browser for delay loading
         tab.browser.__SS_restore = true;
         tab.browser.setAttribute("pending", "true");
       }
-
-      tab.browser.__SS_extdata = tabData.extData;
     }
 
     // Restore the closed tabs array on the current window.
     if (state.windows[0].closedTabs) {
       this._windows[window.__SSID].closedTabs = state.windows[0].closedTabs;
     }
   },
 
@@ -1148,23 +1170,22 @@ SessionStore.prototype = {
     // create a new tab and bring to front
     let params = {
       selected: true,
       isPrivate: aCloseTabData.isPrivate,
       desktopMode: aCloseTabData.desktopMode,
       tabIndex: this._lastClosedTabIndex
     };
     let tab = aWindow.BrowserApp.addTab(aCloseTabData.entries[aCloseTabData.index - 1].url, params);
+    tab.browser.__SS_data = aCloseTabData;
+    tab.browser.__SS_extdata = aCloseTabData.extData;
     this._restoreTab(aCloseTabData, tab.browser);
 
     this._lastClosedTabIndex = -1;
 
-    // Put back the extra data
-    tab.browser.__SS_extdata = aCloseTabData.extData;
-
     if (this._notifyClosedTabs) {
       this._sendClosedTabsToJava(aWindow);
     }
 
     return tab.browser;
   },
 
   forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
--- a/mobile/android/tests/browser/chrome/chrome.ini
+++ b/mobile/android/tests/browser/chrome/chrome.ini
@@ -33,12 +33,13 @@ skip-if = debug
 [test_network_manager.html]
 [test_offline_page.html]
 skip-if = true # Bug 1241478
 [test_reader_view.html]
 [test_resource_substitutions.html]
 [test_restricted_profiles.html]
 [test_selectoraddtab.html]
 [test_session_form_data.html]
+[test_session_zombification.html]
 [test_shared_preferences.html]
 [test_simple_discovery.html]
 [test_video_discovery.html]
 [test_web_channel.html]
--- a/mobile/android/tests/browser/chrome/head.js
+++ b/mobile/android/tests/browser/chrome/head.js
@@ -14,16 +14,29 @@ function promiseBrowserEvent(browser, ev
       resolve(event);
     }
 
     browser.addEventListener(eventType, handle, true);
     info("Now waiting for " + eventType + " event from browser");
   });
 }
 
+function promiseTabEvent(container, eventType) {
+  return new Promise((resolve) => {
+    function handle(event) {
+      info("Received event " + eventType + " from container");
+      container.removeEventListener(eventType, handle, true);
+      resolve(event);
+    }
+
+    container.addEventListener(eventType, handle, true);
+    info("Now waiting for " + eventType + " event from container");
+  });
+}
+
 function promiseNotification(topic) {
   Cu.import("resource://gre/modules/Services.jsm");
 
   return new Promise((resolve, reject) => {
     function observe(subject, topic, data) {
       info("Received " + topic + " notification from Gecko");
       Services.obs.removeObserver(observe, topic);
       resolve();
--- a/mobile/android/tests/browser/chrome/test_selectoraddtab.html
+++ b/mobile/android/tests/browser/chrome/test_selectoraddtab.html
@@ -16,29 +16,16 @@ https://bugzilla.mozilla.org/show_bug.cg
   "use strict";
 
   const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
   Cu.import("resource://gre/modules/Services.jsm");
   Cu.import("resource://gre/modules/Messaging.jsm");
   Cu.import("resource://gre/modules/Task.jsm");
 
-  function promiseTabEvent(container, eventType) {
-    return new Promise((resolve) => {
-      function handle(event) {
-        info("Received event " + eventType + " from container");
-        container.removeEventListener(eventType, handle, true);
-        resolve(event);
-      }
-
-      container.addEventListener(eventType, handle, true);
-      info("Now waiting for " + eventType + " event from container");
-    });
-  }
-
   // The chrome window
   let chromeWin;
 
   // Track the <browser>s where the tests are happening
   let browserBlank;
   let browserTest;
 
   const kTestPage = "http://mochi.test:8888/chrome/mobile/android/tests/browser/chrome/basic_article.html";
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/browser/chrome/test_session_zombification.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1044556
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug 1044556</title>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://global/skin"/>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"/>
+  <script type="application/javascript" src="head.js"></script>
+  <script type="application/javascript">
+
+  /** Test for Bug 1044556 **/
+
+  "use strict";
+
+  const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+  Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+  Cu.import("resource://gre/modules/Services.jsm");
+  Cu.import("resource://gre/modules/Messaging.jsm");
+  Cu.import("resource://gre/modules/Task.jsm");
+
+  // Import the MemoryObserver
+  Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+  XPCOMUtils.defineLazyGetter(this, "MemoryObserver", function() {
+    let sandbox = {};
+    Services.scriptloader.loadSubScript("chrome://browser/content/MemoryObserver.js", sandbox);
+    return sandbox["MemoryObserver"];
+  });
+
+  // The chrome window
+  let chromeWin;
+
+  // Track the tabs where the tests are happening
+  let tabBlank;
+  let tabTest;
+
+  const url1 = "data:text/html;charset=utf-8,It%20was%20a%20dark%20and%20stormy%20night.";
+  const url2 = "data:text/html;charset=utf-8,Suddenly%2C%20a%20tab%20was%20zombified.";
+
+  add_task(function* test_sessionStoreZombify() {
+    chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
+    let BrowserApp = chromeWin.BrowserApp;
+
+    SimpleTest.registerCleanupFunction(function() {
+      BrowserApp.closeTab(tabBlank);
+      BrowserApp.closeTab(tabTest);
+    });
+
+    // Add a new tab with some content
+    tabTest = BrowserApp.addTab(url1 , { selected: true, parentId: BrowserApp.selectedTab.id });
+    yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+
+    // Add a new tab with a blank page
+    tabBlank = BrowserApp.addTab("about:blank", { selected: true, parentId: BrowserApp.selectedTab.id });
+    is(BrowserApp.selectedTab, tabBlank, "Test tab is in background.");
+
+    // Zombify the backgrounded test tab
+    MemoryObserver.zombify(tabTest);
+
+    // Check that the test tab is actually zombified
+    ok(tabTest.browser.__SS_restore, "Test tab is set for delay loading.");
+
+    // Switch back to the test tab and wait for it to reload
+    BrowserApp.selectTab(tabTest);
+    yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+
+    // Check that the test tab has loaded the correct url
+    is(tabTest.browser.currentURI.spec, url1, "Test tab is showing the first URL.");
+
+    // Navigate to some other content
+    BrowserApp.loadURI(url2, tabTest.browser);
+    yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+    is(tabTest.browser.currentURI.spec, url2, "Test tab is showing the second URL.");
+
+    // Switch to the other tab
+    BrowserApp.selectTab(tabBlank);
+    yield promiseTabEvent(BrowserApp.deck, "TabSelect");
+    is(BrowserApp.selectedTab, tabBlank, "Test tab is in background.");
+
+    // Zombify the backgrounded test tab again
+    MemoryObserver.zombify(tabTest);
+
+    // Check that the test tab is actually zombified
+    ok(tabTest.browser.__SS_restore, "Test tab is set for delay loading.");
+
+    // Switch back to the test tab and wait for it to reload
+    BrowserApp.selectTab(tabTest);
+    yield promiseBrowserEvent(tabTest.browser, "DOMContentLoaded");
+
+    // Check that the test tab has loaded the correct url
+    is(tabTest.browser.currentURI.spec, url2, "Test tab is showing the second URL.");
+  });
+
+  </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1044556">Mozilla Bug 1044556</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -666,44 +666,26 @@ ExtensionData.prototype = {
           resolve(JSON.parse(text));
         } catch (e) {
           reject(e);
         }
       });
     });
   },
 
-  // Reads the extension's |manifest.json| file and optional |mozilla.json|,
-  // and stores the parsed and merged contents in |this.manifest|.
+  // Reads the extension's |manifest.json| file, and stores its
+  // parsed contents in |this.manifest|.
   readManifest() {
     return Promise.all([
       this.readJSON("manifest.json"),
-      this.readJSON("mozilla.json").catch(err => null),
       Management.lazyInit(),
-    ]).then(([manifest, mozManifest]) => {
+    ]).then(([manifest]) => {
       this.manifest = manifest;
       this.rawManifest = manifest;
 
-      if (mozManifest) {
-        if (typeof mozManifest != "object") {
-          this.logger.warn(`Loading extension '${this.id}': mozilla.json has unexpected type ${typeof mozManifest}`);
-        } else {
-          Object.keys(mozManifest).forEach(key => {
-            if (key != "applications") {
-              throw new Error(`Illegal property "${key}" in mozilla.json`);
-            }
-            if (key in manifest) {
-              this.logger.warn(`Ignoring property "${key}" from mozilla.json`);
-            } else {
-              manifest[key] = mozManifest[key];
-            }
-          });
-        }
-      }
-
       if (manifest && manifest.default_locale) {
         return this.initLocale();
       }
     }).then(() => {
       let context = {
         url: this.baseURI && this.baseURI.spec,
 
         principal: this.principal,
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -115,17 +115,17 @@ var api = context => {
         return context.lastError;
       },
 
       inIncognitoContext: PrivateBrowsingUtils.isContentWindowPrivate(context.contentWindow),
     },
 
     i18n: {
       getMessage: function(messageName, substitutions) {
-        return context.extension.localizeMessage(messageName, substitutions);
+        return context.extension.localizeMessage(messageName, substitutions, {cloneScope: context.cloneScope});
       },
 
       getAcceptLanguages: function(callback) {
         let result = context.extension.localeData.acceptLanguages;
         return context.wrapPromise(Promise.resolve(result), callback);
       },
 
       getUILanguage: function() {
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -312,16 +312,17 @@ class BaseContext {
     }
   }
 }
 
 function LocaleData(data) {
   this.defaultLocale = data.defaultLocale;
   this.selectedLocale = data.selectedLocale;
   this.locales = data.locales || new Map();
+  this.warnedMissingKeys = new Set();
 
   // Map(locale-name -> Map(message-key -> localized-string))
   //
   // Contains a key for each loaded locale, each of which is a
   // Map of message keys to their localized strings.
   this.messages = data.messages || new Map();
 
   if (data.builtinMessages) {
@@ -343,18 +344,26 @@ LocaleData.prototype = {
 
   BUILTIN: "@@BUILTIN_MESSAGES",
 
   has(locale) {
     return this.messages.has(locale);
   },
 
   // https://developer.chrome.com/extensions/i18n
-  localizeMessage(message, substitutions = [], locale = this.selectedLocale, defaultValue = "??") {
-    let locales = new Set([this.BUILTIN, locale, this.defaultLocale]
+  localizeMessage(message, substitutions = [], options = {}) {
+    let defaultOptions = {
+      locale: this.selectedLocale,
+      defaultValue: "",
+      cloneScope: null,
+    };
+
+    options = Object.assign(defaultOptions, options);
+
+    let locales = new Set([this.BUILTIN, options.locale, this.defaultLocale]
                           .filter(locale => this.messages.has(locale)));
 
     // Message names are case-insensitive, so normalize them to lower-case.
     message = message.toLowerCase();
     for (let locale of locales) {
       let messages = this.messages.get(locale);
       if (messages.has(message)) {
         let str = messages.get(message);
@@ -393,33 +402,40 @@ LocaleData.prototype = {
         return rtl ? "ltr" : "rtl";
       } else if (message == "@@bidi_start_edge") {
         return rtl ? "right" : "left";
       } else if (message == "@@bidi_end_edge") {
         return rtl ? "left" : "right";
       }
     }
 
-    Cu.reportError(`Unknown localization message ${message}`);
-    return defaultValue;
+    if (!this.warnedMissingKeys.has(message)) {
+      let error = `Unknown localization message ${message}`;
+      if (options.cloneScope) {
+        error = new options.cloneScope.Error(error);
+      }
+      Cu.reportError(error);
+      this.warnedMissingKeys.add(message);
+    }
+    return options.defaultValue;
   },
 
   // Localize a string, replacing all |__MSG_(.*)__| tokens with the
   // matching string from the current locale, as determined by
   // |this.selectedLocale|.
   //
   // This may not be called before calling either |initLocale| or
   // |initAllLocales|.
   localize(str, locale = this.selectedLocale) {
     if (!str) {
       return str;
     }
 
     return str.replace(/__MSG_([A-Za-z0-9@_]+?)__/g, (matched, message) => {
-      return this.localizeMessage(message, [], locale, matched);
+      return this.localizeMessage(message, [], {locale, defaultValue: matched});
     });
   },
 
   // Validates the contents of a locale JSON file, normalizes the
   // messages into a Map of message key -> localized string pairs.
   addLocale(locale, messages, extension) {
     let result = new Map();
 
--- a/toolkit/components/extensions/ext-i18n.js
+++ b/toolkit/components/extensions/ext-i18n.js
@@ -6,17 +6,17 @@ Cu.import("resource://gre/modules/Extens
 var {
   detectLanguage,
 } = ExtensionUtils;
 
 extensions.registerSchemaAPI("i18n", null, (extension, context) => {
   return {
     i18n: {
       getMessage: function(messageName, substitutions) {
-        return extension.localizeMessage(messageName, substitutions);
+        return extension.localizeMessage(messageName, substitutions, {cloneScope: context.cloneScope});
       },
 
       getAcceptLanguages: function() {
         let result = extension.localeData.acceptLanguages;
         return Promise.resolve(result);
       },
 
       getUILanguage: function() {
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -29,16 +29,17 @@ function WebRequestEventManager(context,
       let tabId = TabManager.getBrowserId(data.browser);
       if (tabId == -1) {
         return;
       }
 
       let data2 = {
         requestId: data.requestId,
         url: data.url,
+        originUrl: data.originUrl,
         method: data.method,
         type: data.type,
         timeStamp: Date.now(),
         frameId: ExtensionManagement.getFrameId(data.windowId),
         parentFrameId: ExtensionManagement.getParentFrameId(data.parentWindowId, data.windowId),
       };
 
       if ("ip" in data) {
--- a/toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
+++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_page1.html
@@ -23,10 +23,21 @@
 
 <script src="nonexistent_script_url.js"></script>
 
 <iframe src="file_WebRequest_page2.html" width="200" height="200"></iframe>
 <iframe src="redirection.sjs" width="200" height="200"></iframe>
 <iframe src="data:text/plain,webRequestTest" width="200" height="200"></iframe>
 <iframe src="data:text/plain,webRequestTest_bad" width="200" height="200"></iframe>
 <iframe src="https://invalid.localhost/" width="200" height="200"></iframe>
+<a href="file_WebRequest_page3.html?trigger=a" target="webrequest_link">link</a>
+<form method="post" action="file_WebRequest_page3.html?trigger=form" target="webrequest_form"></form>
+<script>
+"use strict";
+for (let a of document.links) {
+  a.click();
+}
+for (let f of document.forms) {
+  f.submit();
+}
+</script>
 </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/mochitest/file_WebRequest_page3.html
@@ -0,0 +1,11 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<meta charset="utf-8">
+<script>
+"use strict";
+window.close();
+</script>
+</head>
+</html>
--- a/toolkit/components/extensions/test/mochitest/mochitest.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest.ini
@@ -1,14 +1,15 @@
 [DEFAULT]
 skip-if = buildapp == 'mulet' || asan
 support-files =
   head.js
   file_WebRequest_page1.html
   file_WebRequest_page2.html
+  file_WebRequest_page3.html
   file_WebNavigation_page1.html
   file_WebNavigation_page2.html
   file_WebNavigation_page3.html
   file_image_good.png
   file_image_bad.png
   file_image_redirect.png
   file_style_good.css
   file_style_bad.css
--- a/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_i18n.html
@@ -23,20 +23,20 @@ add_task(function* test_i18n() {
 
     let url = browser.runtime.getURL("/");
     assertEq(url, `moz-extension://${_("@@extension_id")}/`, "@@extension_id builtin message");
 
     assertEq("Foo.", _("Foo"), "Simple message in selected locale.");
 
     assertEq("(bar)", _("bar"), "Simple message fallback in default locale.");
 
-    assertEq("??", _("some-unknown-locale-string"), "Unknown locale string.");
+    assertEq("", _("some-unknown-locale-string"), "Unknown locale string.");
 
-    assertEq("??", _("@@unknown_builtin_string"), "Unknown built-in string.");
-    assertEq("??", _("@@bidi_unknown_builtin_string"), "Unknown built-in bidi string.");
+    assertEq("", _("@@unknown_builtin_string"), "Unknown built-in string.");
+    assertEq("", _("@@bidi_unknown_builtin_string"), "Unknown built-in bidi string.");
 
     assertEq("Føo.", _("Föo"), "Multi-byte message in selected locale.");
 
     let substitutions = [];
     substitutions[4] = "5";
     substitutions[13] = "14";
 
     assertEq("'$0' '14' '' '5' '$$$$' '$'.", _("basic_substitutions", substitutions),
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
@@ -93,33 +93,40 @@ function compareLists(list1, list2, kind
   is(String(list1), String(list2), `${kind} URLs correct`);
 }
 
 function backgroundScript() {
   let checkCompleted = true;
   let savedTabId = -1;
 
   function shouldRecord(url) {
-    return url.startsWith(BASE) || /^data:.*\bwebRequestTest|\/invalid\./.test(url);
+    return url.startsWith(BASE) && !url.includes("_page3.html") ||
+           /^data:.*\bwebRequestTest|\/invalid\./.test(url);
   }
 
   let statuses = [
     {url: /_script_good\b/, code: 200, line: /^HTTP\/1.1 200 OK\b/i},
     {url: /\bredirection\b/, code: 302, line: /^HTTP\/1.1 302\b/},
     {url: /\bnonexistent_script_/, code: 404, line: /^HTTP\/1.1 404 Not Found\b/i},
   ];
   function checkStatus(details) {
     for (let {url, code, line} of statuses) {
       if (url.test(details.url)) {
         browser.test.assertEq(code, details.statusCode, `HTTP status code ${code} for ${details.url} (found ${details.statusCode})`);
         browser.test.assertTrue(line.test(details.statusLine), `HTTP status line ${line} for ${details.url} (found ${details.statusLine})`);
       }
     }
   }
 
+  function checkOrigin(details) {
+    let isCorrectOrigin = details.url.includes("_page1.html") ? details.originUrl.endsWith("/test_ext_webrequest.html")
+                                                              : /\/file_WebRequest_page\d\.html\b/.test(details.originUrl);
+    browser.test.assertTrue(isCorrectOrigin, `originUrl for ${details.url} is correct (${details.originUrl})`);
+  }
+
   function checkType(details) {
     let expected_type = "???";
     if (details.url.includes("style")) {
       expected_type = "stylesheet";
     } else if (details.url.includes("image")) {
       expected_type = "image";
     } else if (details.url.includes("script")) {
       expected_type = "script";
@@ -275,16 +282,17 @@ function backgroundScript() {
 
     let ids = requestIDs.get(details.url);
     if (ids) {
       ids.add(details.requestId);
     } else {
       requestIDs.set(details.url, new Set([details.requestId]));
     }
     checkResourceType(details.type);
+    checkOrigin(details);
     if (shouldRecord(details.url)) {
       recorded.requested.push(details.url);
 
       if (savedTabId == -1) {
         browser.test.assertTrue(details.tabId !== undefined, "tab ID defined");
         savedTabId = details.tabId;
       }
 
@@ -306,16 +314,17 @@ function backgroundScript() {
       return {cancel: true};
     }
     return {};
   }
 
   function onBeforeSendHeaders(details) {
     browser.test.log(`onBeforeSendHeaders ${details.url}`);
     checkRequestId(details);
+    checkOrigin(details);
     checkResourceType(details.type);
     processHeaders("request", details);
     if (shouldRecord(details.url)) {
       recorded.beforeSendHeaders.push(details.url);
 
       browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
       checkType(details);
 
@@ -326,16 +335,17 @@ function backgroundScript() {
       return {redirectUrl: details.url.replace("_redirect.", "_good.")};
     }
     return {requestHeaders: details.requestHeaders};
   }
 
   function onBeforeRedirect(details) {
     browser.test.log(`onBeforeRedirect ${details.url} -> ${details.redirectUrl}`);
     checkRequestId(details, "redirect");
+    checkOrigin(details);
     checkResourceType(details.type);
     if (shouldRecord(details.url)) {
       recorded.beforeRedirect.push(details.url);
 
       browser.test.assertEq(details.tabId, savedTabId, "correct tab ID");
       checkType(details);
       checkStatus(details);
 
@@ -349,16 +359,17 @@ function backgroundScript() {
     }
     return {};
   }
 
   function onRecord(kind, details) {
     browser.test.log(`${kind} ${details.requestId} ${details.url}`);
     checkResourceType(details.type);
     checkRequestId(details, kind);
+    checkOrigin(details);
     if (kind in recorded && shouldRecord(details.url)) {
       recorded[kind].push(details.url);
     }
   }
 
   function onSendHeaders(details) {
     onRecord("sendHeaders", details);
     checkHeaders("request", details);
--- a/toolkit/components/maintenanceservice/workmonitor.cpp
+++ b/toolkit/components/maintenanceservice/workmonitor.cpp
@@ -25,17 +25,17 @@
 #include "errors.h"
 
 // Wait 15 minutes for an update operation to run at most.
 // Updates usually take less than a minute so this seems like a
 // significantly large and safe amount of time to wait.
 static const int TIME_TO_WAIT_ON_UPDATER = 15 * 60 * 1000;
 wchar_t* MakeCommandLine(int argc, wchar_t** argv);
 BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode);
-BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer,  LPCWSTR siblingFilePath, 
+BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer,  LPCWSTR siblingFilePath,
                             LPCWSTR newFileName);
 
 /*
  * Read the update.status file and sets isApplying to true if
  * the status is set to applying.
  *
  * @param  updateDirPath The directory where update.status is stored
  * @param  isApplying Out parameter for specifying if the status
@@ -475,89 +475,16 @@ ProcessSoftwareUpdateCommand(DWORD argc,
                 GetLastError()));
     }
   }
 
   return result;
 }
 
 /**
- * Obtains the updater path alongside a subdir of the service binary.
- * The purpose of this function is to return a path that is likely high
- * integrity and therefore more safe to execute code from.
- *
- * @param serviceUpdaterPath Out parameter for the path where the updater
- *                           should be copied to.
- * @return TRUE if a file path was obtained.
- */
-BOOL
-GetSecureUpdaterPath(WCHAR serviceUpdaterPath[MAX_PATH + 1])
-{
-  if (!GetModuleFileNameW(nullptr, serviceUpdaterPath, MAX_PATH)) {
-    LOG_WARN(("Could not obtain module filename when attempting to "
-              "use a secure updater path.  (%d)", GetLastError()));
-    return FALSE;
-  }
-
-  if (!PathRemoveFileSpecW(serviceUpdaterPath)) {
-    LOG_WARN(("Couldn't remove file spec when attempting to use a secure "
-              "updater path.  (%d)", GetLastError()));
-    return FALSE;
-  }
-
-  if (!PathAppendSafe(serviceUpdaterPath, L"update")) {
-    LOG_WARN(("Couldn't append file spec when attempting to use a secure "
-              "updater path.  (%d)", GetLastError()));
-    return FALSE;
-  }
-
-  CreateDirectoryW(serviceUpdaterPath, nullptr);
-
-  if (!PathAppendSafe(serviceUpdaterPath, L"updater.exe")) {
-    LOG_WARN(("Couldn't append file spec when attempting to use a secure "
-              "updater path.  (%d)", GetLastError()));
-    return FALSE;
-  }
-
-  return TRUE;
-}
-
-/**
- * Deletes the passed in updater path and the associated updater.ini file.
- *
- * @param serviceUpdaterPath The path to delete.
- * @return TRUE if a file was deleted.
- */
-BOOL
-DeleteSecureUpdater(WCHAR serviceUpdaterPath[MAX_PATH + 1])
-{
-  BOOL result = FALSE;
-  if (serviceUpdaterPath[0]) {
-    result = DeleteFileW(serviceUpdaterPath);
-    if (!result && GetLastError() != ERROR_PATH_NOT_FOUND &&
-        GetLastError() != ERROR_FILE_NOT_FOUND) {
-      LOG_WARN(("Could not delete service updater path: '%ls'.",
-                serviceUpdaterPath));
-    }
-
-    WCHAR updaterINIPath[MAX_PATH + 1] = { L'\0' };
-    if (PathGetSiblingFilePath(updaterINIPath, serviceUpdaterPath,
-                               L"updater.ini")) {
-      result = DeleteFileW(updaterINIPath);
-      if (!result && GetLastError() != ERROR_PATH_NOT_FOUND &&
-          GetLastError() != ERROR_FILE_NOT_FOUND) {
-        LOG_WARN(("Could not delete service updater INI path: '%ls'.",
-                  updaterINIPath));
-      }
-    }
-  }
-  return result;
-}
-
-/**
  * Executes a service command.
  *
  * @param argc The number of arguments in argv
  * @param argv The service command line arguments, argv[0] and argv[1]
  *             and automatically included by Windows.  argv[2] is the
  *             service command.
  *
  * @return FALSE if there was an error executing the service command.
@@ -579,62 +506,17 @@ ExecuteServiceCommand(int argc, LPWSTR *
     UuidToString(&guid, &guidString);
   }
   LOG(("Executing service command %ls, ID: %ls",
        argv[2], reinterpret_cast<LPCWSTR>(guidString)));
   RpcStringFree(&guidString);
 
   BOOL result = FALSE;
   if (!lstrcmpi(argv[2], L"software-update")) {
-
-    // Use the passed in command line arguments for the update, except for the
-    // path to updater.exe.  We copy updater.exe to a the directory of the
-    // MozillaMaintenance service so that a low integrity process cannot
-    // replace the updater.exe at any point and use that for the update.
-    // It also makes DLL injection attacks harder.
-    LPWSTR oldUpdaterPath = argv[3];
-    WCHAR secureUpdaterPath[MAX_PATH + 1] = { L'\0' };
-    result = GetSecureUpdaterPath(secureUpdaterPath); // Does its own logging
-    if (result) {
-      LOG(("Passed in path: '%ls'; Using this path for updating: '%ls'.",
-           oldUpdaterPath, secureUpdaterPath));
-      DeleteSecureUpdater(secureUpdaterPath);
-      result = CopyFileW(oldUpdaterPath, secureUpdaterPath, FALSE);
-    }
-
-    if (!result) {
-      LOG_WARN(("Could not copy path to secure location.  (%d)",
-                GetLastError()));
-      if (argc > 4 && !WriteStatusFailure(argv[4],
-                                          SERVICE_COULD_NOT_COPY_UPDATER)) {
-        LOG_WARN(("Could not write update.status could not copy updater error"));
-      }
-    } else {
-
-      // We obtained the path and copied it successfully, update the path to
-      // use for the service update.
-      argv[3] = secureUpdaterPath;
-
-      WCHAR oldUpdaterINIPath[MAX_PATH + 1] = { L'\0' };
-      WCHAR secureUpdaterINIPath[MAX_PATH + 1] = { L'\0' };
-      if (PathGetSiblingFilePath(secureUpdaterINIPath, secureUpdaterPath,
-                                 L"updater.ini") &&
-          PathGetSiblingFilePath(oldUpdaterINIPath, oldUpdaterPath,
-                                 L"updater.ini")) {
-        // This is non fatal if it fails there is no real harm
-        if (!CopyFileW(oldUpdaterINIPath, secureUpdaterINIPath, FALSE)) {
-          LOG_WARN(("Could not copy updater.ini from: '%ls' to '%ls'.  (%d)",
-                    oldUpdaterINIPath, secureUpdaterINIPath, GetLastError()));
-        }
-      }
-
-      result = ProcessSoftwareUpdateCommand(argc - 3, argv + 3);
-      DeleteSecureUpdater(secureUpdaterPath);
-    }
-
+    result = ProcessSoftwareUpdateCommand(argc - 3, argv + 3);
     // We might not reach here if the service install succeeded
     // because the service self updates itself and the service
     // installer will stop the service.
     LOG(("Service command %ls complete.", argv[2]));
   } else {
     LOG_WARN(("Service command not recognized: %ls.", argv[2]));
     // result is already set to FALSE
   }
--- a/toolkit/components/passwordmgr/test/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest.ini
@@ -15,18 +15,16 @@ support-files =
   subtst_privbrowsing_2.html
   subtst_privbrowsing_3.html
   subtst_privbrowsing_4.html
   subtst_prompt_async.html
 
 [test_basic_form_2pw_2.html]
 [test_basic_form_autocomplete.html]
 skip-if = toolkit == 'android' # Bug 1258975 on android.
-[test_bug_627616.html]
-skip-if = toolkit == 'android' # Bug 1258975 on android.
 [test_master_password.html]
 skip-if = toolkit == 'android' # Bug 1258975 on android.
 [test_master_password_cleanup.html]
 skip-if = toolkit == 'android' # Bug 1258975 on android.
 [test_notifications_popup.html]
 skip-if = true || os == "linux" || toolkit == 'android' # bug 934057. Bug 1258975 on android.
 [test_prompt.html]
 skip-if = os == "linux" || toolkit == 'android' # Bug 1258975 on android.
--- a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -12,16 +12,18 @@ support-files =
 [test_basic_form_0pw.html]
 [test_basic_form_1pw.html]
 [test_basic_form_1pw_2.html]
 [test_basic_form_2pw_1.html]
 [test_basic_form_3pw_1.html]
 [test_basic_form_html5.html]
 [test_basic_form_pwevent.html]
 [test_basic_form_pwonly.html]
+[test_bug_627616.html]
+skip-if = toolkit == 'android' # Bug 1258975 on android.
 [test_bug_776171.html]
 [test_case_differences.html]
 skip-if = toolkit == 'android' # autocomplete
 [test_form_action_1.html]
 [test_form_action_2.html]
 [test_form_action_javascript.html]
 [test_formless_autofill.html]
 skip-if = toolkit == 'android' # Bug 1259768
rename from toolkit/components/passwordmgr/test/test_bug_627616.html
rename to toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html
--- a/toolkit/components/passwordmgr/test/test_bug_627616.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_bug_627616.html
@@ -1,79 +1,22 @@
 <!DOCTYPE HTML>
 <html>
 <head>
   <meta charset="utf-8">
   <title>Test bug 627616 related to proxy authentication</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
-  <script type="text/javascript" src="prompt_common.js"></script>
+  <script type="text/javascript" src="pwmgr_common.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
 </head>
 <body>
 <script class="testbody" type="text/javascript">
     SimpleTest.waitForExplicitFinish();
 
-    var Cc = SpecialPowers.Cc;
     var Ci = SpecialPowers.Ci;
-    var systemPrincipal = SpecialPowers.Services.scriptSecurityManager.getSystemPrincipal();
-
-    testNum = 1;
-
-    var login, login2;
-
-    var resolveCallback = SpecialPowers.wrapCallbackObject({
-
-      QueryInterface : function (iid) {
-        const interfaces = [Ci.nsIProtocolProxyCallback, Ci.nsISupports];
-
-        if (!interfaces.some( function(v) { return iid.equals(v) } ))
-          throw SpecialPowers.Cr.NS_ERROR_NO_INTERFACE;
-        return this;
-      },
-
-      onProxyAvailable : function (req, uri, pi, status) {
-         init2(SpecialPowers.wrap(pi).host, SpecialPowers.wrap(pi).port);
-      }
-    });
-
-    function init1() {
-        var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
-        var pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService();
-
-        var channel = ios.newChannel2("http://example.com",
-                                      null,
-                                      null,
-                                      null,      // aLoadingNode
-                                      systemPrincipal,
-                                      null,      // aTriggeringPrincipal
-                                      Ci.nsILoadInfo.SEC_NORMAL,
-                                      Ci.nsIContentPolicy.TYPE_OTHER);
-        pps.asyncResolve(channel, 0, resolveCallback);
-    }
-
-    function init2(proxyHost, proxyPort) {
-
-        var mozproxy = "moz-proxy://" + proxyHost + ":" + proxyPort;
-
-        var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
-        login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
-        login.init(mozproxy, null, "proxy_realm", "proxy_user", "proxy_pass", "", "");
-        pwmgr.addLogin(login);
-
-        login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
-        login2.init("http://mochi.test:8888", null, "mochirealm", "user1name", "user1pass", "", "");
-        pwmgr.addLogin(login2);
-        startCallbackTimer();
-    }
-    function cleanup() {
-        var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
-        pwmgr.removeLogin(login);
-        pwmgr.removeLogin(login2);
-        timer.cancel();
-    }
 
     function makeXHR(expectedStatus, expectedText, extra) {
       var xhr =  new XMLHttpRequest();
       xhr.open("GET", "authenticate.sjs?" +
                       "proxy_user=proxy_user&" +
                       "proxy_pass=proxy_pass&" +
                       "proxy_realm=proxy_realm&" +
                       "user=user1name&" +
@@ -107,35 +50,102 @@
       SpecialPowers.wrap(xhr).channel.loadFlags |= Ci.nsIChannel.LOAD_ANONYMOUS;
       xhr.send();
     }
 
     var gExpectedDialogs = 0;
     var gCurrentTest;
     function runNextTest() {
       is(gExpectedDialogs, 0, "received expected number of auth dialogs");
-      Cc["@mozilla.org/network/http-auth-manager;1"].getService(SpecialPowers.Ci.nsIHttpAuthManager).clearAll();
-      if (pendingTests.length > 0) {
-        ({expectedDialogs: gExpectedDialogs,
-          test: gCurrentTest} = pendingTests.shift());
-        gCurrentTest.call(this);
-      } else {
-        cleanup();
-        SimpleTest.finish();
-      }
+      mm.sendAsyncMessage("prepareForNextTest");
+      mm.addMessageListener("prepareForNextTestDone", function prepared(msg) {
+        mm.removeMessageListener("prepareForNextTestDone", prepared);
+        if (pendingTests.length > 0) {
+          ({expectedDialogs: gExpectedDialogs,
+            test: gCurrentTest} = pendingTests.shift());
+          gCurrentTest.call(this);
+        } else {
+          mm.sendAsyncMessage("cleanup");
+          mm.addMessageListener("cleanupDone", msg => {
+            // mm.destroy() is called as a cleanup function by runInParent(), no
+            // need to do it here.
+            SimpleTest.finish();
+          });
+        }
+      });
     }
 
     var pendingTests = [{expectedDialogs: 2, test: testNonAnonymousCredentials},
                         {expectedDialogs: 1, test: testAnonymousCredentials},
                         {expectedDialogs: 0, test: testAnonymousNoAuth}];
-    init1();
-    runNextTest();
+
+    let mm = runInParent(() => {
+      const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+      Cu.import("resource://gre/modules/Services.jsm");
+      Cu.import("resource://gre/modules/Timer.jsm");
+      Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+      let channel = Services.io.newChannel2(
+        "http://example.com",
+        null,
+        null,
+        null,      // aLoadingNode
+        Services.scriptSecurityManager.getSystemPrincipal(),
+        null,      // aTriggeringPrincipal
+        Ci.nsILoadInfo.SEC_NORMAL,
+        Ci.nsIContentPolicy.TYPE_OTHER
+      );
+
+      let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].
+                getService(Ci.nsIProtocolProxyService);
+      pps.asyncResolve(channel, 0, {
+        onProxyAvailable(req, uri, pi, status) {
+          let mozproxy = "moz-proxy://" + pi.host + ":" + pi.port;
+          let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
+                      createInstance(Ci.nsILoginInfo);
+          login.init(mozproxy, null, "proxy_realm", "proxy_user", "proxy_pass",
+                     "", "");
+          Services.logins.addLogin(login);
 
-    function handleDialog(doc, testNum)
-    {
-        var dialog = doc.getElementById("commonDialog");
-        dialog.acceptDialog();
-        gExpectedDialogs--;
-        startCallbackTimer();
-    }
+          let login2 = Cc["@mozilla.org/login-manager/loginInfo;1"].
+                       createInstance(Ci.nsILoginInfo);
+          login2.init("http://mochi.test:8888", null, "mochirealm", "user1name",
+                       "user1pass", "", "");
+          Services.logins.addLogin(login2);
+
+          //startWatchingForPrompts();
+          sendAsyncMessage("setupDone");
+        },
+        QueryInterface: XPCOMUtils.generateQI([Ci.nsIProtocolProxyCallback]),
+      });
+
+      addMessageListener("prepareForNextTest", message => {
+        Cc["@mozilla.org/network/http-auth-manager;1"].
+          getService(Ci.nsIHttpAuthManager).
+          clearAll();
+        sendAsyncMessage("prepareForNextTestDone");
+      });
+
+      let dialogObserverTopic = "common-dialog-loaded";
+
+      function dialogObserver(subj, topic, data) {
+        subj.Dialog.ui.prompt.document.documentElement.acceptDialog();
+        sendAsyncMessage("promptAccepted");
+      }
+
+      Services.obs.addObserver(dialogObserver, dialogObserverTopic, false);
+
+      addMessageListener("cleanup", message => {
+        Services.obs.removeObserver(dialogObserver, dialogObserverTopic);
+        sendAsyncMessage("cleanupDone");
+      });
+    });
+
+    mm.addMessageListener("promptAccepted", msg => {
+      gExpectedDialogs--;
+    });
+    mm.addMessageListener("setupDone", msg => {
+      runNextTest();
+    });
 </script>
 </body>
 </html>
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -5254,23 +5254,16 @@
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 10,
     "high": 20000,
     "n_buckets": 20,
     "description": "Time for a URL bar DB search to return (ms)",
     "cpp_guard": "ANDROID"
   },
-  "FENNEC_GECKOAPP_STARTUP_ACTION": {
-    "expires_in_version": "never",
-    "kind": "enumerated",
-    "n_values": 10,
-    "description": "The way the GeckoApp was launched. (Normal, URL, Prefetch, WebApp, Guest, Restricted, Shortcut)",
-    "cpp_guard": "ANDROID"
-  },
   "FENNEC_RESTRICTED_PROFILE_RESTRICTIONS": {
     "expires_in_version": "50",
     "kind": "enumerated",
     "n_values": 30,
     "description": "Whether various restrictions are set, keyed by restiction name",
     "keyed": true,
     "bug_numbers": [1218942],
     "alert_emails": ["mobile-frontend@mozilla.com"]
@@ -9315,33 +9308,16 @@
   },
   "FENNEC_TABQUEUE_QUEUESIZE" : {
     "expires_in_version": "never",
     "kind": "exponential",
     "high": 50,
     "n_buckets": 10,
     "description": "The number of tabs queued when opened."
   },
-  "FENNEC_TABQUEUE_PROMPT_ENABLE_YES" : {
-    "expires_in_version": "never",
-    "kind": "enumerated",
-    "n_values": 3,
-    "description": "The number of times the tab queue prompt was seen before the user selected YES."
-  },
-  "FENNEC_TABQUEUE_PROMPT_ENABLE_NO" : {
-    "expires_in_version": "never",
-    "kind": "enumerated",
-    "n_values": 3,
-    "description": "The number of times the tab queue prompt was seen before the user selected NO."
-  },
-  "FENNEC_TABQUEUE_ENABLED": {
-    "expires_in_version": "never",
-    "kind": "boolean",
-    "description": "Has the tab queue functionality been enabled."
-  },
   "FENNEC_CUSTOM_HOMEPAGE": {
     "expires_in_version": "50",
     "alert_emails": ["mobile-frontend@mozilla.com"],
     "bug_numbers": [1239102],
     "kind": "boolean",
     "description": "Whether the user has set a custom homepage."
   },
   "GRAPHICS_DRIVER_STARTUP_TEST": {
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -120,33 +120,28 @@ var ContentPolicyManager = {
 
     Services.ppmm.addMessageListener("WebRequest:ShouldLoad", this);
     Services.mm.addMessageListener("WebRequest:ShouldLoad", this);
   },
 
   receiveMessage(msg) {
     let browser = msg.target instanceof Ci.nsIDOMXULElement ? msg.target : null;
 
+    let requestId = RequestId.create();
     for (let id of msg.data.ids) {
       let callback = this.policies.get(id);
       if (!callback) {
         // It's possible that this listener has been removed and the
         // child hasn't learned yet.
         continue;
       }
       let response = null;
       let listenerKind = "onStop";
-      let data = {
-        url: msg.data.url,
-        windowId: msg.data.windowId,
-        parentWindowId: msg.data.parentWindowId,
-        type: msg.data.type,
-        browser: browser,
-        requestId: RequestId.create(),
-      };
+      let data = Object.assign({requestId, browser}, msg.data);
+      delete data.ids;
       try {
         response = callback(data);
         if (response) {
           if (response.cancel) {
             listenerKind = "onError";
             data.error = "NS_ERROR_ABORT";
             return {cancel: true};
           }
@@ -510,42 +505,61 @@ HttpObserverManager = {
     let requestHeaderNames;
     let responseHeaderNames;
 
     let includeStatus = kind === "headersReceived" ||
                         kind === "onRedirect" ||
                         kind === "onStart" ||
                         kind === "onStop";
 
+    let commonData = null;
+    let uri = channel.URI;
     for (let [callback, opts] of listeners.entries()) {
-      if (!this.shouldRunListener(policyType, channel.URI, opts.filter)) {
+      if (!this.shouldRunListener(policyType, uri, opts.filter)) {
         continue;
       }
 
-      let data = {
-        requestId: RequestId.get(channel),
-        url: channel.URI.spec,
-        method: channel.requestMethod,
-        browser: browser,
-        type: WebRequestCommon.typeForPolicyType(policyType),
-        windowId: loadInfo ? loadInfo.outerWindowID : 0,
-        parentWindowId: loadInfo ? loadInfo.parentOuterWindowID : 0,
-      };
+      if (!commonData) {
+        commonData = {
+          requestId: RequestId.get(channel),
+          url: uri.spec,
+          method: channel.requestMethod,
+          browser: browser,
+          type: WebRequestCommon.typeForPolicyType(policyType),
+        };
 
-      let httpChannel = channel.QueryInterface(Ci.nsIHttpChannelInternal);
-      try {
-        data.ip = httpChannel.remoteAddress;
-      } catch (e) {
-        // The remoteAddress getter throws if the address is unavailable,
-        // but ip is an optional property so just ignore the exception.
+        if (loadInfo) {
+          let originPrincipal = loadInfo.triggeringPrincipal || loadInfo.loadingPrincipal;
+          if (originPrincipal && originPrincipal.URI) {
+            commonData.originUrl = originPrincipal.URI.spec;
+          }
+          Object.assign(commonData, {
+            windowId: loadInfo.outerWindowID,
+            parentWindowId: loadInfo.parentOuterWindowID,
+          });
+        } else {
+          Object.assign(commonData, {
+            windowId: 0,
+            parentWindowId: 0,
+          });
+        }
+
+        if (channel instanceof Ci.nsIHttpChannelInternal) {
+          try {
+            commonData.ip = channel.remoteAddress;
+          } catch (e) {
+            // The remoteAddress getter throws if the address is unavailable,
+            // but ip is an optional property so just ignore the exception.
+          }
+        }
+        if (extraData) {
+          Object.assign(commonData, extraData);
+        }
       }
-
-      if (extraData) {
-        Object.assign(data, extraData);
-      }
+      let data = Object.assign({}, commonData);
       if (opts.requestHeaders) {
         data.requestHeaders = this.getHeaders(channel, "visitRequestHeaders", kind);
         requestHeaderNames = data.requestHeaders.map(h => h.name);
       }
       if (opts.responseHeaders) {
         data.responseHeaders = this.getHeaders(channel, "visitResponseHeaders", kind);
         responseHeaderNames = data.responseHeaders.map(h => h.name);
       }
--- a/toolkit/modules/addons/WebRequestContent.js
+++ b/toolkit/modules/addons/WebRequestContent.js
@@ -153,17 +153,19 @@ var ContentPolicy = {
       }
     }
 
     let data = {ids,
                 url,
                 type: WebRequestCommon.typeForPolicyType(policyType),
                 windowId,
                 parentWindowId};
-
+    if (requestOrigin) {
+      data.originUrl = requestOrigin.spec;
+    }
     if (block) {
       let rval = mm.sendSyncMessage("WebRequest:ShouldLoad", data);
       if (rval.length == 1 && rval[0].cancel) {
         return Ci.nsIContentPolicy.REJECT;
       }
     } else {
       mm.sendAsyncMessage("WebRequest:ShouldLoad", data);
     }
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -3376,19 +3376,21 @@ this.XPIProvider = {
    * Installs any add-ons located in the extensions directory of the
    * application's distribution specific directory into the profile unless a
    * newer version already exists or the user has previously uninstalled the
    * distributed add-on.
    *
    * @param  aManifests
    *         A dictionary to add new install manifests to to save having to
    *         reload them later
+   * @param  aAppChanged
+   *         See checkForChanges
    * @return true if any new add-ons were installed
    */
-  installDistributionAddons: function(aManifests) {
+  installDistributionAddons: function(aManifests, aAppChanged) {
     let distroDir;
     try {
       distroDir = FileUtils.getDir(KEY_APP_DISTRIBUTION, [DIR_EXTENSIONS]);
     }
     catch (e) {
       return false;
     }
 
@@ -3424,16 +3426,22 @@ this.XPIProvider = {
       }
 
       if (!gIDTest.test(id)) {
         logger.debug("Ignoring distribution add-on whose name is not a valid add-on ID: " +
             entry.path);
         continue;
       }
 
+      /* If this is not an upgrade and we've already handled this extension
+       * just continue */
+      if (!aAppChanged && Preferences.isSet(PREF_BRANCH_INSTALLED_ADDON + id)) {
+        continue;
+      }
+
       let addon;
       try {
         addon = syncLoadManifestFromFile(entry, profileLocation);
       }
       catch (e) {
         logger.warn("File entry " + entry.path + " contains an invalid add-on", e);
         continue;
       }
@@ -3555,20 +3563,19 @@ this.XPIProvider = {
     // active state of add-ons but didn't commit them properly (normally due
     // to the application crashing)
     let hasPendingChanges = Preferences.get(PREF_PENDING_OPERATIONS);
     if (hasPendingChanges) {
       updateReasons.push("hasPendingChanges");
     }
 
     // If the application has changed then check for new distribution add-ons
-    if (aAppChanged !== false &&
-        Preferences.get(PREF_INSTALL_DISTRO_ADDONS, true))
+    if (Preferences.get(PREF_INSTALL_DISTRO_ADDONS, true))
     {
-      updated = this.installDistributionAddons(manifests);
+      updated = this.installDistributionAddons(manifests, aAppChanged);
       if (updated) {
         updateReasons.push("installDistributionAddons");
       }
     }
 
     // Telemetry probe added around getInstallState() to check perf
     let telemetryCaptureTime = Cu.now();
     let installChanged = XPIStates.getInstallState();
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -268,44 +268,43 @@ function createAppInfo(ID, name, version
     crashReporter: true,
     extraProps: {
       browserTabsRemoteAutostart: false,
     },
   });
   gAppInfo = tmp.getAppInfo();
 }
 
-function getManifestURIForBundle(file, manifest="manifest.json") {
+function getManifestURIForBundle(file) {
   if (file.isDirectory()) {
-    let path = file.clone();
-    path.append("install.rdf");
-    if (path.exists()) {
-      return NetUtil.newURI(path);
+    file.append("install.rdf");
+    if (file.exists()) {
+      return NetUtil.newURI(file);
     }
 
-    path.leafName = manifest;
-    if (path.exists()) {
-      return NetUtil.newURI(path);
+    file.leafName = "manifest.json";
+    if (file.exists()) {
+      return NetUtil.newURI(file);
     }
 
     throw new Error("No manifest file present");
   }
 
   let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
             createInstance(AM_Ci.nsIZipReader);
   zip.open(file);
   try {
     let uri = NetUtil.newURI(file);
 
     if (zip.hasEntry("install.rdf")) {
       return NetUtil.newURI("jar:" + uri.spec + "!/" + "install.rdf");
     }
 
-    if (zip.hasEntry(manifest)) {
-      return NetUtil.newURI("jar:" + uri.spec + "!/" + manifest);
+    if (zip.hasEntry("manifest.json")) {
+      return NetUtil.newURI("jar:" + uri.spec + "!/" + "manifest.json");
     }
 
     throw new Error("No manifest file present");
   }
   finally {
     zip.close();
   }
 }
@@ -337,22 +336,18 @@ let getIDForManifest = Task.async(functi
                      getService(AM_Ci.nsIRDFService);
 
     let rdfID = ds.GetTarget(rdfService.GetResource("urn:mozilla:install-manifest"),
                              rdfService.GetResource("http://www.mozilla.org/2004/em-rdf#id"),
                              true);
     return rdfID.QueryInterface(AM_Ci.nsIRDFLiteral).Value;
   }
   else {
-    try {
-      let manifest = JSON.parse(data);
-      return manifest.applications.gecko.id;
-    } catch (err) {
-      return null;
-    }
+    let manifest = JSON.parse(data);
+    return manifest.applications.gecko.id;
   }
 });
 
 let gUseRealCertChecks = false;
 function overrideCertDB(handler) {
   // Unregister the real database. This only works because the add-ons manager
   // hasn't started up and grabbed the certificate database yet.
   let registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar);
@@ -380,25 +375,16 @@ function overrideCertDB(handler) {
       return;
     }
 
     try {
       let manifestURI = getManifestURIForBundle(file);
 
       let id = yield getIDForManifest(manifestURI);
 
-      if (!id) {
-        manifestURI = getManifestURIForBundle(file, "mozilla.json");
-        id = yield getIDForManifest(manifestURI);
-      }
-
-      if (!id) {
-        throw new Error("Cannot find addon ID");
-      }
-
       // Make sure to close the open zip file or it will be locked.
       if (file.isFile()) {
         Services.obs.notifyObservers(file, "flush-cache-entry", "cert-override");
       }
 
       let fakeCert = {
         commonName: id
       }
@@ -1121,68 +1107,54 @@ function writeInstallRDFForExtension(aDa
  * @param   aManifest
  *          The data to write
  * @param   aDir
  *          The install directory to add the extension to
  * @param   aId
  *          An optional string to override the default installation aId
  * @return  A file pointing to where the extension was installed
  */
-function writeWebManifestForExtension(aData, aDir, aId = undefined, aMozData = undefined) {
+function writeWebManifestForExtension(aData, aDir, aId = undefined) {
   if (!aId)
     aId = aData.applications.gecko.id;
 
   if (TEST_UNPACKED) {
     let dir = aDir.clone();
     dir.append(aId);
     if (!dir.exists())
       dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
 
-    function writeOne(filename, raw) {
-      let file = dir.clone();
-      file.append(filename);
-      if (file.exists())
-        file.remove(true);
+    let file = dir.clone();
+    file.append("manifest.json");
+    if (file.exists())
+      file.remove(true);
 
-      let data = JSON.stringify(raw);
-      let fos = AM_Cc["@mozilla.org/network/file-output-stream;1"].
-          createInstance(AM_Ci.nsIFileOutputStream);
-      fos.init(file,
-               FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
-               FileUtils.PERMS_FILE, 0);
-      fos.write(data, data.length);
-      fos.close();
-    }
-
-    writeOne("manifest.json", aData);
-    if (aMozData) {
-      writeOne("mozilla.json", aMozData);
-    }
+    let data = JSON.stringify(aData);
+    let fos = AM_Cc["@mozilla.org/network/file-output-stream;1"].
+              createInstance(AM_Ci.nsIFileOutputStream);
+    fos.init(file,
+             FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
+             FileUtils.PERMS_FILE, 0);
+    fos.write(data, data.length);
+    fos.close();
 
     return dir;
   }
   else {
     let file = aDir.clone();
     file.append(aId + ".xpi");
 
     let stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
                  createInstance(AM_Ci.nsIStringInputStream);
     stream.setData(JSON.stringify(aData), -1);
     let zipW = AM_Cc["@mozilla.org/zipwriter;1"].
                createInstance(AM_Ci.nsIZipWriter);
     zipW.open(file, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
     zipW.addEntryStream("manifest.json", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
                         stream, false);
-    if (aMozData) {
-      let mozStream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
-                      createInstance(AM_Ci.nsIStringInputStream);
-      mozStream.setData(JSON.stringify(aMozData), -1);
-      zipW.addEntryStream("mozilla.json", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
-                          mozStream, false);
-    }
     zipW.close();
 
     return file;
   }
 }
 
 /**
  * Writes an install.rdf manifest into a packed extension using the properties passed
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
@@ -154,51 +154,16 @@ add_task(function*() {
   let file = getFileForAddon(profileDir, ID);
   do_check_true(file.exists());
 
   addon.uninstall();
 
   yield promiseRestartManager();
 });
 
-// applications.gecko.id may be in mozilla.json
-add_task(function* test_mozilla_json() {
-  writeWebManifestForExtension({
-    name: "Web Extension Name",
-    version: "1.0",
-    manifest_version: 2,
-  }, profileDir, ID, {
-    applications: {
-      gecko: {
-        id: ID
-      }
-    }
-  });
-
-  yield promiseRestartManager();
-
-  let addon = yield promiseAddonByID(ID);
-  do_check_neq(addon, null);
-  do_check_eq(addon.version, "1.0");
-  do_check_eq(addon.name, "Web Extension Name");
-  do_check_true(addon.isCompatible);
-  do_check_false(addon.appDisabled);
-  do_check_true(addon.isActive);
-  do_check_false(addon.isSystem);
-  do_check_eq(addon.type, "extension");
-  do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
-
-  let file = getFileForAddon(profileDir, ID);
-  do_check_true(file.exists());
-
-  addon.uninstall();
-
-  yield promiseRestartManager();
-});
-
 add_task(function* test_manifest_localization() {
   const ID = "webextension3@tests.mozilla.org";
 
   yield promiseInstallAllFiles([do_get_addon("webextension_3")], true);
   yield promiseAddonStartup();
 
   let addon = yield promiseAddonByID(ID);
   addon.userDisabled = true;
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -4658,31 +4658,31 @@ enum {
   // kE10sDisabledInSafeMode = 3, was removed in bug 1172491.
   kE10sDisabledForAccessibility = 4,
   // kE10sDisabledForMacGfx = 5, was removed in bug 1068674.
   kE10sDisabledForBidi = 6,
   kE10sDisabledForAddons = 7,
   kE10sForceDisabled = 8,
 };
 
-#ifdef XP_WIN
+#if defined(XP_WIN) || defined(XP_MACOSX)
 const char* kAccessibilityLastRunDatePref = "accessibility.lastLoadDate";
 const char* kAccessibilityLoadedLastSessionPref = "accessibility.loadedInLastSession";
-#endif // XP_WIN
-const char* kForceEnableE10sPref = "browser.tabs.remote.force-enable";
-const char* kForceDisableE10sPref = "browser.tabs.remote.force-disable";
-
-#ifdef XP_WIN
+
 static inline uint32_t
 PRTimeToSeconds(PRTime t_usec)
 {
   PRTime usec_per_sec = PR_USEC_PER_SEC;
   return uint32_t(t_usec /= usec_per_sec);
 }
-#endif // XP_WIN
+#endif // XP_WIN || XP_MACOSX
+
+const char* kForceEnableE10sPref = "browser.tabs.remote.force-enable";
+const char* kForceDisableE10sPref = "browser.tabs.remote.force-disable";
+
 
 uint32_t
 MultiprocessBlockPolicy() {
   if (gMultiprocessBlockPolicyInitialized) {
     return gMultiprocessBlockPolicy;
   }
   gMultiprocessBlockPolicyInitialized = true;
 
@@ -4699,17 +4699,17 @@ MultiprocessBlockPolicy() {
 #endif
 
   if (addonsCanDisable && disabledByAddons) {
     gMultiprocessBlockPolicy = kE10sDisabledForAddons;
     return gMultiprocessBlockPolicy;
   }
 
   bool disabledForA11y = false;
-#ifdef XP_WIN
+#if defined(XP_WIN) || defined(XP_MACOSX)
   /**
    * Avoids enabling e10s if accessibility has recently loaded. Performs the
    * following checks:
    * 1) Checks a pref indicating if a11y loaded in the last session. This pref
    * is set in nsBrowserGlue.js. If a11y was loaded in the last session we
    * do not enable e10s in this session.
    * 2) Accessibility stores a last run date (PR_IntervalNow) when it is
    * initialized (see nsBaseWidget.cpp). We check if this pref exists and
@@ -4726,17 +4726,17 @@ MultiprocessBlockPolicy() {
     uint32_t now = PRTimeToSeconds(PR_Now());
     uint32_t difference = now - a11yRunDate;
     if (difference > ONE_WEEK_IN_SECONDS || !a11yRunDate) {
       Preferences::ClearUser(kAccessibilityLastRunDatePref);
     } else {
       disabledForA11y = true;
     }
   }
-#endif // XP_WIN
+#endif // XP_WIN || XP_MACOSX
 
   if (disabledForA11y) {
     gMultiprocessBlockPolicy = kE10sDisabledForAccessibility;
     return gMultiprocessBlockPolicy;
   }
 
   /**
    * Avoids enabling e10s for certain locales that require bidi selection,
--- a/widget/nsBaseWidget.cpp
+++ b/widget/nsBaseWidget.cpp
@@ -164,17 +164,17 @@ nsBaseWidget::nsBaseWidget()
 , mOriginalBounds(nullptr)
 , mClipRectCount(0)
 , mSizeMode(nsSizeMode_Normal)
 , mPopupLevel(ePopupLevelTop)
 , mPopupType(ePopupTypeAny)
 , mUpdateCursor(true)
 , mUseAttachedEvents(false)
 , mIMEHasFocus(false)
-#ifdef XP_WIN
+#if defined(XP_WIN) || defined(XP_MACOSX)
 , mAccessibilityInUseFlag(false)
 #endif
 {
 #ifdef NOISY_WIDGET_LEAKS
   gNumWidgets++;
   printf("WIDGETS+ = %d\n", gNumWidgets);
 #endif
 
@@ -232,17 +232,17 @@ WidgetShutdownObserver::Unregister()
 {
   if (mRegistered) {
     mWidget = nullptr;
     nsContentUtils::UnregisterShutdownObserver(this);
     mRegistered = false;
   }
 }
 
-#ifdef XP_WIN
+#if defined(XP_WIN) || defined(XP_MACOSX)
 // defined in nsAppRunner.cpp
 extern const char* kAccessibilityLastRunDatePref;
 
 static inline uint32_t
 PRTimeToSeconds(PRTime t_usec)
 {
   PRTime usec_per_sec = PR_USEC_PER_SEC;
   return uint32_t(t_usec /= usec_per_sec);
@@ -254,22 +254,16 @@ nsBaseWidget::Shutdown()
 {
   DestroyCompositor();
   FreeShutdownObserver();
 #if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
   if (sPluginWidgetList) {
     delete sPluginWidgetList;
     sPluginWidgetList = nullptr;
   }
-#if defined(XP_WIN)
-  if (mAccessibilityInUseFlag) {
-    uint32_t now = PRTimeToSeconds(PR_Now());
-    Preferences::SetInt(kAccessibilityLastRunDatePref, now);
-  }
-#endif
 #endif
 }
 
 void nsBaseWidget::DestroyCompositor()
 {
   if (mCompositorBridgeChild) {
     // XXX CompositorBridgeChild and CompositorBridgeParent might be re-created in
     // ClientLayerManager destructor. See bug 1133426.
@@ -1870,18 +1864,22 @@ nsBaseWidget::GetRootAccessible()
   nsPresContext* presContext = presShell->GetPresContext();
   NS_ENSURE_TRUE(presContext->GetContainerWeak(), nullptr);
 
   // Accessible creation might be not safe so use IsSafeToRunScript to
   // make sure it's not created at unsafe times.
   nsCOMPtr<nsIAccessibilityService> accService =
     services::GetAccessibilityService();
   if (accService) {
-#ifdef XP_WIN
-    mAccessibilityInUseFlag = true;
+#if defined(XP_WIN) || defined(XP_MACOSX)
+    if (!mAccessibilityInUseFlag) {
+      mAccessibilityInUseFlag = true;
+      uint32_t now = PRTimeToSeconds(PR_Now());
+      Preferences::SetInt(kAccessibilityLastRunDatePref, now);
+    }
 #endif
     return accService->GetRootDocumentAccessible(presShell, nsContentUtils::IsSafeToRunScript());
   }
 
   return nullptr;
 }
 
 #endif // ACCESSIBILITY
--- a/widget/nsBaseWidget.h
+++ b/widget/nsBaseWidget.h
@@ -543,17 +543,17 @@ protected:
   nsSizeMode        mSizeMode;
   nsPopupLevel      mPopupLevel;
   nsPopupType       mPopupType;
   SizeConstraints   mSizeConstraints;
 
   bool              mUpdateCursor;
   bool              mUseAttachedEvents;
   bool              mIMEHasFocus;
-#ifdef XP_WIN
+#if defined(XP_WIN) || defined(XP_MACOSX)
   bool              mAccessibilityInUseFlag;
 #endif
   static nsIRollupListener* gRollupListener;
 
   // the last rolled up popup. Only set this when an nsAutoRollup is in scope,
   // so it can be cleared automatically.
   static nsIContent* mLastRollup;