Merge m-c to m-i
authorPhil Ringnalda <philringnalda@gmail.com>
Thu, 06 Oct 2016 20:26:20 -0700
changeset 422089 e907031a16123846a0817935a3f64b67286a5d73
parent 422088 d7790b3df5921e31f1a7cdf49475e97133892f1c (current diff)
parent 421960 4b9944879c9a60a9aba4a744a7401bc38e0f39c4 (diff)
child 422090 ac1f8bddeb3f2f6bf3457909b637d07c7e93fa00
child 422092 eccfe9efc72325c165f0175de34c482d83bcb6bb
child 422126 a937bbfad93bc7bda91f6436f8c419c9b7c178d7
push id31687
push usercykesiopka.bmo@gmail.com
push dateFri, 07 Oct 2016 12:33:49 +0000
milestone52.0a1
Merge m-c to m-i MozReview-Commit-ID: Kp60wHZauGi
mobile/android/base/java/org/mozilla/gecko/TextSelectionHandle.java
mobile/android/base/resources/layout/text_selection_handles.xml
mobile/android/chrome/content/SelectionHandler.js
services/sync/tests/unit/test_errorhandler.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1003,17 +1003,17 @@ pref("security.sandbox.content.level", 1
 // to whitelist more system calls.
 //
 // So the purpose of this setting is to allow nightly users to disable the
 // sandbox while we fix their problems. This way, they won't have to wait for
 // another nightly release which disables seccomp-bpf again.
 //
 // This setting may not be required anymore once we decide to permanently
 // enable the content sandbox.
-pref("security.sandbox.content.level", 1);
+pref("security.sandbox.content.level", 2);
 #endif
 
 #if defined(XP_MACOSX) || defined(XP_WIN)
 #if defined(MOZ_SANDBOX) && defined(MOZ_CONTENT_SANDBOX)
 // ID (a UUID when set by gecko) that is used to form the name of a
 // sandbox-writable temporary directory to be used by content processes
 // when a temporary writable file is required in a level 1 sandbox.
 pref("security.sandbox.content.tempDirSuffix", "");
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -424,16 +424,17 @@ class BasePopup {
 
 global.PanelPopup = class PanelPopup extends BasePopup {
   constructor(extension, imageNode, popupURL, browserStyle) {
     let document = imageNode.ownerDocument;
 
     let panel = document.createElement("panel");
     panel.setAttribute("id", makeWidgetId(extension.id) + "-panel");
     panel.setAttribute("class", "browser-extension-panel");
+    panel.setAttribute("tabspecific", "true");
     panel.setAttribute("type", "arrow");
     panel.setAttribute("role", "group");
 
     document.getElementById("mainPopupSet").appendChild(panel);
 
     super(extension, panel, popupURL, browserStyle);
 
     this.ignoreResizes = false;
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
@@ -1,13 +1,15 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 add_task(function* testPageActionPopup() {
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
+
   let scriptPage = url => `<html><head><meta charset="utf-8"><script src="${url}"></script></head></html>`;
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "background": {
         "page": "data/background.html",
       },
       "page_action": {
@@ -64,16 +66,22 @@ add_task(function* testPageActionPopup()
           },
           () => {
             browser.pageAction.setPopup({tabId, popup: "/popup-a.html"});
             sendClick({expectEvent: false, expectPopup: "a", runNextTest: true});
           },
           () => {
             browser.test.sendMessage("next-test", {expectClosed: true});
           },
+          () => {
+            sendClick({expectEvent: false, expectPopup: "a", runNextTest: true});
+          },
+          () => {
+            browser.test.sendMessage("next-test", {closeOnTabSwitch: true});
+          },
         ];
 
         let expect = {};
         sendClick = ({expectEvent, expectPopup, runNextTest}) => {
           expect = {event: expectEvent, popup: expectPopup, runNextTest};
           browser.test.sendMessage("send-click");
         };
 
@@ -148,16 +156,30 @@ add_task(function* testPageActionPopup()
     if (expecting.expectClosed) {
       ok(panel, "Expect panel to exist");
       yield promisePopupShown(panel);
 
       extension.sendMessage("close-popup");
 
       yield promisePopupHidden(panel);
       ok(true, `Panel is closed`);
+    } else if (expecting.closeOnTabSwitch) {
+      ok(panel, "Expect panel to exist");
+      yield promisePopupShown(panel);
+
+      let oldTab = gBrowser.selectedTab;
+      ok(oldTab != gBrowser.tabs[0], "Should have an inactive tab to switch to");
+
+      let hiddenPromise = promisePopupHidden(panel);
+
+      gBrowser.selectedTab = gBrowser.tabs[0];
+      yield hiddenPromise;
+      info("Panel closed");
+
+      gBrowser.selectedTab = oldTab;
     } else if (panel) {
       yield promisePopupShown(panel);
       panel.hidePopup();
     }
 
     if (panel) {
       panel = document.getElementById(panelId);
       is(panel, null, "panel successfully removed from document after hiding");
@@ -172,16 +194,18 @@ add_task(function* testPageActionPopup()
 
   yield extension.unload();
 
   let node = document.getElementById(pageActionId);
   is(node, null, "pageAction image removed from document");
 
   let panel = document.getElementById(panelId);
   is(panel, null, "pageAction panel removed from document");
+
+  yield BrowserTestUtils.removeTab(tab);
 });
 
 
 add_task(function* testPageActionSecurity() {
   const URL = "chrome://browser/content/browser.xul";
 
   let apis = ["browser_action", "page_action"];
 
--- a/browser/components/preferences/cookies.js
+++ b/browser/components/preferences/cookies.js
@@ -1,15 +1,16 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */
 /* 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/. */
 
 const nsICookie = Components.interfaces.nsICookie;
 
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
 Components.utils.import("resource://gre/modules/PluralForm.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm")
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
                                   "resource://gre/modules/ContextualIdentityService.jsm");
 
 var gCookiesWindow = {
--- a/devtools/client/debugger/new/test/mochitest/browser.ini
+++ b/devtools/client/debugger/new/test/mochitest/browser.ini
@@ -1,11 +1,12 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
+skip-if = (os == 'linux' && debug && bits == 32)
 support-files =
   head.js
   !/devtools/client/commandline/test/helpers.js
   !/devtools/client/framework/test/shared-head.js
   examples/bundle.js
   examples/bundle.js.map
   examples/doc-scripts.html
   examples/doc-script-switching.html
--- a/devtools/client/styleeditor/test/browser_styleeditor_autocomplete.js
+++ b/devtools/client/styleeditor/test/browser_styleeditor_autocomplete.js
@@ -3,135 +3,142 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 "use strict";
 
 // Test that autocompletion works as expected.
 
 const TESTCASE_URI = TEST_BASE_HTTP + "autocomplete.html";
 const MAX_SUGGESTIONS = 15;
 
-const {getClientCssProperties} = require("devtools/shared/fronts/css-properties");
-const {CSSProperties, CSSValues} = getCSSKeywords();
+const {initCssProperties} = require("devtools/shared/fronts/css-properties");
 
 // Test cases to test that autocompletion works correctly when enabled.
 // Format:
 // [
 //   key,
 //   {
 //     total: Number of suggestions in the popup (-1 if popup is closed),
 //     current: Index of selected suggestion,
 //     inserted: 1 to check whether the selected suggestion is inserted into the
 //               editor or not,
 //     entered: 1 if the suggestion is inserted and finalized
 //   }
 // ]
-var TEST_CASES = [
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["Ctrl+Space", {total: 1, current: 0}],
-  ["VK_LEFT"],
-  ["VK_RIGHT"],
-  ["VK_DOWN"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["Ctrl+Space", { total: getSuggestionNumberFor("font"), current: 0}],
-  ["VK_END"],
-  ["VK_RETURN"],
-  ["b", {total: getSuggestionNumberFor("b"), current: 0}],
-  ["a", {total: getSuggestionNumberFor("ba"), current: 0}],
-  ["VK_DOWN", {total: getSuggestionNumberFor("ba"), current: 0, inserted: 1}],
-  ["VK_TAB", {total: getSuggestionNumberFor("ba"), current: 1, inserted: 1}],
-  ["VK_RETURN", {current: 1, inserted: 1, entered: 1}],
-  ["b", {total: getSuggestionNumberFor("background", "b"), current: 0}],
-  ["l", {total: getSuggestionNumberFor("background", "bl"), current: 0}],
-  ["VK_TAB", {
-    total: getSuggestionNumberFor("background", "bl"),
-    current: 0, inserted: 1
-  }],
-  ["VK_DOWN", {
-    total: getSuggestionNumberFor("background", "bl"),
-    current: 1, inserted: 1
-  }],
-  ["VK_UP", {
-    total: getSuggestionNumberFor("background", "bl"),
-    current: 0,
-    inserted: 1
-  }],
-  ["VK_TAB", {
-    total: getSuggestionNumberFor("background", "bl"),
-    current: 1,
-    inserted: 1
-  }],
-  ["VK_TAB", {
-    total: getSuggestionNumberFor("background", "bl"),
-    current: 2,
-    inserted: 1
-  }],
-  [";"],
-  ["VK_RETURN"],
-  ["c", {total: getSuggestionNumberFor("c"), current: 0}],
-  ["o", {total: getSuggestionNumberFor("co"), current: 0}],
-  ["VK_RETURN", {current: 0, inserted: 1}],
-  ["r", {total: getSuggestionNumberFor("color", "r"), current: 0}],
-  ["VK_RETURN", {current: 0, inserted: 1}],
-  [";"],
-  ["VK_LEFT"],
-  ["VK_RIGHT"],
-  ["VK_DOWN"],
-  ["VK_RETURN"],
-  ["b", {total: 2, current: 0}],
-  ["u", {total: 1, current: 0}],
-  ["VK_RETURN", {current: 0, inserted: 1}],
-  ["{"],
-  ["VK_HOME"],
-  ["VK_DOWN"],
-  ["VK_DOWN"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["VK_RIGHT"],
-  ["Ctrl+Space", {total: 1, current: 0}],
-];
+
+function getTestCases(cssProperties) {
+  let keywords = getCSSKeywords(cssProperties);
+  let getSuggestionNumberFor = suggestionNumberGetter(keywords);
+
+  return [
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["Ctrl+Space", {total: 1, current: 0}],
+    ["VK_LEFT"],
+    ["VK_RIGHT"],
+    ["VK_DOWN"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["Ctrl+Space", { total: getSuggestionNumberFor("font"), current: 0}],
+    ["VK_END"],
+    ["VK_RETURN"],
+    ["b", {total: getSuggestionNumberFor("b"), current: 0}],
+    ["a", {total: getSuggestionNumberFor("ba"), current: 0}],
+    ["VK_DOWN", {total: getSuggestionNumberFor("ba"), current: 0, inserted: 1}],
+    ["VK_TAB", {total: getSuggestionNumberFor("ba"), current: 1, inserted: 1}],
+    ["VK_RETURN", {current: 1, inserted: 1, entered: 1}],
+    ["b", {total: getSuggestionNumberFor("background", "b"), current: 0}],
+    ["l", {total: getSuggestionNumberFor("background", "bl"), current: 0}],
+    ["VK_TAB", {
+      total: getSuggestionNumberFor("background", "bl"),
+      current: 0, inserted: 1
+    }],
+    ["VK_DOWN", {
+      total: getSuggestionNumberFor("background", "bl"),
+      current: 1, inserted: 1
+    }],
+    ["VK_UP", {
+      total: getSuggestionNumberFor("background", "bl"),
+      current: 0,
+      inserted: 1
+    }],
+    ["VK_TAB", {
+      total: getSuggestionNumberFor("background", "bl"),
+      current: 1,
+      inserted: 1
+    }],
+    ["VK_TAB", {
+      total: getSuggestionNumberFor("background", "bl"),
+      current: 2,
+      inserted: 1
+    }],
+    [";"],
+    ["VK_RETURN"],
+    ["c", {total: getSuggestionNumberFor("c"), current: 0}],
+    ["o", {total: getSuggestionNumberFor("co"), current: 0}],
+    ["VK_RETURN", {current: 0, inserted: 1}],
+    ["r", {total: getSuggestionNumberFor("color", "r"), current: 0}],
+    ["VK_RETURN", {current: 0, inserted: 1}],
+    [";"],
+    ["VK_LEFT"],
+    ["VK_RIGHT"],
+    ["VK_DOWN"],
+    ["VK_RETURN"],
+    ["b", {total: 2, current: 0}],
+    ["u", {total: 1, current: 0}],
+    ["VK_RETURN", {current: 0, inserted: 1}],
+    ["{"],
+    ["VK_HOME"],
+    ["VK_DOWN"],
+    ["VK_DOWN"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["VK_RIGHT"],
+    ["Ctrl+Space", {total: 1, current: 0}],
+  ];
+}
 
 add_task(function* () {
   let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI);
+  let { cssProperties } = yield initCssProperties(panel._toolbox);
+  let testCases = getTestCases(cssProperties);
 
   yield ui.selectStyleSheet(ui.editors[1].styleSheet);
   let editor = yield ui.editors[1].getSourceEditor();
 
   let sourceEditor = editor.sourceEditor;
   let popup = sourceEditor.getAutocompletionPopup();
 
   yield SimpleTest.promiseFocus(panel.panelWindow);
 
-  for (let index in TEST_CASES) {
-    yield testState(index, sourceEditor, popup, panel.panelWindow);
-    yield checkState(index, sourceEditor, popup);
+  for (let index in testCases) {
+    yield testState(testCases, index, sourceEditor, popup, panel.panelWindow);
+    yield checkState(testCases, index, sourceEditor, popup);
   }
 });
 
-function testState(index, sourceEditor, popup, panelWindow) {
-  let [key, details] = TEST_CASES[index];
+function testState(testCases, index, sourceEditor, popup, panelWindow) {
+  let [key, details] = testCases[index];
   let entered;
   if (details) {
     entered = details.entered;
   }
   let mods = {};
 
   info("pressing key " + key + " to get result: " +
-                JSON.stringify(TEST_CASES[index]) + " for index " + index);
+                JSON.stringify(testCases[index]) + " for index " + index);
 
   let evt = "after-suggest";
 
   if (key == "Ctrl+Space") {
     key = " ";
     mods.ctrlKey = true;
   } else if (key == "VK_RETURN" && entered) {
     evt = "popup-hidden";
@@ -143,20 +150,20 @@ function testState(index, sourceEditor, 
   }
 
   let ready = sourceEditor.once(evt);
   EventUtils.synthesizeKey(key, mods, panelWindow);
 
   return ready;
 }
 
-function checkState(index, sourceEditor, popup) {
+function checkState(testCases, index, sourceEditor, popup) {
   let deferred = defer();
   executeSoon(() => {
-    let [, details] = TEST_CASES[index];
+    let [, details] = testCases[index];
     details = details || {};
     let {total, current, inserted} = details;
 
     if (total != undefined) {
       ok(popup.isOpen, "Popup is open for index " + index);
       is(total, popup.itemCount,
          "Correct total suggestions for index " + index);
       is(current, popup.selectedIndex,
@@ -190,34 +197,35 @@ function checkState(index, sourceEditor,
  *
  * @return {Object} An object with following properties:
  *         - CSSProperties {Array} Array of string containing all the possible
  *                         CSS property names.
  *         - CSSValues {Object|Map} A map where key is the property name and
  *                     value is an array of string containing all the possible
  *                     CSS values the property can have.
  */
-function getCSSKeywords() {
-  let cssProperties = getClientCssProperties();
+function getCSSKeywords(cssProperties) {
   let props = {};
   let propNames = cssProperties.getNames();
   propNames.forEach(prop => {
     props[prop] = cssProperties.getValues(prop).sort();
   });
   return {
     CSSValues: props,
     CSSProperties: propNames.sort()
   };
 }
 
 /**
- * Returns the number of expected suggestions for the given property and value.
- * If the value is not null, returns the number of values starting with `value`.
- * Returns the number of properties starting with `property` otherwise.
+ * Returns a function that returns the number of expected suggestions for the given
+ * property and value. If the value is not null, returns the number of values starting
+ * with `value`. Returns the number of properties starting with `property` otherwise.
  */
-function getSuggestionNumberFor(property, value) {
-  if (value == null) {
-    return CSSProperties.filter(prop => prop.startsWith(property))
-                        .slice(0, MAX_SUGGESTIONS).length;
-  }
-  return CSSValues[property].filter(val => val.startsWith(value))
-                            .slice(0, MAX_SUGGESTIONS).length;
+function suggestionNumberGetter({CSSProperties, CSSValues}) {
+  return (property, value) => {
+    if (value == null) {
+      return CSSProperties.filter(prop => prop.startsWith(property))
+                          .slice(0, MAX_SUGGESTIONS).length;
+    }
+    return CSSValues[property].filter(val => val.startsWith(value))
+                              .slice(0, MAX_SUGGESTIONS).length;
+  };
 }
--- a/devtools/server/actors/css-properties.js
+++ b/devtools/server/actors/css-properties.js
@@ -5,20 +5,16 @@
 "use strict";
 
 const { Cc, Ci } = require("chrome");
 
 loader.lazyGetter(this, "DOMUtils", () => {
   return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
 });
 
-loader.lazyGetter(this, "appInfo", () => {
-  return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
-});
-
 const protocol = require("devtools/shared/protocol");
 const { ActorClassWithSpec, Actor } = protocol;
 const { cssPropertiesSpec } = require("devtools/shared/specs/css-properties");
 const { CSS_PROPERTIES, CSS_TYPES } = require("devtools/shared/css/properties-db");
 const { cssColors } = require("devtools/shared/css/color-db");
 
 exports.CssPropertiesActor = ActorClassWithSpec(cssPropertiesSpec, {
   typeName: "cssProperties",
@@ -27,25 +23,17 @@ exports.CssPropertiesActor = ActorClassW
     Actor.prototype.initialize.call(this, conn);
     this.parent = parent;
   },
 
   destroy() {
     Actor.prototype.destroy.call(this);
   },
 
-  getCSSDatabase(clientBrowserVersion) {
-    // If the client and server are both the same version of Firefox, do not return a
-    // database, use the client-side css-properties-db.js.
-    const serverBrowserVersion = appInfo.platformVersion.match(/^\d+/)[0];
-
-    if (clientBrowserVersion !== 0 && clientBrowserVersion === serverBrowserVersion) {
-      return {};
-    }
-
+  getCSSDatabase() {
     const properties = generateCssProperties();
     const pseudoElements = DOMUtils.getCSSPseudoElementNames();
 
     return { properties, pseudoElements };
   }
 });
 
 /**
--- a/devtools/shared/fronts/css-properties.js
+++ b/devtools/shared/fronts/css-properties.js
@@ -200,19 +200,20 @@ const initCssProperties = Task.async(fun
     return cachedCssProperties.get(client);
   }
 
   let db, front;
 
   // Get the list dynamically if the cssProperties actor exists.
   if (toolbox.target.hasActor("cssProperties")) {
     front = CssPropertiesFront(client, toolbox.target.form);
-    const serverDB = yield front.getCSSDatabase(getClientBrowserVersion(toolbox));
+    const serverDB = yield front.getCSSDatabase();
 
-    // The serverDB will be blank if the browser versions match, so use the static list.
+    // Ensure the database was returned in a format that is understood.
+    // Older versions of the protocol could return a blank database.
     if (!serverDB.properties && !serverDB.margin) {
       db = CSS_PROPERTIES_DB;
     } else {
       db = serverDB;
     }
   } else {
     // The target does not support this actor, so require a static list of supported
     // properties.
@@ -245,26 +246,16 @@ function getCssProperties(toolbox) {
  * the target.
  * @return {CssProperties}
  */
 function getClientCssProperties() {
   return new CssProperties(normalizeCssData(CSS_PROPERTIES_DB));
 }
 
 /**
- * Get the current browser version.
- * @returns {string} The browser version.
- */
-function getClientBrowserVersion(toolbox) {
-  const regexResult = toolbox.win.navigator
-                             .userAgent.match(/Firefox\/(\d+)\.\d/);
-  return Array.isArray(regexResult) ? regexResult[1] : "0";
-}
-
-/**
  * Even if the target has the cssProperties actor, the returned data may not be in the
  * same shape or have all of the data we need. This normalizes the data and fills in
  * any missing information like color values.
  *
  * @return {Object} The normalized CSS database.
  */
 function normalizeCssData(db) {
   if (db !== CSS_PROPERTIES_DB) {
--- a/devtools/shared/specs/css-properties.js
+++ b/devtools/shared/specs/css-properties.js
@@ -1,22 +1,20 @@
 /* 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 { Arg, RetVal, generateActorSpec } = require("devtools/shared/protocol");
+const { RetVal, generateActorSpec } = require("devtools/shared/protocol");
 
 const cssPropertiesSpec = generateActorSpec({
   typeName: "cssProperties",
 
   methods: {
     getCSSDatabase: {
-      request: {
-        clientBrowserVersion: Arg(0, "string"),
-      },
+      request: {},
 
       response: RetVal("json"),
     }
   }
 });
 
 exports.cssPropertiesSpec = cssPropertiesSpec;
--- a/devtools/shared/tests/unit/xpcshell.ini
+++ b/devtools/shared/tests/unit/xpcshell.ini
@@ -5,16 +5,21 @@ tail =
 firefox-appdir = browser
 skip-if = toolkit == 'android' || toolkit == 'gonk'
 support-files =
   exposeLoader.js
 
 [test_assert.js]
 [test_csslexer.js]
 [test_css-properties-db.js]
+# This test only enforces that the CSS database is up to date with nightly. The DB is
+# only used when inspecting a target that doesn't support the getCSSDatabase actor.
+# CSS properties are behind compile-time flags, and there is no automatic rebuild
+# process for uplifts, so this test breaks on uplift.
+run-if = nightly_build
 [test_fetch-bom.js]
 [test_fetch-chrome.js]
 [test_fetch-file.js]
 [test_fetch-http.js]
 [test_fetch-resource.js]
 [test_flatten.js]
 [test_indentation.js]
 [test_independent_loaders.js]
--- a/dom/browser-element/mochitest/browserElementTestHelpers.js
+++ b/dom/browser-element/mochitest/browserElementTestHelpers.js
@@ -60,18 +60,21 @@ const browserElementTestHelpers = {
   setClipboardPlainTextOnlyPref: function(value) {
     this._setPref('clipboard.plainTextOnly', value);
   },
 
   setEnabledPref: function(value) {
     this._setPref('dom.mozBrowserFramesEnabled', value);
   },
 
-  setAccessibleCaretEnabledPref: function(value) {
-    this._setPref('layout.accessiblecaret.enabled', value);
+  setupAccessibleCaretPref: function() {
+    this._setPref('layout.accessiblecaret.enabled', true);
+    // Disable hide carets for mouse input for select-all tests so that we can
+    // get mozbrowsercaretstatechanged events.
+    this._setPref('layout.accessiblecaret.hide_carets_for_mouse_input', false);
   },
 
   getOOPByDefaultPref: function() {
     return this._getBoolPref("dom.ipc.browser_frames.oop_by_default");
   },
 
   addPermission: function() {
     this.lockTestReady();
--- a/dom/browser-element/mochitest/browserElement_CopyPaste.js
+++ b/dom/browser-element/mochitest/browserElement_CopyPaste.js
@@ -2,17 +2,17 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Test that "cut, copy, paste, selectall" and caretstatechanged event works from inside an <iframe mozbrowser>.
 "use strict";
 
 SimpleTest.waitForExplicitFinish();
 SimpleTest.requestFlakyTimeout("untriaged");
 browserElementTestHelpers.setEnabledPref(true);
-browserElementTestHelpers.setAccessibleCaretEnabledPref(true);
+browserElementTestHelpers.setupAccessibleCaretPref();
 browserElementTestHelpers.addPermission();
 const { Services } = SpecialPowers.Cu.import('resource://gre/modules/Services.jsm');
 
 var gTextarea = null;
 var mm;
 var iframeOuter;
 var iframeInner;
 var state = 0;
--- a/dom/browser-element/mochitest/browserElement_Manifestchange.js
+++ b/dom/browser-element/mochitest/browserElement_Manifestchange.js
@@ -37,17 +37,17 @@ function runTest() {
 
   iframe1.addEventListener('mozbrowsermanifestchange', function(e) {
 
     numManifestChanges++;
 
     if (numManifestChanges == 1) {
       is(e.detail.href, 'manifest.1', 'manifest.1 matches');
 
-      // We should recieve manifestchange events when the user creates new
+      // We should receive manifestchange events when the user creates new
       // manifests
       SpecialPowers.getBrowserFrameMessageManager(iframe1)
                    .loadFrameScript("data:,content.document.title='New title';",
                                     /* allowDelayedLoad = */ false);
 
       SpecialPowers.getBrowserFrameMessageManager(iframe1)
                    .loadFrameScript("data:,content.document.head.insertAdjacentHTML('beforeend', '<link rel=manifest href=manifest.2>')",
                                     /* allowDelayedLoad = */ false);
@@ -81,16 +81,16 @@ function runTest() {
   });
 
   iframe3.addEventListener('mozbrowsermanifestchange', function(e) {
     ok(false, 'Should not get a manifestchange event for iframe3.');
   });
 
 
   iframe1.src = createHtml(createManifest('manifest.1'));
-  // We should not recieve manifest change events for either of the below iframes
+  // We should not receive manifest change events for either of the below iframes
   iframe2.src = createHtml(createManifest('manifest.1'));
   iframe3.src = createHtml(createManifest('manifest.1'));
 
 }
 
 addEventListener('testready', runTest);
 
--- a/dom/events/test/test_all_synthetic_events.html
+++ b/dom/events/test/test_all_synthetic_events.html
@@ -439,16 +439,20 @@ const kEventConstructors = {
   RecordErrorEvent:                          { create: function (aName, aProps) {
                                                          return new RecordErrorEvent(aName, aProps);
                                                        },
                                              },
   RTCDataChannelEvent:                       { create: function (aName, aProps) {
                                                          return new RTCDataChannelEvent(aName, aProps);
                                                        },
                                              },
+  RTCDTMFToneChangeEvent:                       { create: function (aName, aProps) {
+                                                         return new RTCDTMFToneChangeEvent(aName, aProps);
+                                                       },
+                                             },
   RTCPeerConnectionIceEvent:                 { create: function (aName, aProps) {
                                                          return new RTCPeerConnectionIceEvent(aName, aProps);
                                                        },
                                              },
   RTCTrackEvent:                             {
                                                // Difficult to test required arguments.
                                              },
   ScrollAreaEvent:                           { create: function (aName, aProps) {
--- a/dom/grid/GridLines.cpp
+++ b/dom/grid/GridLines.cpp
@@ -63,16 +63,17 @@ GridLines::IndexedGetter(uint32_t aIndex
 }
 
 void
 GridLines::SetLineInfo(const ComputedGridTrackInfo* aTrackInfo,
                        const ComputedGridLineInfo* aLineInfo,
                        const nsTArray<RefPtr<GridArea>>& aAreas,
                        bool aIsRow)
 {
+  MOZ_ASSERT(aLineInfo);
   mLines.Clear();
 
   if (!aTrackInfo) {
     return;
   }
 
   uint32_t trackCount = aTrackInfo->mEndFragmentTrack -
                         aTrackInfo->mStartFragmentTrack;
@@ -91,19 +92,17 @@ GridLines::SetLineInfo(const ComputedGri
          i++) {
       uint32_t line1Index = i + 1;
 
       startOfNextTrack = (i < aTrackInfo->mEndFragmentTrack) ?
                          aTrackInfo->mPositions[i] :
                          lastTrackEdge;
 
       nsTArray<nsString> lineNames;
-      if (aLineInfo) {
-        lineNames = aLineInfo->mNames.SafeElementAt(i, nsTArray<nsString>());
-      }
+      lineNames = aLineInfo->mNames.SafeElementAt(i, nsTArray<nsString>());
 
       // Add in names from grid areas where this line is used as a boundary.
       for (auto area : aAreas) {
         bool haveNameToAdd = false;
         nsAutoString nameToAdd;
         area->GetName(nameToAdd);
         if (aIsRow) {
           if (area->RowStart() == line1Index) {
--- a/dom/media/MediaDecoderStateMachine.cpp
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -932,21 +932,21 @@ public:
 
     // With buffering heuristics we will remain in the buffering state if
     // we've not decoded enough data to begin playback, or if we've not
     // downloaded a reasonable amount of data inside our buffering time.
     if (Reader()->UseBufferingHeuristics()) {
       TimeDuration elapsed = now - mBufferingStart;
       bool isLiveStream = Resource()->IsLiveStream();
       if ((isLiveStream || !mMaster->CanPlayThrough()) &&
-          elapsed < TimeDuration::FromSeconds(mMaster->mBufferingWait * mMaster->mPlaybackRate) &&
-          mMaster->HasLowBufferedData(mMaster->mBufferingWait * USECS_PER_S) &&
+          elapsed < TimeDuration::FromSeconds(mBufferingWait * mMaster->mPlaybackRate) &&
+          mMaster->HasLowBufferedData(mBufferingWait * USECS_PER_S) &&
           Resource()->IsExpectingMoreData()) {
         SLOG("Buffering: wait %ds, timeout in %.3lfs",
-             mMaster->mBufferingWait, mMaster->mBufferingWait - elapsed.ToSeconds());
+             mBufferingWait, mBufferingWait - elapsed.ToSeconds());
         mMaster->ScheduleStateMachineIn(USECS_PER_S);
         return;
       }
     } else if (mMaster->OutOfDecodedAudio() || mMaster->OutOfDecodedVideo()) {
       MOZ_ASSERT(Reader()->IsWaitForDataSupported(),
                  "Don't yet have a strategy for non-heuristic + non-WaitForData");
       mMaster->DispatchDecodeTasksIfNeeded();
       MOZ_ASSERT(mMaster->mMinimizePreroll ||
@@ -1010,16 +1010,20 @@ public:
     seekJob.mTarget = aTarget;
     RefPtr<MediaDecoder::SeekPromise> p = seekJob.mPromise.Ensure(__func__);
     mMaster->InitiateSeek(Move(seekJob));
     return p.forget();
   }
 
 private:
   TimeStamp mBufferingStart;
+
+  // The maximum number of second we spend buffering when we are short on
+  // unbuffered data.
+  const uint32_t mBufferingWait = 15;
 };
 
 class MediaDecoderStateMachine::CompletedState
   : public MediaDecoderStateMachine::StateObject
 {
 public:
   explicit CompletedState(Master* aPtr) : StateObject(aPtr) {}
 
@@ -1188,19 +1192,16 @@ MediaDecoderStateMachine::MediaDecoderSt
   INIT_CANONICAL(mPlaybackOffset, 0),
   INIT_CANONICAL(mIsAudioDataAudible, false)
 {
   MOZ_COUNT_CTOR(MediaDecoderStateMachine);
   NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
 
   InitVideoQueuePrefs();
 
-  mBufferingWait = 15;
-  mLowDataThresholdUsecs = detail::LOW_DATA_THRESHOLD_USECS;
-
 #ifdef XP_WIN
   // Ensure high precision timers are enabled on Windows, otherwise the state
   // machine isn't woken up at reliable intervals to set the next frame,
   // and we drop frames while painting. Note that multiple calls to this
   // function per-process is OK, provided each call is matched by a corresponding
   // timeEndPeriod() call.
   timeBeginPeriod(1);
 #endif
@@ -2458,17 +2459,17 @@ bool MediaDecoderStateMachine::OutOfDeco
     return IsAudioDecoding() && !AudioQueue().IsFinished() &&
            AudioQueue().GetSize() == 0 &&
            !mMediaSink->HasUnplayedFrames(TrackInfo::kAudioTrack);
 }
 
 bool MediaDecoderStateMachine::HasLowBufferedData()
 {
   MOZ_ASSERT(OnTaskQueue());
-  return HasLowBufferedData(mLowDataThresholdUsecs);
+  return HasLowBufferedData(detail::LOW_DATA_THRESHOLD_USECS);
 }
 
 bool MediaDecoderStateMachine::HasLowBufferedData(int64_t aUsecs)
 {
   MOZ_ASSERT(OnTaskQueue());
   MOZ_ASSERT(mState >= DECODER_STATE_DECODING,
              "Must have loaded first frame for mBuffered to be valid");
 
--- a/dom/media/MediaDecoderStateMachine.h
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -641,21 +641,16 @@ private:
 
   // The end time of the last decoded video frame. Used to check if we are low
   // on decoded video data.
   int64_t mDecodedVideoEndTime;
 
   // Playback rate. 1.0 : normal speed, 0.5 : two times slower.
   double mPlaybackRate;
 
-  // The maximum number of second we spend buffering when we are short on
-  // unbuffered data.
-  uint32_t mBufferingWait;
-  int64_t  mLowDataThresholdUsecs;
-
   // If we've got more than this number of decoded video frames waiting in
   // the video queue, we will not decode any more video frames until some have
   // been consumed by the play state machine thread.
   // Must hold monitor.
   uint32_t GetAmpleVideoFrames() const;
 
   // Low audio threshold. If we've decoded less than this much audio we
   // consider our audio decode "behind", and we may skip video decoding
--- a/dom/media/PeerConnection.js
+++ b/dom/media/PeerConnection.js
@@ -21,27 +21,29 @@ const PC_OBS_CONTRACT = "@mozilla.org/do
 const PC_ICE_CONTRACT = "@mozilla.org/dom/rtcicecandidate;1";
 const PC_SESSION_CONTRACT = "@mozilla.org/dom/rtcsessiondescription;1";
 const PC_MANAGER_CONTRACT = "@mozilla.org/dom/peerconnectionmanager;1";
 const PC_STATS_CONTRACT = "@mozilla.org/dom/rtcstatsreport;1";
 const PC_STATIC_CONTRACT = "@mozilla.org/dom/peerconnectionstatic;1";
 const PC_SENDER_CONTRACT = "@mozilla.org/dom/rtpsender;1";
 const PC_RECEIVER_CONTRACT = "@mozilla.org/dom/rtpreceiver;1";
 const PC_COREQUEST_CONTRACT = "@mozilla.org/dom/createofferrequest;1";
+const PC_DTMF_SENDER_CONTRACT = "@mozilla.org/dom/rtcdtmfsender;1";
 
 const PC_CID = Components.ID("{bdc2e533-b308-4708-ac8e-a8bfade6d851}");
 const PC_OBS_CID = Components.ID("{d1748d4c-7f6a-4dc5-add6-d55b7678537e}");
 const PC_ICE_CID = Components.ID("{02b9970c-433d-4cc2-923d-f7028ac66073}");
 const PC_SESSION_CID = Components.ID("{1775081b-b62d-4954-8ffe-a067bbf508a7}");
 const PC_MANAGER_CID = Components.ID("{7293e901-2be3-4c02-b4bd-cbef6fc24f78}");
 const PC_STATS_CID = Components.ID("{7fe6e18b-0da3-4056-bf3b-440ef3809e06}");
 const PC_STATIC_CID = Components.ID("{0fb47c47-a205-4583-a9fc-cbadf8c95880}");
 const PC_SENDER_CID = Components.ID("{4fff5d46-d827-4cd4-a970-8fd53977440e}");
 const PC_RECEIVER_CID = Components.ID("{d974b814-8fde-411c-8c45-b86791b81030}");
 const PC_COREQUEST_CID = Components.ID("{74b2122d-65a8-4824-aa9e-3d664cb75dc2}");
+const PC_DTMF_SENDER_CID = Components.ID("{3610C242-654E-11E6-8EC0-6D1BE389A607}");
 
 // Global list of PeerConnection objects, so they can be cleaned up when
 // a page is torn down. (Maps inner window ID to an array of PC objects).
 function GlobalPCList() {
   this._list = {};
   this._networkdown = false; // XXX Need to query current state somehow
   this._lifecycleobservers = {};
   this._nextId = 1;
@@ -1061,16 +1063,24 @@ RTCPeerConnection.prototype = {
     this._checkClosed();
     var i = this._senders.indexOf(sender);
     if (i >= 0) {
       this._senders.splice(i, 1);
       this._impl.removeTrack(sender.track); // fires negotiation needed
     }
   },
 
+  _insertDTMF: function(sender, tones, duration, interToneGap) {
+    return this._impl.insertDTMF(sender.__DOM_IMPL__, tones, duration, interToneGap);
+  },
+
+  _getDTMFToneBuffer: function(sender) {
+    return this._impl.getDTMFToneBuffer(sender.__DOM_IMPL__);
+  },
+
   _replaceTrack: function(sender, withTrack) {
     // TODO: Do a (sender._stream.getTracks().indexOf(track) < 0) check
     //       on both track args someday.
     //
     // The proposed API will be that both tracks must already be in the same
     // stream. However, since our MediaStreams currently are limited to one
     // track per type, we allow replacement with an outside track not already
     // in the same stream.
@@ -1553,16 +1563,23 @@ PeerConnectionObserver.prototype = {
   foundIceCandidate: function(cand) {
     this.dispatchEvent(new this._dompc._win.RTCPeerConnectionIceEvent("icecandidate",
                                                                       { candidate: cand } ));
   },
 
   notifyDataChannel: function(channel) {
     this.dispatchEvent(new this._dompc._win.RTCDataChannelEvent("datachannel",
                                                                 { channel: channel }));
+  },
+
+  onDTMFToneChange: function(trackId, tone) {
+    var pc = this._dompc;
+    var sender = pc._senders.find(sender => sender.track.id == trackId)
+    sender.dtmf.dispatchEvent(new pc._win.RTCDTMFToneChangeEvent("tonechange",
+                                                                 { tone: tone }));
   }
 };
 
 function RTCPeerConnectionStatic() {
 }
 RTCPeerConnectionStatic.prototype = {
   classDescription: "RTCPeerConnectionStatic",
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
@@ -1576,20 +1593,68 @@ RTCPeerConnectionStatic.prototype = {
       .getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
   },
 
   registerPeerConnectionLifecycleCallback: function(cb) {
     _globalPCList._registerPeerConnectionLifecycleCallback(this._winID, cb);
   },
 };
 
+function RTCDTMFSender(sender) {
+  this._sender = sender;
+  this.duration = 100;
+  this.interToneGap = 70;
+}
+RTCDTMFSender.prototype = {
+  classDescription: "RTCDTMFSender",
+  classID: PC_DTMF_SENDER_CID,
+  contractID: PC_DTMF_SENDER_CONTRACT,
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
+
+  get toneBuffer() {
+    return this._sender._pc._getDTMFToneBuffer(this._sender);
+  },
+
+  get ontonechange() {
+    return this.__DOM_IMPL__.getEventHandler("ontonechange");
+  },
+
+  set ontonechange(handler) {
+    this.__DOM_IMPL__.setEventHandler("ontonechange", handler);
+  },
+
+  insertDTMF: function(tones, duration, interToneGap) {
+    this._sender._pc._checkClosed();
+
+    if (this._sender._pc._senders.indexOf(this._sender.__DOM_IMPL__) == -1) {
+      throw new this._sender._pc._win.DOMException("RTCRtpSender is stopped",
+                                                   "InvalidStateError");
+    }
+
+    this.duration = Math.max(40, Math.min(duration, 6000));
+
+    if (interToneGap < 30) interToneGap = 30;
+    this.interToneGap = interToneGap;
+
+    tones = tones.toUpperCase();
+
+    if (tones.match(/[^0-9A-D#*,]/)) {
+      throw new this._sender._pc._win.DOMException("Invalid DTMF characters",
+                                                   "InvalidCharacterError");
+    }
+
+    this._sender._pc._insertDTMF(this._sender, tones, duration, interToneGap);
+  },
+};
+
 function RTCRtpSender(pc, track, stream) {
   this._pc = pc;
   this.track = track;
   this._stream = stream;
+  this.dtmf = pc._win.RTCDTMFSender._create(pc._win, new RTCDTMFSender(this));
 }
 RTCRtpSender.prototype = {
   classDescription: "RTCRtpSender",
   classID: PC_SENDER_CID,
   contractID: PC_SENDER_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
 
   replaceTrack: function(withTrack) {
@@ -1627,16 +1692,17 @@ CreateOfferRequest.prototype = {
   classDescription: "CreateOfferRequest",
   classID: PC_COREQUEST_CID,
   contractID: PC_COREQUEST_CONTRACT,
   QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports]),
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory(
   [GlobalPCList,
+   RTCDTMFSender,
    RTCIceCandidate,
    RTCSessionDescription,
    RTCPeerConnection,
    RTCPeerConnectionStatic,
    RTCRtpReceiver,
    RTCRtpSender,
    RTCStatsReport,
    PeerConnectionObserver,
--- a/dom/media/PeerConnection.manifest
+++ b/dom/media/PeerConnection.manifest
@@ -3,19 +3,21 @@ component {d1748d4c-7f6a-4dc5-add6-d55b7
 component {02b9970c-433d-4cc2-923d-f7028ac66073} PeerConnection.js
 component {1775081b-b62d-4954-8ffe-a067bbf508a7} PeerConnection.js
 component {7293e901-2be3-4c02-b4bd-cbef6fc24f78} PeerConnection.js
 component {7fe6e18b-0da3-4056-bf3b-440ef3809e06} PeerConnection.js
 component {0fb47c47-a205-4583-a9fc-cbadf8c95880} PeerConnection.js
 component {4fff5d46-d827-4cd4-a970-8fd53977440e} PeerConnection.js
 component {d974b814-8fde-411c-8c45-b86791b81030} PeerConnection.js
 component {74b2122d-65a8-4824-aa9e-3d664cb75dc2} PeerConnection.js
+component {3610C242-654E-11E6-8EC0-6D1BE389A607} PeerConnection.js
 
 contract @mozilla.org/dom/peerconnection;1 {bdc2e533-b308-4708-ac8e-a8bfade6d851}
 contract @mozilla.org/dom/peerconnectionobserver;1 {d1748d4c-7f6a-4dc5-add6-d55b7678537e}
+contract @mozilla.org/dom/rtcdtmfsender;1 {3610C242-654E-11E6-8EC0-6D1BE389A607}
 contract @mozilla.org/dom/rtcicecandidate;1 {02b9970c-433d-4cc2-923d-f7028ac66073}
 contract @mozilla.org/dom/rtcsessiondescription;1 {1775081b-b62d-4954-8ffe-a067bbf508a7}
 contract @mozilla.org/dom/peerconnectionmanager;1 {7293e901-2be3-4c02-b4bd-cbef6fc24f78}
 contract @mozilla.org/dom/rtcstatsreport;1 {7fe6e18b-0da3-4056-bf3b-440ef3809e06}
 contract @mozilla.org/dom/peerconnectionstatic;1 {0fb47c47-a205-4583-a9fc-cbadf8c95880}
 contract @mozilla.org/dom/rtpsender;1 {4fff5d46-d827-4cd4-a970-8fd53977440e}
 contract @mozilla.org/dom/rtpreceiver;1 {d974b814-8fde-411c-8c45-b86791b81030}
 contract @mozilla.org/dom/createofferrequest;1 {74b2122d-65a8-4824-aa9e-3d664cb75dc2}
--- a/dom/media/tests/mochitest/mochitest.ini
+++ b/dom/media/tests/mochitest/mochitest.ini
@@ -134,16 +134,17 @@ skip-if = (android_version == '18' && de
 # [test_peerConnection_certificates.html] # bug 1180968
 [test_peerConnection_close.html]
 [test_peerConnection_closeDuringIce.html]
 [test_peerConnection_constructedStream.html]
 skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_errorCallbacks.html]
 [test_peerConnection_iceFailure.html]
 skip-if = os == 'linux' || os == 'mac' || os == 'win' || android_version == '18' # (Bug 1180388 for win, mac and linux), android(Bug 1189784, timeouts on 4.3 emulator)
+[test_peerConnection_insertDTMF.html]
 [test_peerConnection_forwarding_basicAudioVideoCombined.html]
 skip-if = android_version == '18' # android(Bug 1189784, timeouts on 4.3 emulator)
 [test_peerConnection_noTrickleAnswer.html]
 [test_peerConnection_noTrickleOffer.html]
 [test_peerConnection_noTrickleOfferAnswer.html]
 [test_peerConnection_offerRequiresReceiveAudio.html]
 [test_peerConnection_offerRequiresReceiveVideo.html]
 [test_peerConnection_offerRequiresReceiveVideoAudio.html]
new file mode 100644
--- /dev/null
+++ b/dom/media/tests/mochitest/test_peerConnection_insertDTMF.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript;version=1.8">
+createHTML({
+  bug: "1291715",
+  title: "Test insertDTMF on sender",
+  visible: true
+});
+
+function insertdtmftest(pc) {
+  ok(pc.getSenders().length > 0, "have senders");
+  var sender = pc.getSenders()[0];
+  ok(sender.dtmf, "sender has dtmf object");
+
+  ok(sender.dtmf.toneBuffer === "", "sender should start with empty tonebuffer");
+
+  sender.dtmf.insertDTMF("A", 10);
+  is(sender.dtmf.duration, 40, "minimum duration is 40");
+  sender.dtmf.insertDTMF("A", 10000);
+  is(sender.dtmf.duration, 6000, "maximum duration is 6000");
+  sender.dtmf.insertDTMF("A", 70, 10);
+  is(sender.dtmf.duration, 70, "default duration is 70");
+  is(sender.dtmf.interToneGap, 30, "minimum interToneGap is 30");
+  sender.dtmf.insertDTMF("", 100, 40);
+  is(sender.dtmf.duration, 100, "duration is 70");
+  is(sender.dtmf.interToneGap, 40, "interToneGap is 40");
+
+  var threw = false;
+  try {
+    sender.dtmf.insertDTMF("bad tones");
+  } catch (ex) {
+    threw = true;
+    is(ex.code, DOMException.INVALID_CHARACTER_ERR, "Expected InvalidCharacterError");
+  }
+  ok(threw, "Expected exception");
+
+  sender.dtmf.insertDTMF("A");
+  sender.dtmf.insertDTMF("B");
+  ok(sender.dtmf.toneBuffer.indexOf("A") == -1, "calling insertDTMF should replace current characters");
+
+  sender.dtmf.insertDTMF("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+  ok(sender.dtmf.toneBuffer.indexOf("A") != -1, "lowercase characters should be normalized");
+
+  pc.removeTrack(sender);
+  threw = false;
+  try {
+    sender.dtmf.insertDTMF("AAA");
+  } catch (ex) {
+    threw = true;
+    is(ex.code, DOMException.INVALID_STATE_ERR, "Expected InvalidStateError");
+  }
+  ok(threw, "Expected exception");
+}
+
+runNetworkTest(() => {
+  test = new PeerConnectionTest();
+  test.setMediaConstraints([{audio: true}], [{audio: true}]);
+  test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW");
+
+  // Test sender dtmf.
+  test.chain.append([
+    function PC_LOCAL_INSERT_DTMF(test) {
+      // We want to call removeTrack
+      test.pcLocal.expectNegotiationNeeded();
+      return insertdtmftest(test.pcLocal._pc);
+    }
+  ]);
+
+  var pushPrefs = (...p) => new Promise(r => SpecialPowers.pushPrefEnv({set: p}, r));
+
+  return pushPrefs(['media.peerconnection.dtmf.enabled', true])
+    .then(() => { test.run() })
+    .catch(e => ok(false, "unexpected failure: " + e));
+});
+
+</script>
+</pre>
+</body>
+</html>
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -996,16 +996,20 @@ var interfaceNamesInGlobalScope =
     "Response",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "RGBColor",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "RTCCertificate",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "RTCDataChannelEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
+    "RTCDTMFSender",
+// IMPORTANT: Do not change this list without review from a DOM peer!
+    "RTCDTMFToneChangeEvent",
+// IMPORTANT: Do not change this list without review from a DOM peer!
     "RTCIceCandidate",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "RTCPeerConnection",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "RTCPeerConnectionIceEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "RTCRtpReceiver",
 // IMPORTANT: Do not change this list without review from a DOM peer!
--- a/dom/webidl/PeerConnectionImpl.webidl
+++ b/dom/webidl/PeerConnectionImpl.webidl
@@ -40,16 +40,22 @@ interface PeerConnectionImpl  {
   void getStats(MediaStreamTrack? selector);
 
   /* Adds the tracks created by GetUserMedia */
   [Throws]
   void addTrack(MediaStreamTrack track, MediaStream... streams);
   [Throws]
   void removeTrack(MediaStreamTrack track);
   [Throws]
+  void insertDTMF(RTCRtpSender sender, DOMString tones,
+                  optional unsigned long duration = 100,
+                  optional unsigned long interToneGap = 70);
+  [Throws]
+  DOMString getDTMFToneBuffer(RTCRtpSender sender);
+  [Throws]
   void replaceTrack(MediaStreamTrack thisTrack, MediaStreamTrack withTrack);
   [Throws]
   void setParameters(MediaStreamTrack track,
                      optional RTCRtpParameters parameters);
   [Throws]
   RTCRtpParameters getParameters(MediaStreamTrack track);
   [Throws]
   void closeStreams();
--- a/dom/webidl/PeerConnectionObserver.webidl
+++ b/dom/webidl/PeerConnectionObserver.webidl
@@ -39,9 +39,12 @@ interface PeerConnectionObserver
   /* Notification of one of several types of state changed */
   void onStateChange(PCObserverStateType state);
 
   /* Changes to MediaStreamTracks */
   void onAddStream(MediaStream stream);
   void onRemoveStream(MediaStream stream);
   void onAddTrack(MediaStreamTrack track, sequence<MediaStream> streams);
   void onRemoveTrack(MediaStreamTrack track);
+
+  /* DTMF callback */
+  void onDTMFToneChange(DOMString trackId, DOMString tone);
 };
new file mode 100644
--- /dev/null
+++ b/dom/webidl/RTCDTMFSender.webidl
@@ -0,0 +1,19 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * The origin of this IDL file is
+ * https://www.w3.org/TR/webrtc/#rtcdtmfsender
+ */
+
+[JSImplementation="@mozilla.org/dom/rtcdtmfsender;1"]
+interface RTCDTMFSender : EventTarget {
+    void insertDTMF(DOMString tones,
+                    optional unsigned long duration = 100,
+                    optional unsigned long interToneGap = 70);
+             attribute EventHandler  ontonechange;
+    readonly attribute DOMString     toneBuffer;
+    readonly attribute unsigned long duration;
+    readonly attribute unsigned long interToneGap;
+};
new file mode 100644
--- /dev/null
+++ b/dom/webidl/RTCDTMFToneChangeEvent.webidl
@@ -0,0 +1,8 @@
+[Constructor(DOMString type, optional RTCDTMFToneChangeEventInit eventInitDict)]
+interface RTCDTMFToneChangeEvent : Event {
+    readonly attribute DOMString tone;
+};
+
+dictionary RTCDTMFToneChangeEventInit : EventInit {
+    DOMString tone = "";
+};
--- a/dom/webidl/RTCRtpSender.webidl
+++ b/dom/webidl/RTCRtpSender.webidl
@@ -68,9 +68,11 @@ dictionary RTCRtpParameters {
 
 [Pref="media.peerconnection.enabled",
  JSImplementation="@mozilla.org/dom/rtpsender;1"]
 interface RTCRtpSender {
   readonly attribute MediaStreamTrack track;
   Promise<void> setParameters (optional RTCRtpParameters parameters);
   RTCRtpParameters getParameters();
   Promise<void> replaceTrack(MediaStreamTrack track);
+  [Pref="media.peerconnection.dtmf.enabled"]
+  readonly attribute RTCDTMFSender? dtmf;
 };
--- a/dom/webidl/moz.build
+++ b/dom/webidl/moz.build
@@ -644,16 +644,17 @@ if CONFIG['MOZ_WEBRTC']:
         'DataChannel.webidl',
         'MediaStreamList.webidl',
         'PeerConnectionImpl.webidl',
         'PeerConnectionImplEnums.webidl',
         'PeerConnectionObserver.webidl',
         'PeerConnectionObserverEnums.webidl',
         'RTCCertificate.webidl',
         'RTCConfiguration.webidl',
+        'RTCDTMFSender.webidl',
         'RTCIceCandidate.webidl',
         'RTCIdentityAssertion.webidl',
         'RTCIdentityProvider.webidl',
         'RTCPeerConnection.webidl',
         'RTCPeerConnectionStatic.webidl',
         'RTCRtpReceiver.webidl',
         'RTCRtpSender.webidl',
         'RTCSessionDescription.webidl',
@@ -840,16 +841,17 @@ GENERATED_EVENTS_WEBIDL_FILES = [
     'UserProximityEvent.webidl',
     'USSDReceivedEvent.webidl',
     'WebGLContextEvent.webidl',
 ]
 
 if CONFIG['MOZ_WEBRTC']:
     GENERATED_EVENTS_WEBIDL_FILES += [
         'RTCDataChannelEvent.webidl',
+        'RTCDTMFToneChangeEvent.webidl',
         'RTCPeerConnectionIceEvent.webidl',
         'RTCTrackEvent.webidl',
     ]
 
 if CONFIG['MOZ_WEBSPEECH']:
     GENERATED_EVENTS_WEBIDL_FILES += [
         'SpeechRecognitionEvent.webidl',
         'SpeechSynthesisErrorEvent.webidl',
--- a/gfx/angle/src/libANGLE/renderer/d3d/d3d11/Framebuffer11.cpp
+++ b/gfx/angle/src/libANGLE/renderer/d3d/d3d11/Framebuffer11.cpp
@@ -177,17 +177,20 @@ gl::Error Framebuffer11::invalidateBase(
             {
                 // Handle color attachments
                 ASSERT((attachments[i] >= GL_COLOR_ATTACHMENT0 && attachments[i] <= GL_COLOR_ATTACHMENT15) ||
                        (attachments[i] == GL_COLOR));
 
                 size_t colorIndex =
                     (attachments[i] == GL_COLOR ? 0u : (attachments[i] - GL_COLOR_ATTACHMENT0));
                 auto colorAttachment = mState.getColorAttachment(colorIndex);
-                ANGLE_TRY(invalidateAttachment(colorAttachment));
+		if (colorAttachment)
+		{
+		    ANGLE_TRY(invalidateAttachment(colorAttachment));
+		}
                 break;
             }
         }
     }
 
     bool discardDepth = false;
     bool discardStencil = false;
 
--- a/gfx/skia/generate_mozbuild.py
+++ b/gfx/skia/generate_mozbuild.py
@@ -120,16 +120,17 @@ if CONFIG['MOZ_TREE_FREETYPE']:
 
 # Suppress warnings in third-party code.
 if CONFIG['GNU_CXX'] or CONFIG['CLANG_CL']:
     CXXFLAGS += [
         '-Wno-deprecated-declarations',
         '-Wno-overloaded-virtual',
         '-Wno-shadow',
         '-Wno-sign-compare',
+        '-Wno-unreachable-code',
         '-Wno-unused-function',
     ]
 if CONFIG['GNU_CXX'] and not CONFIG['CLANG_CXX'] and not CONFIG['CLANG_CL']:
     CXXFLAGS += [
         '-Wno-logical-op',
         '-Wno-maybe-uninitialized',
     ]
 if CONFIG['CLANG_CXX'] or CONFIG['CLANG_CL']:
--- a/gfx/skia/moz.build
+++ b/gfx/skia/moz.build
@@ -652,16 +652,17 @@ if CONFIG['MOZ_TREE_FREETYPE']:
 
 # Suppress warnings in third-party code.
 if CONFIG['GNU_CXX'] or CONFIG['CLANG_CL']:
     CXXFLAGS += [
         '-Wno-deprecated-declarations',
         '-Wno-overloaded-virtual',
         '-Wno-shadow',
         '-Wno-sign-compare',
+        '-Wno-unreachable-code',
         '-Wno-unused-function',
     ]
 if CONFIG['GNU_CXX'] and not CONFIG['CLANG_CXX'] and not CONFIG['CLANG_CL']:
     CXXFLAGS += [
         '-Wno-logical-op',
         '-Wno-maybe-uninitialized',
     ]
 if CONFIG['CLANG_CXX'] or CONFIG['CLANG_CL']:
--- a/layout/base/AccessibleCaretManager.cpp
+++ b/layout/base/AccessibleCaretManager.cpp
@@ -191,16 +191,24 @@ AccessibleCaretManager::OnSelectionChang
 
   // For mouse input we don't want to show the carets.
   if (sHideCaretsForMouseInput &&
       mLastInputSource == nsIDOMMouseEvent::MOZ_SOURCE_MOUSE) {
     HideCarets();
     return NS_OK;
   }
 
+  // No need to show the carets for select all action when we want to hide
+  // the carets for mouse input.
+  if (sHideCaretsForMouseInput &&
+      (aReason & nsISelectionListener::SELECTALL_REASON)) {
+    HideCarets();
+    return NS_OK;
+  }
+
   UpdateCarets();
   return NS_OK;
 }
 
 void
 AccessibleCaretManager::HideCarets()
 {
   if (mFirstCaret->IsLogicallyVisible() || mSecondCaret->IsLogicallyVisible()) {
--- a/layout/generic/nsBlockFrame.cpp
+++ b/layout/generic/nsBlockFrame.cpp
@@ -223,16 +223,31 @@ nsBlockFrame::InitDebugFlags()
 #ifdef DEBUG
 const char* nsBlockFrame::kReflowCommandType[] = {
   "ContentChanged",
   "StyleChanged",
   "ReflowDirty",
   "Timeout",
   "UserDefined",
 };
+
+const char*
+nsBlockFrame::LineReflowStatusToString(LineReflowStatus aLineReflowStatus) const
+{
+  switch (aLineReflowStatus) {
+    case LineReflowStatus::OK: return "LINE_REFLOW_OK";
+    case LineReflowStatus::Stop: return "LINE_REFLOW_STOP";
+    case LineReflowStatus::RedoNoPull: return "LINE_REFLOW_REDO_NO_PULL";
+    case LineReflowStatus::RedoMoreFloats: return "LINE_REFLOW_REDO_MORE_FLOATS";
+    case LineReflowStatus::RedoNextBand: return "LINE_REFLOW_REDO_NEXT_BAND";
+    case LineReflowStatus::Truncated: return "LINE_REFLOW_TRUNCATED";
+  }
+  return "unknown";
+}
+
 #endif
 
 #ifdef REFLOW_STATUS_COVERAGE
 static void
 RecordReflowStatus(bool aChildIsBlock, nsReflowStatus aFrameReflowStatus)
 {
   static uint32_t record[2];
 
@@ -3758,19 +3773,19 @@ nsBlockFrame::ReflowInlineFrames(BlockRe
           lineLayout.ForceBreakAtPosition(forceBreakInFrame, forceBreakOffset);
         }
         DoReflowInlineFrames(aState, lineLayout, aLine,
                              floatAvailableSpace, availableSpaceHeight,
                              &floatManagerState, aKeepReflowGoing,
                              &lineReflowStatus, allowPullUp);
         lineLayout.EndLineReflow();
 
-        if (LINE_REFLOW_REDO_NO_PULL == lineReflowStatus ||
-            LINE_REFLOW_REDO_MORE_FLOATS == lineReflowStatus ||
-            LINE_REFLOW_REDO_NEXT_BAND == lineReflowStatus) {
+        if (LineReflowStatus::RedoNoPull == lineReflowStatus ||
+            LineReflowStatus::RedoMoreFloats == lineReflowStatus ||
+            LineReflowStatus::RedoNextBand == lineReflowStatus) {
           if (lineLayout.NeedsBackup()) {
             NS_ASSERTION(!forceBreakInFrame, "Backing up twice; this should never be necessary");
             // If there is no saved break position, then this will set
             // set forceBreakInFrame to null and we won't back up, which is
             // correct.
             forceBreakInFrame =
               lineLayout.GetLastOptionalBreakPosition(&forceBreakOffset, &forceBreakPriority);
           } else {
@@ -3778,41 +3793,33 @@ nsBlockFrame::ReflowInlineFrames(BlockRe
           }
           // restore the float manager state
           aState.mReflowInput.mFloatManager->PopState(&floatManagerState);
           // Clear out float lists
           aState.mCurrentLineFloats.DeleteAll();
           aState.mBelowCurrentLineFloats.DeleteAll();
         }
 
-        // Don't allow pullup on a subsequent LINE_REFLOW_REDO_NO_PULL pass
+        // Don't allow pullup on a subsequent LineReflowStatus::RedoNoPull pass
         allowPullUp = false;
-      } while (LINE_REFLOW_REDO_NO_PULL == lineReflowStatus);
-    } while (LINE_REFLOW_REDO_MORE_FLOATS == lineReflowStatus);
-  } while (LINE_REFLOW_REDO_NEXT_BAND == lineReflowStatus);
+      } while (LineReflowStatus::RedoNoPull == lineReflowStatus);
+    } while (LineReflowStatus::RedoMoreFloats == lineReflowStatus);
+  } while (LineReflowStatus::RedoNextBand == lineReflowStatus);
 }
 
 void
 nsBlockFrame::PushTruncatedLine(BlockReflowInput& aState,
                                 line_iterator       aLine,
                                 bool*               aKeepReflowGoing)
 {
   PushLines(aState, aLine.prev());
   *aKeepReflowGoing = false;
   NS_FRAME_SET_INCOMPLETE(aState.mReflowStatus);
 }
 
-#ifdef DEBUG
-static const char* LineReflowStatusNames[] = {
-  "LINE_REFLOW_OK", "LINE_REFLOW_STOP", "LINE_REFLOW_REDO_NO_PULL",
-  "LINE_REFLOW_REDO_MORE_FLOATS",
-  "LINE_REFLOW_REDO_NEXT_BAND", "LINE_REFLOW_TRUNCATED"
-};
-#endif
-
 void
 nsBlockFrame::DoReflowInlineFrames(BlockReflowInput& aState,
                                    nsLineLayout& aLineLayout,
                                    line_iterator aLine,
                                    nsFlowAreaRect& aFloatAvailableSpace,
                                    nscoord& aAvailableSpaceHeight,
                                    nsFloatManager::SavedState*
                                      aFloatStateBeforeLine,
@@ -3869,65 +3876,65 @@ nsBlockFrame::DoReflowInlineFrames(Block
       (NS_BLOCK_HAS_FIRST_LETTER_STYLE & mState)) {
     aLineLayout.SetFirstLetterStyleOK(true);
   }
   NS_ASSERTION(!((NS_BLOCK_HAS_FIRST_LETTER_CHILD & mState) &&
                  GetPrevContinuation()),
                "first letter child bit should only be on first continuation");
 
   // Reflow the frames that are already on the line first
-  LineReflowStatus lineReflowStatus = LINE_REFLOW_OK;
+  LineReflowStatus lineReflowStatus = LineReflowStatus::OK;
   int32_t i;
   nsIFrame* frame = aLine->mFirstChild;
 
   if (aFloatAvailableSpace.mHasFloats) {
     // There is a soft break opportunity at the start of the line, because
     // we can always move this line down below float(s).
     if (aLineLayout.NotifyOptionalBreakPosition(
             frame, 0, true, gfxBreakPriority::eNormalBreak)) {
-      lineReflowStatus = LINE_REFLOW_REDO_NEXT_BAND;
+      lineReflowStatus = LineReflowStatus::RedoNextBand;
     }
   }
 
   // need to repeatedly call GetChildCount here, because the child
   // count can change during the loop!
-  for (i = 0; LINE_REFLOW_OK == lineReflowStatus && i < aLine->GetChildCount();
+  for (i = 0; LineReflowStatus::OK == lineReflowStatus && i < aLine->GetChildCount();
        i++, frame = frame->GetNextSibling()) {
     ReflowInlineFrame(aState, aLineLayout, aLine, frame, &lineReflowStatus);
-    if (LINE_REFLOW_OK != lineReflowStatus) {
+    if (LineReflowStatus::OK != lineReflowStatus) {
       // It is possible that one or more of next lines are empty
       // (because of DeleteNextInFlowChild). If so, delete them now
       // in case we are finished.
       ++aLine;
       while ((aLine != end_lines()) && (0 == aLine->GetChildCount())) {
         // XXX Is this still necessary now that DeleteNextInFlowChild
         // uses DoRemoveFrame?
         nsLineBox *toremove = aLine;
         aLine = mLines.erase(aLine);
         NS_ASSERTION(nullptr == toremove->mFirstChild, "bad empty line");
         FreeLineBox(toremove);
       }
       --aLine;
 
-      NS_ASSERTION(lineReflowStatus != LINE_REFLOW_TRUNCATED,
+      NS_ASSERTION(lineReflowStatus != LineReflowStatus::Truncated,
                    "ReflowInlineFrame should never determine that a line "
                    "needs to go to the next page/column");
     }
   }
 
   // Don't pull up new frames into lines with continuation placeholders
   if (aAllowPullUp) {
     // Pull frames and reflow them until we can't
-    while (LINE_REFLOW_OK == lineReflowStatus) {
+    while (LineReflowStatus::OK == lineReflowStatus) {
       frame = PullFrame(aState, aLine);
       if (!frame) {
         break;
       }
 
-      while (LINE_REFLOW_OK == lineReflowStatus) {
+      while (LineReflowStatus::OK == lineReflowStatus) {
         int32_t oldCount = aLine->GetChildCount();
         ReflowInlineFrame(aState, aLineLayout, aLine, frame, &lineReflowStatus);
         if (aLine->GetChildCount() != oldCount) {
           // We just created a continuation for aFrame AND its going
           // to end up on this line (e.g. :first-letter
           // situation). Therefore we have to loop here before trying
           // to pull another frame.
           frame = frame->GetNextSibling();
@@ -3938,39 +3945,40 @@ nsBlockFrame::DoReflowInlineFrames(Block
       }
     }
   }
 
   aState.mFlags.mIsLineLayoutEmpty = aLineLayout.LineIsEmpty();
 
   // We only need to backup if the line isn't going to be reflowed again anyway
   bool needsBackup = aLineLayout.NeedsBackup() &&
-    (lineReflowStatus == LINE_REFLOW_STOP || lineReflowStatus == LINE_REFLOW_OK);
+    (lineReflowStatus == LineReflowStatus::Stop ||
+     lineReflowStatus == LineReflowStatus::OK);
   if (needsBackup && aLineLayout.HaveForcedBreakPosition()) {
     NS_WARNING("We shouldn't be backing up more than once! "
                "Someone must have set a break opportunity beyond the available width, "
                "even though there were better break opportunities before it");
     needsBackup = false;
   }
   if (needsBackup) {
     // We need to try backing up to before a text run
     // XXX It's possible, in fact not unusual, for the break opportunity to already
     // be the end of the line. We should detect that and optimize to not
     // re-do the line.
     if (aLineLayout.HasOptionalBreakPosition()) {
       // We can back up!
-      lineReflowStatus = LINE_REFLOW_REDO_NO_PULL;
+      lineReflowStatus = LineReflowStatus::RedoNoPull;
     }
   } else {
     // In case we reflow this line again, remember that we don't
     // need to force any breaking
     aLineLayout.ClearOptionalBreakPosition();
   }
 
-  if (LINE_REFLOW_REDO_NEXT_BAND == lineReflowStatus) {
+  if (LineReflowStatus::RedoNextBand == lineReflowStatus) {
     // This happens only when we have a line that is impacted by
     // floats and the first element in the line doesn't fit with
     // the floats.
     //
     // What we do is to advance past the first float we find and
     // then reflow the line all over again.
     NS_ASSERTION(NS_UNCONSTRAINEDSIZE !=
                  aFloatAvailableSpace.mRect.BSize(outerWM),
@@ -3998,42 +4006,42 @@ nsBlockFrame::DoReflowInlineFrames(Block
         // and needs to happen after the caller pops the space manager
         // state.
         aState.mFloatManager->AssertStateMatches(aFloatStateBeforeLine);
         aFloatAvailableSpace = aState.GetFloatAvailableSpace();
       } else {
         // There's nowhere to retry placing the line, so we want to push
         // it to the next page/column where its contents can fit not
         // next to a float.
-        lineReflowStatus = LINE_REFLOW_TRUNCATED;
+        lineReflowStatus = LineReflowStatus::Truncated;
         PushTruncatedLine(aState, aLine, aKeepReflowGoing);
       }
     }
 
     // XXX: a small optimization can be done here when paginating:
     // if the new Y coordinate is past the end of the block then
     // push the line and return now instead of later on after we are
     // past the float.
   }
-  else if (LINE_REFLOW_TRUNCATED != lineReflowStatus &&
-           LINE_REFLOW_REDO_NO_PULL != lineReflowStatus) {
+  else if (LineReflowStatus::Truncated != lineReflowStatus &&
+           LineReflowStatus::RedoNoPull != lineReflowStatus) {
     // If we are propagating out a break-before status then there is
     // no point in placing the line.
     if (!NS_INLINE_IS_BREAK_BEFORE(aState.mReflowStatus)) {
       if (!PlaceLine(aState, aLineLayout, aLine, aFloatStateBeforeLine,
                      aFloatAvailableSpace.mRect, aAvailableSpaceHeight,
                      aKeepReflowGoing)) {
-        lineReflowStatus = LINE_REFLOW_REDO_MORE_FLOATS;
+        lineReflowStatus = LineReflowStatus::RedoMoreFloats;
         // PlaceLine already called GetAvailableSpaceForBSize for us.
       }
     }
   }
 #ifdef DEBUG
   if (gNoisyReflow) {
-    printf("Line reflow status = %s\n", LineReflowStatusNames[lineReflowStatus]);
+    printf("Line reflow status = %s\n", LineReflowStatusToString(lineReflowStatus));
   }
 #endif
 
   if (aLineLayout.GetDirtyNextLine()) {
     // aLine may have been pushed to the overflow lines.
     FrameLines* overflowLines = GetOverflowLines();
     // We can't just compare iterators front() to aLine here, since they may be in
     // different lists.
@@ -4070,18 +4078,18 @@ nsBlockFrame::ReflowInlineFrame(BlockRef
                                 line_iterator aLine,
                                 nsIFrame* aFrame,
                                 LineReflowStatus* aLineReflowStatus)
 {
   if (!aFrame) { // XXX change to MOZ_ASSERT(aFrame)
     NS_ERROR("why call me?");
     return;
   }
-  
-  *aLineReflowStatus = LINE_REFLOW_OK;
+
+  *aLineReflowStatus = LineReflowStatus::OK;
 
 #ifdef NOISY_FIRST_LETTER
   ListTag(stdout);
   printf(": reflowing ");
   nsFrame::ListTag(stdout, aFrame);
   printf(" reflowingFirstLetter=%s\n",
          aLineLayout.GetFirstLetterStyleOK() ? "on" : "off");
 #endif
@@ -4119,31 +4127,31 @@ nsBlockFrame::ReflowInlineFrame(BlockRef
   // break-after-not-complete. There are two situations: we are a
   // block or we are an inline. This makes a total of 10 cases
   // (fortunately, there is some overlap).
   aLine->SetBreakTypeAfter(StyleClear::None);
   if (NS_INLINE_IS_BREAK(frameReflowStatus) ||
       StyleClear::None != aState.mFloatBreakType) {
     // Always abort the line reflow (because a line break is the
     // minimal amount of break we do).
-    *aLineReflowStatus = LINE_REFLOW_STOP;
+    *aLineReflowStatus = LineReflowStatus::Stop;
 
     // XXX what should aLine's break-type be set to in all these cases?
     StyleClear breakType = NS_INLINE_GET_BREAK_TYPE(frameReflowStatus);
     MOZ_ASSERT(StyleClear::None != breakType ||
                StyleClear::None != aState.mFloatBreakType, "bad break type");
 
     if (NS_INLINE_IS_BREAK_BEFORE(frameReflowStatus)) {
       // Break-before cases.
       if (aFrame == aLine->mFirstChild) {
         // If we break before the first frame on the line then we must
         // be trying to place content where there's no room (e.g. on a
         // line with wide floats). Inform the caller to reflow the
         // line after skipping past a float.
-        *aLineReflowStatus = LINE_REFLOW_REDO_NEXT_BAND;
+        *aLineReflowStatus = LineReflowStatus::RedoNextBand;
       }
       else {
         // It's not the first child on this line so go ahead and split
         // the line. We will see the frame again on the next-line.
         SplitLine(aState, aLineLayout, aLine, aFrame, aLineReflowStatus);
 
         // If we're splitting the line because the frame didn't fit and it
         // was pushed, then mark the line as having word wrapped. We need to
@@ -4185,25 +4193,25 @@ nsBlockFrame::ReflowInlineFrame(BlockRef
     // Create a continuation for the incomplete frame. Note that the
     // frame may already have a continuation.
     CreateContinuationFor(aState, aLine, aFrame);
 
     // Remember that the line has wrapped
     if (!aLineLayout.GetLineEndsInBR()) {
       aLine->SetLineWrapped(true);
     }
-    
-    // If we just ended a first-letter frame or reflowed a placeholder then 
+
+    // If we just ended a first-letter frame or reflowed a placeholder then
     // don't split the line and don't stop the line reflow...
     // But if we are going to stop anyways we'd better split the line.
-    if ((!(frameReflowStatus & NS_INLINE_BREAK_FIRST_LETTER_COMPLETE) && 
+    if ((!(frameReflowStatus & NS_INLINE_BREAK_FIRST_LETTER_COMPLETE) &&
          nsGkAtoms::placeholderFrame != aFrame->GetType()) ||
-        *aLineReflowStatus == LINE_REFLOW_STOP) {
+        *aLineReflowStatus == LineReflowStatus::Stop) {
       // Split line after the current frame
-      *aLineReflowStatus = LINE_REFLOW_STOP;
+      *aLineReflowStatus = LineReflowStatus::Stop;
       SplitLine(aState, aLineLayout, aLine, aFrame->GetNextSibling(), aLineReflowStatus);
     }
   }
 }
 
 bool
 nsBlockFrame::CreateContinuationFor(BlockReflowInput& aState,
                                     nsLineBox*          aLine,
@@ -4353,22 +4361,22 @@ nsBlockFrame::SplitLine(BlockReflowInput
 #endif
 
     // Let line layout know that some frames are no longer part of its
     // state.
     aLineLayout.SplitLineTo(aLine->GetChildCount());
 
     // If floats have been placed whose placeholders have been pushed to the new
     // line, we need to reflow the old line again. We don't want to look at the
-    // frames in the new line, because as a large paragraph is laid out the 
+    // frames in the new line, because as a large paragraph is laid out the
     // we'd get O(N^2) performance. So instead we just check that the last
     // float and the last below-current-line float are still in aLine.
     if (!CheckPlaceholderInLine(this, aLine, GetLastFloat(aLine)) ||
         !CheckPlaceholderInLine(this, aLine, aState.mBelowCurrentLineFloats.Tail())) {
-      *aLineReflowStatus = LINE_REFLOW_REDO_NO_PULL;
+      *aLineReflowStatus = LineReflowStatus::RedoNoPull;
     }
 
 #ifdef DEBUG
     VerifyLines(true);
 #endif
   }
 }
 
@@ -4458,17 +4466,17 @@ nsBlockFrame::PlaceLine(BlockReflowInput
                                           aAvailableSpaceHeight,
                                           aFloatStateBeforeLine).mRect;
   NS_ASSERTION(aFloatAvailableSpace.BStart(wm) ==
                oldFloatAvailableSpace.BStart(wm), "yikes");
   // Restore the height to the position of the next band.
   aFloatAvailableSpace.BSize(wm) = oldFloatAvailableSpace.BSize(wm);
   // If the available space between the floats is smaller now that we
   // know the height, return false (and cause another pass with
-  // LINE_REFLOW_REDO_MORE_FLOATS).  We ensure aAvailableSpaceHeight
+  // LineReflowStatus::RedoMoreFloats).  We ensure aAvailableSpaceHeight
   // never decreases, which means that we can't reduce the set of floats
   // we intersect, which means that the available space cannot grow.
   if (AvailableSpaceShrunk(wm, oldFloatAvailableSpace, aFloatAvailableSpace,
                            false)) {
     return false;
   }
 
 #ifdef DEBUG
--- a/layout/generic/nsBlockFrame.h
+++ b/layout/generic/nsBlockFrame.h
@@ -13,36 +13,36 @@
 #define nsBlockFrame_h___
 
 #include "nsContainerFrame.h"
 #include "nsHTMLParts.h"
 #include "nsLineBox.h"
 #include "nsCSSPseudoElements.h"
 #include "nsFloatManager.h"
 
-enum LineReflowStatus {
+enum class LineReflowStatus {
   // The line was completely reflowed and fit in available width, and we should
   // try to pull up content from the next line if possible.
-  LINE_REFLOW_OK,
+  OK,
   // The line was completely reflowed and fit in available width, but we should
   // not try to pull up content from the next line.
-  LINE_REFLOW_STOP,
+  Stop,
   // We need to reflow the line again at its current vertical position. The
   // new reflow should not try to pull up any frames from the next line.
-  LINE_REFLOW_REDO_NO_PULL,
+  RedoNoPull,
   // We need to reflow the line again using the floats from its height
   // this reflow, since its height made it hit floats that were not
   // adjacent to its top.
-  LINE_REFLOW_REDO_MORE_FLOATS,
+  RedoMoreFloats,
   // We need to reflow the line again at a lower vertical postion where there
   // may be more horizontal space due to different float configuration.
-  LINE_REFLOW_REDO_NEXT_BAND,
+  RedoNextBand,
   // The line did not fit in the available vertical space. Try pushing it to
   // the next page or column if it's not the first line on the current page/column.
-  LINE_REFLOW_TRUNCATED
+  Truncated
 };
 
 class nsBlockInFlowLineIterator;
 class nsBulletFrame;
 namespace mozilla {
 class BlockReflowInput;
 } // namespace mozilla
 
@@ -141,16 +141,17 @@ public:
 
 #ifdef DEBUG_FRAME_DUMP
   void List(FILE* out = stderr, const char* aPrefix = "", uint32_t aFlags = 0) const override;
   virtual nsresult GetFrameName(nsAString& aResult) const override;
 #endif
 
 #ifdef DEBUG
   virtual nsFrameState GetDebugStateBits() const override;
+  const char* LineReflowStatusToString(LineReflowStatus aLineReflowStatus) const;
 #endif
 
 #ifdef ACCESSIBILITY
   virtual mozilla::a11y::AccType AccessibleType() override;
 #endif
 
   // line cursor methods to speed up searching for the line(s)
   // containing a point. The basic idea is that we set the cursor
--- a/layout/generic/nsLineBox.cpp
+++ b/layout/generic/nsLineBox.cpp
@@ -197,18 +197,17 @@ nsLineBox::BreakTypeToString(StyleClear 
   switch (aBreakType) {
     case StyleClear::None: return "nobr";
     case StyleClear::Left: return "leftbr";
     case StyleClear::Right: return "rightbr";
     case StyleClear::InlineStart: return "inlinestartbr";
     case StyleClear::InlineEnd: return "inlineendbr";
     case StyleClear::Both: return "leftbr+rightbr";
     case StyleClear::Line: return "linebr";
-    default:
-      break;
+    case StyleClear::Max: return "leftbr+rightbr+linebr";
   }
   return "unknown";
 }
 
 char*
 nsLineBox::StateToString(char* aBuf, int32_t aBufSize) const
 {
   snprintf(aBuf, aBufSize, "%s,%s,%s,%s,%s,before:%s,after:%s[0x%x]",
--- a/media/webrtc/signaling/src/media-conduit/AudioConduit.cpp
+++ b/media/webrtc/signaling/src/media-conduit/AudioConduit.cpp
@@ -23,16 +23,17 @@
 #include "mozilla/Telemetry.h"
 #endif
 
 #include "webrtc/common.h"
 #include "webrtc/modules/audio_processing/include/audio_processing.h"
 #include "webrtc/modules/rtp_rtcp/interface/rtp_rtcp.h"
 #include "webrtc/voice_engine/include/voe_dtmf.h"
 #include "webrtc/voice_engine/include/voe_errors.h"
+#include "webrtc/voice_engine/voice_engine_impl.h"
 #include "webrtc/system_wrappers/interface/clock.h"
 
 #ifdef MOZ_WIDGET_ANDROID
 #include "AndroidJNIWrapper.h"
 #endif
 
 namespace mozilla {
 
@@ -234,16 +235,30 @@ bool WebrtcAudioConduit::SetDtmfPayloadT
   int result = mPtrVoEDtmf->SetSendTelephoneEventPayloadType(mChannel, type);
   if (result == -1) {
     CSFLogError(logTag, "%s Failed call to SetSendTelephoneEventPayloadType",
                         __FUNCTION__);
   }
   return result != -1;
 }
 
+bool WebrtcAudioConduit::InsertDTMFTone(int channel, int eventCode,
+                                        bool outOfBand, int lengthMs,
+                                        int attenuationDb) {
+  NS_ASSERTION(!NS_IsMainThread(), "Do not call on main thread");
+
+  if (!mVoiceEngine || !mDtmfEnabled) {
+    return false;
+  }
+
+  webrtc::VoiceEngineImpl* s = static_cast<webrtc::VoiceEngineImpl*>(mVoiceEngine);
+  int result = s->SendTelephoneEvent(channel, eventCode, outOfBand, lengthMs, attenuationDb);
+  return result != -1;
+}
+
 /*
  * WebRTCAudioConduit Implementation
  */
 MediaConduitErrorCode WebrtcAudioConduit::Init()
 {
   CSFLogDebug(logTag,  "%s this=%p", __FUNCTION__, this);
 
 #ifdef MOZ_WIDGET_ANDROID
--- a/media/webrtc/signaling/src/media-conduit/AudioConduit.h
+++ b/media/webrtc/signaling/src/media-conduit/AudioConduit.h
@@ -217,16 +217,19 @@ public:
                              uint32_t *cumulativeLost,
                              int32_t* rttMs) override;
   bool GetRTCPSenderReport(DOMHighResTimeStamp* timestamp,
                            unsigned int* packetsSent,
                            uint64_t* bytesSent) override;
 
   bool SetDtmfPayloadType(unsigned char type) override;
 
+  bool InsertDTMFTone(int channel, int eventCode, bool outOfBand,
+                      int lengthMs, int attenuationDb) override;
+
 private:
   WebrtcAudioConduit(const WebrtcAudioConduit& other) = delete;
   void operator=(const WebrtcAudioConduit& other) = delete;
 
   //Local database of currently applied receive codecs
   typedef std::vector<AudioCodecConfig* > RecvCodecList;
 
   //Function to convert between WebRTC and Conduit codec structures
--- a/media/webrtc/signaling/src/media-conduit/MediaConduitInterface.h
+++ b/media/webrtc/signaling/src/media-conduit/MediaConduitInterface.h
@@ -482,11 +482,14 @@ public:
     * @param enabled: enable extension
     * @param id: id to be used for this rtp header extension
     * NOTE: See AudioConduit for more information
     */
   virtual MediaConduitErrorCode EnableAudioLevelExtension(bool enabled, uint8_t id) = 0;
 
   virtual bool SetDtmfPayloadType(unsigned char type) = 0;
 
+  virtual bool InsertDTMFTone(int channel, int eventCode, bool outOfBand,
+                              int lengthMs, int attenuationDb) = 0;
+
 };
 }
 #endif
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.cpp
@@ -74,16 +74,18 @@
 #include "nsNetUtil.h"
 #include "nsIURLParser.h"
 #include "nsIDOMDataChannel.h"
 #include "nsIDOMLocation.h"
 #include "nsNullPrincipal.h"
 #include "mozilla/PeerIdentity.h"
 #include "mozilla/dom/RTCCertificate.h"
 #include "mozilla/dom/RTCConfigurationBinding.h"
+#include "mozilla/dom/RTCDTMFSenderBinding.h"
+#include "mozilla/dom/RTCDTMFToneChangeEvent.h"
 #include "mozilla/dom/RTCRtpSenderBinding.h"
 #include "mozilla/dom/RTCStatsReportBinding.h"
 #include "mozilla/dom/RTCPeerConnectionBinding.h"
 #include "mozilla/dom/PeerConnectionImplBinding.h"
 #include "mozilla/dom/DataChannelBinding.h"
 #include "mozilla/dom/PerformanceTiming.h"
 #include "mozilla/dom/PluginCrashedEvent.h"
 #include "MediaStreamList.h"
@@ -133,17 +135,16 @@
 
 using namespace mozilla;
 using namespace mozilla::dom;
 
 typedef PCObserverString ObString;
 
 static const char* logTag = "PeerConnectionImpl";
 
-
 // Getting exceptions back down from PCObserver is generally not harmful.
 namespace {
 // This is a terrible hack.  The problem is that SuppressException is not
 // inline, and we link this file without libxul in some cases (e.g. for our test
 // setup).  So we can't use ErrorResult or IgnoredErrorResult because those call
 // SuppressException...  And we can't use FastErrorResult because we can't
 // include BindingUtils.h, because our linking is completely fucked up.  Use
 // BaseErrorResult directly.  Please do not let me see _anyone_ doing this
@@ -2493,16 +2494,29 @@ PeerConnectionImpl::SelectSsrc(MediaStre
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PeerConnectionImpl::RemoveTrack(MediaStreamTrack& aTrack) {
   PC_AUTO_ENTER_API_CALL(true);
 
   std::string trackId = PeerConnectionImpl::GetTrackId(aTrack);
+
+#if !defined(MOZILLA_EXTERNAL_LINKAGE)
+  nsString wideTrackId;
+  aTrack.GetId(wideTrackId);
+  for (size_t i = 0; i < mDTMFStates.Length(); ++i) {
+    if (mDTMFStates[i].mTrackId == wideTrackId) {
+      mDTMFStates[i].mSendTimer->Cancel();
+      mDTMFStates.RemoveElementAt(i);
+      break;
+    }
+  }
+#endif
+
   RefPtr<LocalSourceStreamInfo> info = media()->GetLocalStreamByTrackId(trackId);
 
   if (!info) {
     CSFLogError(logTag, "%s: Unknown stream", __FUNCTION__);
     return NS_ERROR_INVALID_ARG;
   }
 
   nsresult rv =
@@ -2520,21 +2534,140 @@ PeerConnectionImpl::RemoveTrack(MediaStr
 
   aTrack.RemovePrincipalChangeObserver(this);
 
   OnNegotiationNeeded();
 
   return NS_OK;
 }
 
+static int GetDTMFToneCode(uint16_t c)
+{
+  const char* DTMF_TONECODES = "0123456789*#ABCD";
+
+  if (c == ',') {
+    // , is a special character indicating a 2 second delay
+    return -1;
+  }
+
+  const char* i = strchr(DTMF_TONECODES, c);
+  MOZ_ASSERT(i);
+  return i - DTMF_TONECODES;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::InsertDTMF(mozilla::dom::RTCRtpSender& sender,
+                               const nsAString& tones, uint32_t duration,
+                               uint32_t interToneGap) {
+#if !defined(MOZILLA_EXTERNAL_LINKAGE)
+  PC_AUTO_ENTER_API_CALL(false);
+
+  JSErrorResult jrv;
+
+  // Retrieve track
+  RefPtr<MediaStreamTrack> mst = sender.GetTrack(jrv);
+  if (jrv.Failed()) {
+    NS_WARNING("Failed to retrieve track for RTCRtpSender!");
+    return jrv.StealNSResult();
+  }
+
+  nsString senderTrackId;
+  mst->GetId(senderTrackId);
+
+  // Attempt to locate state for the DTMFSender
+  DTMFState* state = nullptr;
+  for (auto& dtmfState : mDTMFStates) {
+    if (dtmfState.mTrackId == senderTrackId) {
+      state = &dtmfState;
+      break;
+    }
+  }
+
+  // No state yet, create a new one
+  if (!state) {
+    state = mDTMFStates.AppendElement();
+    state->mPeerConnectionImpl = this;
+    state->mTrackId = senderTrackId;
+    state->mSendTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+    MOZ_ASSERT(state->mSendTimer);
+  }
+  MOZ_ASSERT(state);
+
+  auto trackPairs = mJsepSession->GetNegotiatedTrackPairs();
+  state->mLevel = -1;
+  for (auto& trackPair : trackPairs) {
+    if (state->mTrackId.EqualsASCII(trackPair.mSending->GetTrackId().c_str())) {
+      if (trackPair.mBundleLevel.isSome()) {
+        state->mLevel = *trackPair.mBundleLevel;
+      } else {
+        state->mLevel = trackPair.mLevel;
+      }
+      break;
+    }
+  }
+
+  state->mTones = tones;
+  state->mDuration = duration;
+  state->mInterToneGap = interToneGap;
+  if (!state->mTones.IsEmpty()) {
+    state->mSendTimer->InitWithFuncCallback(DTMFSendTimerCallback_m, state, 0,
+                                            nsITimer::TYPE_ONE_SHOT);
+  }
+#endif
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::GetDTMFToneBuffer(mozilla::dom::RTCRtpSender& sender,
+                                      nsAString& outToneBuffer) {
+#if !defined(MOZILLA_EXTERNAL_LINKAGE)
+  PC_AUTO_ENTER_API_CALL(false);
+
+  JSErrorResult jrv;
+
+  // Retrieve track
+  RefPtr<MediaStreamTrack> mst = sender.GetTrack(jrv);
+  if (jrv.Failed()) {
+    NS_WARNING("Failed to retrieve track for RTCRtpSender!");
+    return jrv.StealNSResult();
+  }
+
+  nsString senderTrackId;
+  mst->GetId(senderTrackId);
+
+  // Attempt to locate state for the DTMFSender
+  for (auto& dtmfState : mDTMFStates) {
+    if (dtmfState.mTrackId == senderTrackId) {
+      outToneBuffer = dtmfState.mTones;
+      break;
+    }
+  }
+#endif
+
+  return NS_OK;
+}
+
 NS_IMETHODIMP
 PeerConnectionImpl::ReplaceTrack(MediaStreamTrack& aThisTrack,
                                  MediaStreamTrack& aWithTrack) {
   PC_AUTO_ENTER_API_CALL(true);
 
+#if !defined(MOZILLA_EXTERNAL_LINKAGE)
+  nsString trackId;
+  aThisTrack.GetId(trackId);
+
+  for (size_t i = 0; i < mDTMFStates.Length(); ++i) {
+    if (mDTMFStates[i].mTrackId == trackId) {
+      mDTMFStates[i].mSendTimer->Cancel();
+      mDTMFStates.RemoveElementAt(i);
+      break;
+    }
+  }
+#endif
+
   RefPtr<PeerConnectionObserver> pco = do_QueryObjectReferent(mPCObserver);
   if (!pco) {
     return NS_ERROR_UNEXPECTED;
   }
   JSErrorResult jrv;
 
 #if !defined(MOZILLA_EXTERNAL_LINKAGE)
   if (&aThisTrack == &aWithTrack) {
@@ -2941,16 +3074,20 @@ PeerConnectionImpl::RecordEndOfCallTelem
 #endif
 }
 
 nsresult
 PeerConnectionImpl::CloseInt()
 {
   PC_AUTO_ENTER_API_CALL_NO_CHECK();
 
+  for (auto& dtmfState : mDTMFStates) {
+    dtmfState.mSendTimer->Cancel();
+  }
+
   // We do this at the end of the call because we want to make sure we've waited
   // for all trickle ICE candidates to come in; this can happen well after we've
   // transitioned to connected. As a bonus, this allows us to detect race
   // conditions where a stats dispatch happens right as the PC closes.
   if (!mPrivateWindow) {
     RecordLongtermICEStatistics();
   }
   RecordEndOfCallTelemetry();
@@ -3983,9 +4120,68 @@ PeerConnectionImpl::GetRemoteStreams(nsT
     result.AppendElement(info->GetMediaStream());
   }
   return NS_OK;
 #else
   return NS_ERROR_FAILURE;
 #endif
 }
 
+void
+PeerConnectionImpl::DTMFSendTimerCallback_m(nsITimer* timer, void* closure)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  auto state = static_cast<DTMFState*>(closure);
+
+  nsString eventTone;
+  if (!state->mTones.IsEmpty()) {
+    uint16_t toneChar = state->mTones.CharAt(0);
+    int tone = GetDTMFToneCode(toneChar);
+
+    eventTone.Assign(toneChar);
+
+    state->mTones.Cut(0, 1);
+
+    if (tone == -1) {
+      state->mSendTimer->InitWithFuncCallback(DTMFSendTimerCallback_m, state,
+                                              2000, nsITimer::TYPE_ONE_SHOT);
+    } else {
+      // Reset delay if necessary
+      state->mSendTimer->InitWithFuncCallback(DTMFSendTimerCallback_m, state,
+                                              state->mDuration + state->mInterToneGap,
+                                              nsITimer::TYPE_ONE_SHOT);
+
+      RefPtr<AudioSessionConduit> conduit =
+        state->mPeerConnectionImpl->mMedia->GetAudioConduit(state->mLevel);
+
+      if (conduit) {
+        uint32_t duration = state->mDuration;
+        state->mPeerConnectionImpl->mSTSThread->Dispatch(WrapRunnableNM([conduit, tone, duration] () {
+            //Note: We default to channel 0, not inband, and 6dB attenuation.
+            //      here. We might want to revisit these choices in the future.
+            conduit->InsertDTMFTone(0, tone, true, duration, 6);
+          }), NS_DISPATCH_NORMAL);
+      }
+
+    }
+  } else {
+    state->mSendTimer->Cancel();
+  }
+
+#if !defined(MOZILLA_EXTERNAL_LINKAGE)
+  RefPtr<PeerConnectionObserver> pco = do_QueryObjectReferent(state->mPeerConnectionImpl->mPCObserver);
+  if (!pco) {
+    NS_WARNING("Failed to dispatch the RTCDTMFToneChange event!");
+    return;
+  }
+
+  JSErrorResult jrv;
+  pco->OnDTMFToneChange(state->mTrackId, eventTone, jrv);
+
+  if (jrv.Failed()) {
+    NS_WARNING("Failed to dispatch the RTCDTMFToneChange event!");
+    return;
+  }
+#endif
+}
+
 }  // end mozilla namespace
--- a/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
+++ b/media/webrtc/signaling/src/peerconnection/PeerConnectionImpl.h
@@ -71,19 +71,21 @@ class MediaPipeline;
 typedef Fake_DOMMediaStream DOMMediaStream;
 #else
 class DOMMediaStream;
 #endif
 
 namespace dom {
 class RTCCertificate;
 struct RTCConfiguration;
+class RTCDTMFSender;
 struct RTCIceServer;
 struct RTCOfferOptions;
 struct RTCRtpParameters;
+class RTCRtpSender;
 #ifdef USE_FAKE_MEDIA_STREAMS
 typedef Fake_MediaStreamTrack MediaStreamTrack;
 #else
 class MediaStreamTrack;
 #endif
 
 #ifdef USE_FAKE_PCOBSERVER
 typedef test::AFakePCObserver PeerConnectionObserver;
@@ -431,16 +433,29 @@ public:
                                mozilla::dom::MediaStreamTrack& aTrack)
   {
     rv = RemoveTrack(aTrack);
   }
 
   nsresult
   AddTrack(mozilla::dom::MediaStreamTrack& aTrack, DOMMediaStream& aStream);
 
+  NS_IMETHODIMP_TO_ERRORRESULT(InsertDTMF, ErrorResult &rv,
+                               dom::RTCRtpSender& sender,
+                               const nsAString& tones,
+                               uint32_t duration, uint32_t interToneGap) {
+    rv = InsertDTMF(sender, tones, duration, interToneGap);
+  }
+
+  NS_IMETHODIMP_TO_ERRORRESULT(GetDTMFToneBuffer, ErrorResult &rv,
+                               dom::RTCRtpSender& sender,
+                               nsAString& outToneBuffer) {
+    rv = GetDTMFToneBuffer(sender, outToneBuffer);
+  }
+
   NS_IMETHODIMP_TO_ERRORRESULT(ReplaceTrack, ErrorResult &rv,
                                mozilla::dom::MediaStreamTrack& aThisTrack,
                                mozilla::dom::MediaStreamTrack& aWithTrack)
   {
     rv = ReplaceTrack(aThisTrack, aWithTrack);
   }
 
 #if !defined(MOZILLA_EXTERNAL_LINKAGE)
@@ -832,16 +847,32 @@ private:
   bool mNegotiationNeeded;
 
   bool mPrivateWindow;
 
   // storage for Telemetry data
   uint16_t mMaxReceiving[SdpMediaSection::kMediaTypes];
   uint16_t mMaxSending[SdpMediaSection::kMediaTypes];
 
+  // DTMF
+  struct DTMFState {
+    PeerConnectionImpl* mPeerConnectionImpl;
+    nsCOMPtr<nsITimer> mSendTimer;
+    nsString mTrackId;
+    nsString mTones;
+    size_t mLevel;
+    uint32_t mDuration;
+    uint32_t mInterToneGap;
+  };
+
+  static void
+  DTMFSendTimerCallback_m(nsITimer* timer, void*);
+
+  nsTArray<DTMFState> mDTMFStates;
+
 public:
   //these are temporary until the DataChannel Listen/Connect API is removed
   unsigned short listenPort;
   unsigned short connectPort;
   char *connectStr; // XXX ownership/free
 };
 
 // This is what is returned when you acquire on a handle
--- a/media/webrtc/trunk/webrtc/voice_engine/dtmf_inband.cc
+++ b/media/webrtc/trunk/webrtc/voice_engine/dtmf_inband.cc
@@ -29,16 +29,26 @@ const int16_t Dtmf_a_times2Tab16Khz[8]=
 	29144, 28361, 27409, 26258
 };
 
 const int16_t Dtmf_a_times2Tab32Khz[8]=
 {
 	32462,32394, 32311, 32210, 31849, 31647, 31400, 31098
 };
 
+const int16_t Dtmf_a_times2Tab44_1Khz[8]=
+{
+  32607, 32571, 32527, 32474, 32283, 32176, 32045, 31885
+};
+
+const int16_t Dtmf_a_times2Tab48Khz[8]=
+{
+  32612, 32577, 32534, 32483, 32298, 32194, 32067, 31912
+};
+
 // Second table is sin(2*pi*f/fs) in Q14
 
 const int16_t Dtmf_ym2Tab8Khz[8]=
 {
 	8527, 9315, 10163, 11036,
 	13322, 14206, 15021, 15708
 };
 
@@ -48,16 +58,26 @@ const int16_t Dtmf_ym2Tab16Khz[8]=
 	7490, 8207, 8979, 9801
 };
 
 const int16_t Dtmf_ym2Tab32Khz[8]=
 {
 	2235, 2468, 2728, 3010, 3853, 4249, 4685, 5164
 };
 
+const int16_t Dtmf_ym2Tab44_1Khz[8]=
+{
+  1624, 1794, 1984, 2190, 2808, 3100, 3422, 3777
+};
+
+const int16_t Dtmf_ym2Tab48Khz[8]=
+{
+  1599, 1766, 1953, 2156, 2765, 3052, 3369, 3719
+};
+
 const int16_t Dtmf_dBm0kHz[37]=
 {
        16141,      14386,      12821,      11427,      10184,       9077,
         8090,       7210,       6426,       5727,       5104,       4549,
         4054,       3614,       3221,       2870,       2558,       2280,
         2032,       1811,       1614,       1439,       1282,       1143,
         1018,        908,        809,        721,        643,        573,
          510,        455,        405,        361,        322,        287,
@@ -87,17 +107,19 @@ DtmfInband::~DtmfInband()
 	delete &_critSect;
 }
 
 int
 DtmfInband::SetSampleRate(uint16_t frequency)
 {
     if (frequency != 8000 &&
             frequency != 16000 &&
-            frequency != 32000)
+            frequency != 32000 &&
+            frequency != 44100 &&
+            frequency != 48000)
     {
         // invalid sample rate
         assert(false);
         return -1;
     }
     _outputFrequencyHz = frequency;
     return 0;
 }
@@ -277,16 +299,22 @@ DtmfInband::DtmfFix_generate(int16_t *de
         a_times2Tbl=Dtmf_a_times2Tab8Khz;
         y2_Table=Dtmf_ym2Tab8Khz;
     } else if (fs==16000) {
         a_times2Tbl=Dtmf_a_times2Tab16Khz;
         y2_Table=Dtmf_ym2Tab16Khz;
     } else if (fs==32000) {
         a_times2Tbl=Dtmf_a_times2Tab32Khz;
         y2_Table=Dtmf_ym2Tab32Khz;
+    } else if (fs==44100) {
+        a_times2Tbl=Dtmf_a_times2Tab44_1Khz;
+        y2_Table=Dtmf_ym2Tab44_1Khz;
+    } else if (fs==48000) {
+        a_times2Tbl=Dtmf_a_times2Tab48Khz;
+        y2_Table=Dtmf_ym2Tab48Khz;
     } else {
         return(-1);
     }
 
     if ((value==1)||(value==2)||(value==3)||(value==12)) {
         a1_times2=a_times2Tbl[0];
         if (_reinit) {
             _oldOutputLow[0]=y2_Table[0];
--- a/media/webrtc/trunk/webrtc/voice_engine/output_mixer.cc
+++ b/media/webrtc/trunk/webrtc/voice_engine/output_mixer.cc
@@ -594,16 +594,32 @@ void OutputMixer::APMAnalyzeReverseStrea
 //                             Private methods
 // ----------------------------------------------------------------------------
 
 int
 OutputMixer::InsertInbandDtmfTone()
 {
     uint16_t sampleRate(0);
     _dtmfGenerator.GetSampleRate(sampleRate);
+
+    // We're not using a supported sample rate for the DtmfInband generator, so
+    // we won't be able to generate feedback tones.
+    if (!(_audioFrame.sample_rate_hz_ == 8000 ||
+          _audioFrame.sample_rate_hz_ == 16000 ||
+          _audioFrame.sample_rate_hz_ == 32000 ||
+          _audioFrame.sample_rate_hz_ == 44100 ||
+          _audioFrame.sample_rate_hz_ == 48000)) {
+
+        WEBRTC_TRACE(kTraceError, kTraceVoice, VoEId(_instanceId, -1),
+                     "OutputMixer::InsertInbandDtmfTone() Sample rate"
+                     "not supported");
+
+        return -1;
+    }
+
     if (sampleRate != _audioFrame.sample_rate_hz_)
     {
         // Update sample rate of Dtmf tone since the mixing frequency changed.
         _dtmfGenerator.SetSampleRate(
             (uint16_t)(_audioFrame.sample_rate_hz_));
         // Reset the tone to be added taking the new sample rate into account.
         _dtmfGenerator.ResetTone();
     }
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -84,17 +84,16 @@ android {
         main {
             manifest.srcFile "${project.buildDir}/generated/source/preprocessed_manifest/AndroidManifest.xml"
 
             aidl {
                 srcDir "${topsrcdir}/mobile/android/base/aidl"
             }
 
             java {
-                srcDir "${topsrcdir}/mobile/android/geckoview/src/main/java"
                 srcDir "${topsrcdir}/mobile/android/base/java"
                 srcDir "${topsrcdir}/mobile/android/search/java"
                 srcDir "${topsrcdir}/mobile/android/javaaddons/java"
                 srcDir "${topsrcdir}/mobile/android/services/src/main/java"
 
                 if (mozconfig.substs.MOZ_ANDROID_MLS_STUMBLER) {
                     srcDir "${topsrcdir}/mobile/android/stumbler/java"
                 }
@@ -225,16 +224,17 @@ dependencies {
     // of this library.
     // It doesn't seem like there is a non-trivial way to be conditional on 'localOld', so instead we explicitly
     // define a version of leakcanary for every flavor:
     localCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta1'
     localOldCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
     automationCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
     testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta1'
 
+    compile project(':geckoview')
     compile project(':thirdparty')
 
     testCompile 'junit:junit:4.12'
     testCompile 'org.robolectric:robolectric:3.1.2'
     testCompile 'org.simpleframework:simple-http:6.0.1'
     testCompile 'org.mockito:mockito-core:1.10.19'
 
     // Including the Robotium JAR directly can cause issues with dexing.
@@ -248,50 +248,22 @@ task checkstyle(type: Checkstyle) {
     // TODO: should use sourceSets from project instead of hard-coded str.
     source '../base/java/'
     // TODO: This ignores our pre-processed resources.
     include '**/*.java'
     // TODO: classpath should probably be something.
     classpath = files()
 }
 
-task syncOmnijarFromDistDir(type: Sync) {
-    into("${project.buildDir}/generated/omnijar")
-    from("${topobjdir}/dist/fennec/assets") {
-        include 'omni.ja'
-    }
-}
-
-task checkLibsExistInDistDir<< {
-    if (syncLibsFromDistDir.source.empty) {
-        throw new GradleException("Required JNI libraries not found in ${topobjdir}/dist/fennec/lib.  Have you built and packaged?")
-    }
-}
-
-task syncLibsFromDistDir(type: Sync, dependsOn: checkLibsExistInDistDir) {
-    into("${project.buildDir}/generated/jniLibs")
-    from("${topobjdir}/dist/fennec/lib")
-}
-
-task checkAssetsExistInDistDir<< {
-    if (syncAssetsFromDistDir.source.empty) {
-        throw new GradleException("Required assets not found in ${topobjdir}/dist/fennec/assets.  Have you built and packaged?")
-    }
-}
-
-task syncAssetsFromDistDir(type: Sync, dependsOn: checkAssetsExistInDistDir) {
-    into("${project.buildDir}/generated/assets")
-    from("${topobjdir}/dist/fennec/assets") {
-        exclude 'omni.ja'
-    }
-}
-
 task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
     into("${project.buildDir}/generated/source/preprocessed_code")
-    from("${topobjdir}/mobile/android/base/generated/preprocessed")
+    from("${topobjdir}/mobile/android/base/generated/preprocessed") {
+        // All other preprocessed code is included in the geckoview project.
+        include '**/AdjustConstants.java'
+    }
 }
 
 // The localization system uses the moz.build preprocessor to interpolate a .dtd
 // file of XML entity definitions into an XML file of elements referencing those
 // entities.  (Each locale produces its own .dtd file, backstopped by the en-US
 // .dtd file in tree.)  Android Studio (and IntelliJ) don't handle these inline
 // entities smoothly.  This filter merely expands the entities in place, making
 // them appear properly throughout the IDE.  Be aware that this assumes that the
@@ -307,82 +279,48 @@ class ExpandXMLEntitiesFilter extends Fi
 task syncPreprocessedResources(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
     into("${project.buildDir}/generated/source/preprocessed_resources")
     from("${topobjdir}/mobile/android/base/res")
     filesMatching('**/strings.xml') {
         filter(ExpandXMLEntitiesFilter)
     }
 }
 
-// The omnijar inputs are listed as resource directory inputs to a dummy JAR.
-// That arrangement labels them nicely in IntelliJ.  See the comment in the
-// :omnijar project for more context.
-evaluationDependsOn(':omnijar')
-
-task buildOmnijar(type:Exec) {
-    dependsOn rootProject.generateCodeAndResources
-
-    // See comment in :omnijar project regarding interface mismatches here.
-    inputs.source project(':omnijar').sourceSets.main.resources.srcDirs
-
-    // Produce a single output file.
-    outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
-
-    workingDir "${topobjdir}"
-
-    commandLine mozconfig.substs.GMAKE
-    args '-C'
-    args "${topobjdir}/mobile/android/base"
-    args 'gradle-omnijar'
-
-    // Only show the output if something went wrong.
-    ignoreExitValue = true
-    standardOutput = new ByteArrayOutputStream()
-    errorOutput = standardOutput
-    doLast {
-        if (execResult.exitValue != 0) {
-            throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
-        }
-    }
-}
-
 // It's not easy -- see the backout in Bug 1242213 -- to change the <manifest>
 // package for Fennec.  Gradle has grown a mechanism to achieve what we want for
 // Fennec, however, with applicationId.  To use the same manifest as moz.build,
 // we replace the package with org.mozilla.gecko (the eventual package) here.
 task rewriteManifestPackage(type: Copy, dependsOn: rootProject.generateCodeAndResources) {
     into("${project.buildDir}/generated/source/preprocessed_manifest")
     from("${topobjdir}/mobile/android/base/AndroidManifest.xml")
     filter { it.replaceFirst(/package=".*?"/, 'package="org.mozilla.gecko"') }
 }
 
+apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
+
 android.applicationVariants.all { variant ->
     variant.preBuild.dependsOn rewriteManifestPackage
     variant.preBuild.dependsOn syncPreprocessedCode
     variant.preBuild.dependsOn syncPreprocessedResources
 
+    // Automation builds don't include Gecko binaries, since those binaries are
+    // not produced until after build time (at package time).  Therefore,
+    // automation builds include the Gecko binaries into the APK at package
+    // time.  The "withGeckoBinaries" variant of the :geckoview project also
+    // does this.  (It does what it says on the tin!)  For notes on this
+    // approach, see mobile/android/gradle/with_gecko_binaries.gradle.
+
     // Like 'local' or 'localOld'.
     def productFlavor = variant.productFlavors[0].name
-    // Like 'debug' or 'release'.
-    def buildType = variant.buildType.name
 
-    // We insert omni.ja and the .so libraries into all local builds.
-    if (!productFlavor.startsWith('local')) {
-        return
+    // :app uses :geckoview:release and handles it's own Gecko binary inclusion,
+    // even though this would be most naturally done in the :geckoview project.
+    if (!productFlavor.equals('automation')) {
+        configureVariantWithGeckoBinaries(variant)
     }
-
-    syncOmnijarFromDistDir.dependsOn buildOmnijar
-    def generateAssetsTask = tasks.findByName("generate${productFlavor.capitalize()}${buildType.capitalize()}Assets")
-    generateAssetsTask.dependsOn syncOmnijarFromDistDir
-    generateAssetsTask.dependsOn syncLibsFromDistDir
-    generateAssetsTask.dependsOn syncAssetsFromDistDir
-
-    android.sourceSets."${productFlavor}${buildType.capitalize()}".assets.srcDir syncOmnijarFromDistDir.destinationDir
-    android.sourceSets."${productFlavor}${buildType.capitalize()}".assets.srcDir syncAssetsFromDistDir.destinationDir
-    android.sourceSets."${productFlavor}${buildType.capitalize()}".jniLibs.srcDir syncLibsFromDistDir.destinationDir
 }
 
 apply plugin: 'spoon'
 
 spoon {
     // For now, let's be verbose.
     debug = true
     // It's not helpful to pass when we don't have a device connected.
@@ -421,37 +359,37 @@ afterEvaluate {
 
 // Bug 1299015: Complain to treeherder if checkstyle, lint, or unittest fails.  It's not obvious
 // how to listen to individual errors in most cases, so we just link to the reports for now.
 def makeTaskExecutionListener(artifactRootUrl) {
     return new TaskExecutionListener() {
         void beforeExecute(Task task) {
             // Do nothing.
         }
-    
+
         void afterExecute(Task task, TaskState state) {
             if (!state.failure) {
                 return
             }
-    
+
             // Link to the failing report.  The task path and the report path
             // depend on the android-lint task in
             // taskcluster/ci/android-stuff/kind.yml.  It's not possible to link
             // directly, so for now consumers will need to copy-paste the URL.
             switch (task.path) {
             case ':app:checkstyle':
                 def url = "${artifactRootUrl}/public/android/checkstyle/checkstyle.xml"
                 println "TEST-UNEXPECTED-FAIL | android-checkstyle | Checkstyle rule violations were found. See the report at: $url"
                 break
-    
+
             case ':app:lintAutomationDebug':
                 def url = "${artifactRootUrl}/public/android/lint/lint-results-automationDebug.html"
                 println "TEST-UNEXPECTED-FAIL | android-lint | Lint found errors in the project; aborting build. See the report at: $url"
                 break
-    
+
             case ':app:testAutomationDebugUnitTest':
                 def url = "${artifactRootUrl}/public/android/unittest/automationDebug/index.html"
                 println "TEST-UNEXPECTED-FAIL | android-test | There were failing tests. See the report at: $url"
                 break
             }
         }
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
@@ -28,17 +28,17 @@ import java.util.Timer;
 import java.util.TimerTask;
 
 import android.util.Log;
 
 class ActionBarTextSelection implements TextSelection, GeckoEventListener {
     private static final String LOGTAG = "GeckoTextSelection";
     private static final int SHUTDOWN_DELAY_MS = 250;
 
-    private final TextSelectionHandle anchorHandle;
+    private final Context context;
 
     private boolean mDraggingHandles;
 
     private String selectionID; // Unique ID provided for each selection action.
 
     private String mCurrentItems;
 
     private TextSelectionActionModeCallback mCallback;
@@ -54,25 +54,25 @@ class ActionBarTextSelection implements 
                 public void run() {
                     endActionMode();
                 }
             });
         }
     };
     private ActionModeTimerTask mActionModeTimerTask;
 
-    ActionBarTextSelection(TextSelectionHandle anchorHandle) {
-        this.anchorHandle = anchorHandle;
+    ActionBarTextSelection(Context context) {
+        this.context = context;
     }
 
     @Override
     public void create() {
         // Only register listeners if we have valid start/middle/end handles
-        if (anchorHandle == null) {
-            Log.e(LOGTAG, "Failed to initialize text selection because at least one handle is null");
+        if (context == null) {
+            Log.e(LOGTAG, "Failed to initialize text selection because at least one context is null");
         } else {
             GeckoApp.getEventDispatcher().registerGeckoThreadListener(this,
                 "TextSelection:ActionbarInit",
                 "TextSelection:ActionbarStatus",
                 "TextSelection:ActionbarUninit",
                 "TextSelection:Update");
         }
     }
@@ -80,18 +80,18 @@ class ActionBarTextSelection implements 
     @Override
     public boolean dismiss() {
         // We do not call endActionMode() here because this is already handled by the activity.
         return false;
     }
 
     @Override
     public void destroy() {
-        if (anchorHandle == null) {
-            Log.e(LOGTAG, "Do not unregister TextSelection:* listeners since anchorHandle is null");
+        if (context == null) {
+            Log.e(LOGTAG, "Do not unregister TextSelection:* listeners since context is null");
         } else {
             GeckoApp.getEventDispatcher().unregisterGeckoThreadListener(this,
                     "TextSelection:ActionbarInit",
                     "TextSelection:ActionbarStatus",
                     "TextSelection:ActionbarUninit",
                     "TextSelection:Update");
         }
     }
@@ -149,27 +149,25 @@ class ActionBarTextSelection implements 
         }
         mCurrentItems = itemsString;
 
         if (mCallback != null) {
             mCallback.updateItems(items);
             return;
         }
 
-        final Context context = anchorHandle.getContext();
         if (context instanceof ActionModeCompat.Presenter) {
             final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
             mCallback = new TextSelectionActionModeCallback(items);
             presenter.startActionModeCompat(mCallback);
             mCallback.animateIn();
         }
     }
 
     private void endActionMode() {
-        Context context = anchorHandle.getContext();
         if (context instanceof ActionModeCompat.Presenter) {
             final ActionModeCompat.Presenter presenter = (ActionModeCompat.Presenter) context;
             presenter.endActionModeCompat();
         }
         mCurrentItems = null;
     }
 
     private class TextSelectionActionModeCallback implements Callback {
@@ -205,17 +203,17 @@ class ActionBarTextSelection implements 
             for (int i = 0; i < length; i++) {
                 try {
                     final JSONObject obj = mItems.getJSONObject(i);
                     final GeckoMenuItem menuitem = (GeckoMenuItem) menu.add(0, i, 0, obj.optString("label"));
                     final int actionEnum = obj.optBoolean("showAsAction") ? GeckoMenuItem.SHOW_AS_ACTION_ALWAYS : GeckoMenuItem.SHOW_AS_ACTION_NEVER;
                     menuitem.setShowAsAction(actionEnum, R.attr.menuItemActionModeStyle);
 
                     final String iconString = obj.optString("icon");
-                    BitmapUtils.getDrawable(anchorHandle.getContext(), iconString, new BitmapLoader() {
+                    BitmapUtils.getDrawable(context, iconString, new BitmapLoader() {
                         @Override
                         public void onBitmapFound(Drawable d) {
                             if (d != null) {
                                 menuitem.setIcon(d);
                             }
                         }
                     });
                 } catch (Exception ex) {
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -1249,18 +1249,17 @@ public abstract class GeckoApp
         mLayerView = (GeckoView) findViewById(R.id.layer_view);
 
         Tabs.getInstance().attachToContext(this, mLayerView);
 
         // Use global layout state change to kick off additional initialization
         mMainLayout.getViewTreeObserver().addOnGlobalLayoutListener(this);
 
         if (Versions.preMarshmallow) {
-            mTextSelection = new ActionBarTextSelection(
-                    (TextSelectionHandle) findViewById(R.id.anchor_handle));
+            mTextSelection = new ActionBarTextSelection(this);
         } else {
             mTextSelection = new FloatingToolbarTextSelection(this, mLayerView);
         }
         mTextSelection.create();
 
         // Determine whether we should restore tabs.
         mLastSessionCrashed = updateCrashedState();
         mShouldRestore = getSessionRestoreState(savedInstanceState);
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/TextSelectionHandle.java
+++ /dev/null
@@ -1,219 +0,0 @@
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-package org.mozilla.gecko;
-
-import org.mozilla.gecko.animation.ViewHelper;
-import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
-import org.mozilla.gecko.gfx.LayerView;
-
-import org.json.JSONObject;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Point;
-import android.graphics.PointF;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.MotionEvent;
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.RelativeLayout;
-
-/**
- * Text selection handles enable a user to change position of selected text in
- * Gecko's DOM structure.
- *
- * A text "Selection" or nsISelection object, has start and end positions,
- * referred to as Anchor and Focus objects.
- *
- * If the Anchor and Focus objects are at the same point, it represents a text
- * selection Caret, commonly diplayed as a blinking, vertical |.
- *
- * Anchor and Focus objects each represent a DOM node, and character offset
- * from the start of the node. The Anchor always refers to the start of the
- * Selection, and the Focus refers to its end.
- *
- * In LTR languages such as English, the Anchor is to the left of the Focus.
- * In RTL languages such as Hebrew, the Anchor is to the right of the Focus.
- *
- * For multi-line Selections, in both LTR and RTL languages, the Anchor starts
- * above the Focus.
- */
-class TextSelectionHandle extends ImageView implements View.OnTouchListener {
-    private static final String LOGTAG = "GeckoTextSelectionHandle";
-
-    public enum HandleType { ANCHOR, CARET, FOCUS };
-
-    private final HandleType mHandleType;
-    private final int mWidth;
-    private final int mHeight;
-    private final int mShadow;
-
-    private float mLeft;
-    private float mTop;
-    private boolean mIsRTL;
-    private PointF mGeckoPoint;
-    private PointF mTouchStart;
-
-    private RelativeLayout.LayoutParams mLayoutParams;
-
-    private static final int IMAGE_LEVEL_LTR = 0;
-    private static final int IMAGE_LEVEL_RTL = 1;
-
-    public TextSelectionHandle(Context context, AttributeSet attrs) {
-        super(context, attrs);
-        setOnTouchListener(this);
-
-        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TextSelectionHandle);
-        int handleType = a.getInt(R.styleable.TextSelectionHandle_handleType, 0x01);
-        a.recycle();
-
-        if (handleType == 0x01)
-            mHandleType = HandleType.ANCHOR;
-        else if (handleType == 0x02)
-            mHandleType = HandleType.CARET;
-        else
-            mHandleType = HandleType.FOCUS;
-
-        mGeckoPoint = new PointF(0.0f, 0.0f);
-        mTouchStart = new PointF(0.0f, 0.0f);
-
-        mWidth = getResources().getDimensionPixelSize(R.dimen.text_selection_handle_width);
-        mHeight = getResources().getDimensionPixelSize(R.dimen.text_selection_handle_height);
-        mShadow = getResources().getDimensionPixelSize(R.dimen.text_selection_handle_shadow);
-    }
-
-    private int getStatusBarHeight() {
-        int result = 0;
-        int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
-        if (resourceId > 0) {
-            result = getResources().getDimensionPixelSize(resourceId);
-        }
-        return result;
-    }
-
-    @Override
-    public boolean onTouch(View v, MotionEvent event) {
-        switch (event.getActionMasked()) {
-            case MotionEvent.ACTION_DOWN: {
-                mTouchStart.x = event.getX();
-                mTouchStart.y = event.getY();
-                break;
-            }
-            case MotionEvent.ACTION_UP: {
-                mTouchStart.x = 0;
-                mTouchStart.y = 0;
-
-                // Reposition handles to line up with ends of selection
-                JSONObject args = new JSONObject();
-                try {
-                    args.put("handleType", mHandleType.toString());
-                } catch (Exception e) {
-                    Log.e(LOGTAG, "Error building JSON arguments for TextSelection:Position");
-                }
-                GeckoAppShell.notifyObservers("TextSelection:Position", args.toString());
-                break;
-            }
-            case MotionEvent.ACTION_MOVE: {
-                move(event.getRawX(), event.getRawY());
-                break;
-            }
-        }
-        return true;
-    }
-
-    private void move(float newX, float newY) {
-        LayerView layerView = GeckoAppShell.getLayerView();
-
-        // newX and newY are in screen coordinates, but mLeft/mTop are relative
-        // to the ancestor (which is what LayerView is relative to also). So,
-        // we need to adjust newX/newY. The |ancestorOrigin| variable computed
-        // below is the origin of the ancestor relative to the screen coordinates,
-        // so subtracting that from newY puts newY into the desired coordinate
-        // space. We also need to include the offset amount of the touch location
-        // relative to the top left of the handle (mTouchStart).
-        int[] layerViewPosition = new int[2];
-        layerView.getLocationOnScreen(layerViewPosition);
-        float ancestorOrigin = layerViewPosition[1];
-
-        mLeft = newX - mTouchStart.x;
-        mTop = newY - mTouchStart.y - ancestorOrigin;
-
-        // Send x coordinate on the right side of the start handle, left side of the end handle.
-        float layerViewTranslation = layerView.getSurfaceTranslation();
-        PointF geckoPoint = new PointF(mLeft + adjustLeftForHandle(),
-                                       mTop - layerViewTranslation);
-        geckoPoint = layerView.convertViewPointToLayerPoint(geckoPoint);
-
-        JSONObject args = new JSONObject();
-        try {
-            args.put("handleType", mHandleType.toString());
-            args.put("x", (int) geckoPoint.x);
-            args.put("y", (int) geckoPoint.y);
-        } catch (Exception e) {
-            Log.e(LOGTAG, "Error building JSON arguments for TextSelection:Move");
-        }
-        GeckoAppShell.notifyObservers("TextSelection:Move", args.toString());
-
-        // If we're positioning a cursor, don't move the handle here. Gecko
-        // will tell us the position of the caret, so we set the handle
-        // position then. This allows us to lock the handle to wherever the
-        // caret appears.
-        if (mHandleType != HandleType.CARET) {
-            setLayoutPosition();
-        }
-    }
-
-    void positionFromGecko(int left, int top, boolean rtl) {
-        LayerView layerView = GeckoAppShell.getLayerView();
-        if (layerView == null) {
-            Log.e(LOGTAG, "Can't position handle because layerView is null");
-            return;
-        }
-
-        mGeckoPoint = new PointF(left, top);
-        if (mIsRTL != rtl) {
-            mIsRTL = rtl;
-            setImageLevel(mIsRTL ? IMAGE_LEVEL_RTL : IMAGE_LEVEL_LTR);
-        }
-
-        ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
-        repositionWithViewport(metrics.viewportRectLeft, metrics.viewportRectTop, metrics.zoomFactor);
-    }
-
-    void repositionWithViewport(float x, float y, float zoom) {
-        PointF viewPoint = new PointF((mGeckoPoint.x * zoom) - x,
-                                      (mGeckoPoint.y * zoom) - y);
-        mLeft = viewPoint.x - adjustLeftForHandle();
-        mTop = viewPoint.y + GeckoAppShell.getLayerView().getSurfaceTranslation();
-
-        setLayoutPosition();
-    }
-
-    private float adjustLeftForHandle() {
-        if (mHandleType == HandleType.ANCHOR) {
-            return mIsRTL ? mShadow : mWidth - mShadow;
-        } else if (mHandleType == HandleType.CARET) {
-            return mWidth / 2;
-        } else {
-            return mIsRTL ? mWidth - mShadow : mShadow;
-        }
-    }
-
-    private void setLayoutPosition() {
-        if (mLayoutParams == null) {
-            mLayoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
-            // Set negative right/bottom margins so that the handles can be dragged outside of
-            // the content area (if they are dragged to the left/top, the dyanmic margins set
-            // below will take care of that).
-            mLayoutParams.rightMargin = 0 - mWidth;
-            mLayoutParams.bottomMargin = 0 - mHeight;
-        }
-
-        mLayoutParams.leftMargin = (int) mLeft;
-        mLayoutParams.topMargin = (int) mTop;
-        setLayoutParams(mLayoutParams);
-    }
-}
--- a/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/HomeConfigPrefsBackend.java
@@ -217,27 +217,42 @@ public class HomeConfigPrefsBackend impl
     /**
      * Iterate over all homepanels to verify that there is at least one default panel. If there is
      * no default panel, set History as the default panel. (This is only relevant for two botched
      * migrations where the history panel should have been made the default panel, but wasn't.)
      */
     private static void ensureDefaultPanelForV5orV8(Context context, JSONArray jsonPanels) throws JSONException {
         int historyIndex = -1;
 
+        // If all panels are disabled, there is no default panel - this is the only valid state
+        // that has no default. We can use this flag to track whether any visible panels have been
+        // found.
+        boolean enabledPanelsFound = false;
+
         for (int i = 0; i < jsonPanels.length(); i++) {
             final PanelConfig panelConfig = new PanelConfig(jsonPanels.getJSONObject(i));
             if (panelConfig.isDefault()) {
                 return;
             }
 
+            if (!panelConfig.isDisabled()) {
+                enabledPanelsFound = true;
+            }
+
             if (panelConfig.getType() == PanelType.COMBINED_HISTORY) {
                 historyIndex = i;
             }
         }
 
+        if (!enabledPanelsFound) {
+            // No panels are enabled, hence there can be no default (see noEnabledPanelsFound declaration
+            // for more information).
+            return;
+        }
+
         // Make the History panel default. We can't modify existing PanelConfigs, so make a new one.
         final PanelConfig historyPanelConfig = createBuiltinPanelConfig(context, PanelType.COMBINED_HISTORY, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL));
         jsonPanels.put(historyIndex, historyPanelConfig.toJSON());
     }
 
     /**
      * Removes a panel from the home panel config.
      * If the removed panel was set as the default home panel, we provide a replacement for it.
@@ -250,46 +265,48 @@ public class HomeConfigPrefsBackend impl
      * @param alwaysUnhide If true, the replacement panel will always be unhidden,
      *                     otherwise only if we turn it into the new default panel.
      * @return new array of updated JSON panels
      * @throws JSONException
      */
     private static JSONArray removePanel(Context context, JSONArray jsonPanels,
                                          PanelType panelToRemove, PanelType replacementPanel, boolean alwaysUnhide) throws JSONException {
         boolean wasDefault = false;
+        boolean wasDisabled = false;
         int replacementPanelIndex = -1;
         boolean replacementWasDefault = false;
 
         // JSONArrary doesn't provide remove() for API < 19, therefore we need to manually copy all
         // the items we don't want deleted into a new array.
         final JSONArray newJSONPanels = new JSONArray();
 
         for (int i = 0; i < jsonPanels.length(); i++) {
             final JSONObject panelJSON = jsonPanels.getJSONObject(i);
             final PanelConfig panelConfig = new PanelConfig(panelJSON);
 
             if (panelConfig.getType() == panelToRemove) {
                 // If this panel was the default we'll need to assign a new default:
                 wasDefault = panelConfig.isDefault();
+                wasDisabled = panelConfig.isDisabled();
             } else {
                 if (panelConfig.getType() == replacementPanel) {
                     replacementPanelIndex = newJSONPanels.length();
                     if (panelConfig.isDefault()) {
                         replacementWasDefault = true;
                     }
                 }
 
                 newJSONPanels.put(panelJSON);
             }
         }
 
         // Unless alwaysUnhide is true, we make the replacement panel visible only if it is going
         // to be the new default panel, since a hidden default panel doesn't make sense.
         // This is to allow preserving the behaviour of the original reading list migration function.
-        if (wasDefault || alwaysUnhide) {
+        if ((wasDefault || alwaysUnhide) && !wasDisabled) {
             final JSONObject replacementPanelConfig;
             if (wasDefault) {
                 // If the removed panel was the default, the replacement has to be made the new default
                 replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON();
             } else {
                 final EnumSet<HomeConfig.PanelConfig.Flags> flags;
                 if (replacementWasDefault) {
                     // However if the replacement panel was already default, we need to preserve it's default status
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -685,17 +685,16 @@ gbjar.sources += ['java/org/mozilla/geck
     'telemetry/TelemetryPing.java',
     'telemetry/TelemetryPreferences.java',
     'telemetry/TelemetryUploadService.java',
     'TelemetryContract.java',
     'text/FloatingActionModeCallback.java',
     'text/FloatingToolbarTextSelection.java',
     'text/TextAction.java',
     'text/TextSelection.java',
-    'TextSelectionHandle.java',
     'ThumbnailHelper.java',
     'toolbar/AutocompleteHandler.java',
     'toolbar/BackButton.java',
     'toolbar/BrowserToolbar.java',
     'toolbar/BrowserToolbarPhone.java',
     'toolbar/BrowserToolbarPhoneBase.java',
     'toolbar/BrowserToolbarTablet.java',
     'toolbar/BrowserToolbarTabletBase.java',
--- a/mobile/android/base/resources/layout/gecko_app.xml
+++ b/mobile/android/base/resources/layout/gecko_app.xml
@@ -37,18 +37,16 @@
                             android:layout_width="match_parent"
                             android:layout_height="match_parent"/>
 
             <org.mozilla.gecko.FormAssistPopup android:id="@+id/form_assist_popup"
                                                android:layout_width="match_parent"
                                                android:layout_height="match_parent"
                                                android:visibility="gone"/>
 
-            <include layout="@layout/text_selection_handles"/>
-
             <FrameLayout android:id="@+id/camera_layout"
                          android:layout_height="wrap_content"
                          android:layout_width="wrap_content"
                          android:layout_alignParentRight="true"
                          android:layout_alignParentBottom="true">
             </FrameLayout>
 
             <view class="org.mozilla.gecko.media.VideoPlayer" android:id="@+id/video_player"
deleted file mode 100644
--- a/mobile/android/base/resources/layout/text_selection_handles.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- This Source Code Form is subject to the terms of the Mozilla Public
-   - License, v. 2.0. If a copy of the MPL was not distributed with this
-   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
-
-<merge xmlns:android="http://schemas.android.com/apk/res/android"
-       xmlns:gecko="http://schemas.android.com/apk/res-auto">
-
-   <org.mozilla.gecko.TextSelectionHandle android:id="@+id/anchor_handle"
-                                          android:layout_width="@dimen/text_selection_handle_width"
-                                          android:layout_height="@dimen/text_selection_handle_height"
-                                          android:src="@drawable/handle_anchor_level"
-                                          android:visibility="gone"
-                                          gecko:handleType="start"/>
-
-   <org.mozilla.gecko.TextSelectionHandle android:id="@+id/caret_handle"
-                                          android:layout_width="@dimen/text_selection_handle_width"
-                                          android:layout_height="@dimen/text_selection_handle_height"
-                                          android:src="@drawable/handle_middle"
-                                          android:visibility="gone"
-                                          gecko:handleType="middle"/>
-
-   <org.mozilla.gecko.TextSelectionHandle android:id="@+id/focus_handle"
-                                          android:layout_width="@dimen/text_selection_handle_width"
-                                          android:layout_height="@dimen/text_selection_handle_height"
-                                          android:src="@drawable/handle_focus_level"
-                                          android:visibility="gone"
-                                          gecko:handleType="end"/>
-</merge>
--- a/mobile/android/base/resources/values/attrs.xml
+++ b/mobile/android/base/resources/values/attrs.xml
@@ -88,24 +88,16 @@
             <flag name="tabs_private" value ="0x01" />
         </attr>
     </declare-styleable>
 
     <declare-styleable name="TabCounter">
         <attr name="android:layout"/>
     </declare-styleable>
 
-    <declare-styleable name="TextSelectionHandle">
-        <attr name="handleType">
-            <flag name="start" value="0x01" />
-            <flag name="middle" value="0x02" />
-            <flag name="end" value="0x03" />
-        </attr>
-    </declare-styleable>
-
     <declare-styleable name="PrivateBrowsing">
         <attr name="state_private" format="boolean"/>
     </declare-styleable>
 
     <declare-styleable name="LightweightTheme">
         <attr name="state_light" format="boolean"/>
         <attr name="state_dark" format="boolean"/>
         <attr name="autoUpdateTheme" format="boolean"/>
--- a/mobile/android/base/resources/values/dimens.xml
+++ b/mobile/android/base/resources/values/dimens.xml
@@ -134,19 +134,16 @@
     <dimen name="prompt_service_left_right_text_with_icon_padding">10dp</dimen>
     <dimen name="prompt_service_top_bottom_text_with_icon_padding">8dp</dimen>
     <dimen name="tabs_panel_indicator_width">60dp</dimen>
     <dimen name="tabs_panel_button_width">48dp</dimen>
     <dimen name="tabs_strip_height">48dp</dimen>
     <dimen name="tabs_strip_button_width">100dp</dimen>
     <dimen name="tabs_strip_button_padding">18dp</dimen>
     <dimen name="tabs_strip_shadow_size">1dp</dimen>
-    <dimen name="text_selection_handle_width">47dp</dimen>
-    <dimen name="text_selection_handle_height">58dp</dimen>
-    <dimen name="text_selection_handle_shadow">11dp</dimen>
     <dimen name="validation_message_height">50dp</dimen>
     <dimen name="validation_message_margin_top">6dp</dimen>
 
     <dimen name="tab_thumbnail_width">121dp</dimen>
     <dimen name="tab_thumbnail_height">90dp</dimen>
     <dimen name="tab_panel_column_width">129dp</dimen>
     <dimen name="tab_panel_grid_padding">20dp</dimen>
     <dimen name="tab_panel_grid_vspacing">20dp</dimen>
deleted file mode 100644
--- a/mobile/android/chrome/content/SelectionHandler.js
+++ /dev/null
@@ -1,1482 +0,0 @@
-// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this file,
- * You can obtain one at http://mozilla.org/MPL/2.0/. */
-"use strict";
-
-XPCOMUtils.defineLazyModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
-
-// Define elements that bound phone number containers.
-const PHONE_NUMBER_CONTAINERS = "td,div";
-const DEFER_CLOSE_TRIGGER_MS = 125; // Grace period delay before deferred _closeSelection()
-
-// Gecko AccessibleCaret pref names.
-const PREF_GECKO_ACCESSIBLECARET_ENABLED = "layout.accessiblecaret.enabled";
-
-var SelectionHandler = {
-
-  // Successful startSelection() or attachCaret().
-  ERROR_NONE: "",
-
-  // Error codes returned during startSelection().
-  START_ERROR_INVALID_MODE: "Invalid selection mode requested.",
-  START_ERROR_NONTEXT_INPUT: "Target element by definition contains no text.",
-  START_ERROR_NO_WORD_SELECTED: "No word selected at point.",
-  START_ERROR_SELECT_WORD_FAILED: "Word selection at point failed.",
-  START_ERROR_SELECT_ALL_PARAGRAPH_FAILED: "Select-All Paragraph failed.",
-  START_ERROR_NO_SELECTION: "Selection performed, but nothing resulted.",
-  START_ERROR_PROXIMITY: "Selection target and result seem unrelated.",
-  START_ERROR_SELECTIONCARETS_ENABLED: "Native selectionCarets requested while Gecko enabled.",
-
-  // Error codes returned during attachCaret().
-  ATTACH_ERROR_INCOMPATIBLE: "Element disabled, handled natively, or not editable.",
-  ATTACH_ERROR_TOUCHCARET_ENABLED: "Native touchCaret requested while Gecko enabled.",
-
-  HANDLE_TYPE_ANCHOR: "ANCHOR",
-  HANDLE_TYPE_CARET: "CARET",
-  HANDLE_TYPE_FOCUS: "FOCUS",
-
-  TYPE_NONE: 0,
-  TYPE_CURSOR: 1,
-  TYPE_SELECTION: 2,
-
-  SELECT_ALL: 0,
-  SELECT_AT_POINT: 1,
-
-  // Gecko TouchCaret/SelectionCaret pref values.
-  _accessibleCaretEnabledValue: null,
-  _selectionCaretEnabledValue: null,
-
-  // Keeps track of data about the dimensions of the selection. Coordinates
-  // stored here are relative to the _contentWindow window.
-  _cache: { anchorPt: {}, focusPt: {} },
-  _targetIsRTL: false,
-  _anchorIsRTL: false,
-  _focusIsRTL: false,
-
-  _activeType: 0, // TYPE_NONE
-  _selectionPrivate: null, // private selection reference
-  _selectionID: null, // Unique Selection ID
-
-  _draggingHandles: false, // True while user drags text selection handles
-  _dragStartAnchorOffset: null, // Editables need initial pos during HandleMove events
-  _dragStartFocusOffset: null, // Editables need initial pos during HandleMove events
-
-  _ignoreCompositionChanges: false, // Persist caret during IME composition updates
-  _prevHandlePositions: [], // Avoid issuing duplicate "TextSelection:Position" messages
-  _deferCloseTimer: null, // Used to defer _closeSelection() actions during programmatic changes
-
-  // TargetElement changes (text <--> no text) trigger actionbar UI update
-  _prevTargetElementHasText: null,
-
-  // The window that holds the selection (can be a sub-frame)
-  get _contentWindow() {
-    if (this._contentWindowRef)
-      return this._contentWindowRef.get();
-    return null;
-  },
-
-  set _contentWindow(aContentWindow) {
-    this._contentWindowRef = Cu.getWeakReference(aContentWindow);
-  },
-
-  // Main target element, always provides editor for editables.
-  get _targetElement() {
-    if (this._targetElementRef)
-      return this._targetElementRef.get();
-    return null;
-  },
-
-  set _targetElement(aTargetElement) {
-    this._targetElementRef = Cu.getWeakReference(aTargetElement);
-  },
-
-  // Alternate target element. Always provides public DOM node for editables
-  // that contain anonymous inner content structures.
-  _targetDOMCaretNode: null,
-
-  get _domWinUtils() {
-    return BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
-                                                    getInterface(Ci.nsIDOMWindowUtils);
-  },
-
-  // Provides UUID service for selection ID's.
-  get _idService() {
-    delete this._idService;
-    return this._idService = Cc["@mozilla.org/uuid-generator;1"].
-      getService(Ci.nsIUUIDGenerator);
-  },
-
-  // Are we supporting Accessible-core or native-Java carets?
-  get _accessibleCaretEnabled() {
-    if (this._accessibleCaretEnabledValue == null) {
-      try {
-        this._accessibleCaretEnabledValue = Services.prefs.getBoolPref(PREF_GECKO_ACCESSIBLECARET_ENABLED);
-      } catch (unused) { }
-      Services.prefs.addObserver(PREF_GECKO_ACCESSIBLECARET_ENABLED, function() {
-        SelectionHandler._accessibleCaretEnabledValue =
-          Services.prefs.getBoolPref(PREF_GECKO_ACCESSIBLECARET_ENABLED);
-      }, false);
-    }
-    return this._accessibleCaretEnabledValue;
-  },
-
-  _addObservers: function sh_addObservers() {
-    Services.obs.addObserver(this, "Gesture:SingleTap", false);
-    Services.obs.addObserver(this, "Tab:Selected", false);
-    Services.obs.addObserver(this, "TextSelection:Move", false);
-    Services.obs.addObserver(this, "TextSelection:Position", false);
-    Services.obs.addObserver(this, "TextSelection:End", false);
-    Services.obs.addObserver(this, "TextSelection:Action", false);
-    Services.obs.addObserver(this, "TextSelection:LayerReflow", false);
-
-    BrowserApp.deck.addEventListener("pagehide", this, false);
-    BrowserApp.deck.addEventListener("blur", this, true);
-    BrowserApp.deck.addEventListener("scroll", this, true);
-  },
-
-  _removeObservers: function sh_removeObservers() {
-    Services.obs.removeObserver(this, "Gesture:SingleTap");
-    Services.obs.removeObserver(this, "Tab:Selected");
-    Services.obs.removeObserver(this, "TextSelection:Move");
-    Services.obs.removeObserver(this, "TextSelection:Position");
-    Services.obs.removeObserver(this, "TextSelection:End");
-    Services.obs.removeObserver(this, "TextSelection:Action");
-    Services.obs.removeObserver(this, "TextSelection:LayerReflow");
-
-    BrowserApp.deck.removeEventListener("pagehide", this, false);
-    BrowserApp.deck.removeEventListener("blur", this, true);
-    BrowserApp.deck.removeEventListener("scroll", this, true);
-  },
-
-  observe: function sh_observe(aSubject, aTopic, aData) {
-    // Ignore all but selectionListener notifications during deferred _closeSelection().
-    if (this._deferCloseTimer) {
-      return;
-    }
-
-    switch (aTopic) {
-      // Update selectionListener and handle/caret positions, on page reflow
-      // (keyboard open/close, dynamic DOM changes, orientation updates, etc).
-      case "TextSelection:LayerReflow": {
-        if (this._activeType == this.TYPE_SELECTION) {
-          this._updateSelectionListener();
-        }
-        if (this._activeType != this.TYPE_NONE) {
-          this._positionHandlesOnChange();
-        }
-        break;
-      }
-
-      case "Gesture:SingleTap": {
-        if (this._activeType == this.TYPE_CURSOR) {
-          // attachCaret() is called in the "Gesture:SingleTap" handler in BrowserEventHandler
-          // We're guaranteed to call this first, because this observer was added last
-          this._deactivate();
-        }
-        break;
-      }
-
-      case "Tab:Selected":
-        this._closeSelection();
-        break;
-
-      case "TextSelection:End":
-        let data = JSON.parse(aData);
-        // End the requested selection only.
-        if (this._selectionID === data.selectionID) {
-          this._closeSelection();
-        }
-        break;
-
-      case "TextSelection:Action":
-        for (let type in this.actions) {
-          if (this.actions[type].id == aData) {
-            this.actions[type].action(this._targetElement);
-            break;
-          }
-        }
-        break;
-
-      case "TextSelection:Move": {
-        let data = JSON.parse(aData);
-        if (this._activeType == this.TYPE_SELECTION) {
-          this._startDraggingHandles();
-          this._moveSelection(data.handleType, new Point(data.x, data.y));
-
-        } else if (this._activeType == this.TYPE_CURSOR) {
-          this._startDraggingHandles();
-
-          // Ignore IMM composition notifications when caret movement starts
-          this._ignoreCompositionChanges = true;
-          this._moveCaret(data.x, data.y);
-
-          // Move the handle directly under the caret
-          this._positionHandles();
-        }
-        break;
-      }
-
-      case "TextSelection:Position": {
-        if (this._activeType == this.TYPE_SELECTION) {
-          this._startDraggingHandles();
-          this._ensureSelectionDirection();
-          this._stopDraggingHandles();
-          this._positionHandles();
-
-          // Changes to handle position can affect selection context and actionbar display
-          this._updateMenu();
-
-        } else if (this._activeType == this.TYPE_CURSOR) {
-          // Act on IMM composition notifications after caret movement ends
-          this._ignoreCompositionChanges = false;
-          this._stopDraggingHandles();
-          this._positionHandles();
-
-        } else {
-          Cu.reportError("Ignored \"TextSelection:Position\" message during invalid selection status");
-        }
-
-        break;
-      }
-
-      case "TextSelection:Get":
-        Messaging.sendRequest({
-          type: "TextSelection:Data",
-          requestId: aData,
-          text: this._getSelectedText()
-        });
-        break;
-    }
-  },
-
-  // Ignore selectionChange notifications during handle dragging, disable dynamic
-  // IME text compositions (autoSuggest, autoCorrect, etc)
-  _startDraggingHandles: function sh_startDraggingHandles() {
-    if (!this._draggingHandles) {
-      this._draggingHandles = true;
-      let selection = this._getSelection();
-      this._dragStartAnchorOffset = selection.anchorOffset;
-      this._dragStartFocusOffset = selection.focusOffset;
-      Messaging.sendRequest({ type: "TextSelection:DraggingHandle", dragging: true });
-    }
-  },
-
-  // Act on selectionChange notifications when not dragging handles, allow dynamic
-  // IME text compositions (autoSuggest, autoCorrect, etc)
-  _stopDraggingHandles: function sh_stopDraggingHandles() {
-    if (this._draggingHandles) {
-      this._draggingHandles = false;
-      this._dragStartAnchorOffset = null;
-      this._dragStartFocusOffset = null;
-      Messaging.sendRequest({ type: "TextSelection:DraggingHandle", dragging: false });
-    }
-  },
-
-  handleEvent: function sh_handleEvent(aEvent) {
-    // Ignore all but selectionListener notifications during deferred _closeSelection().
-    if (this._deferCloseTimer) {
-      return;
-    }
-
-    switch (aEvent.type) {
-      case "scroll":
-        // Maintain position when top-level document is scrolled
-        this._positionHandlesOnChange();
-        break;
-
-      case "pagehide": {
-        // We only care about events on the selected tab.
-        let tab = BrowserApp.getTabForWindow(aEvent.originalTarget.defaultView);
-        if (tab == BrowserApp.selectedTab) {
-          this._closeSelection();
-        }
-        break;
-      }
-
-      case "blur":
-        this._closeSelection();
-        break;
-
-      // Update caret position on keyboard activity
-      case "keyup":
-        // Not generated by Swiftkeyboard
-      case "compositionupdate":
-      case "compositionend":
-        // Generated by SwiftKeyboard, et. al.
-        if (!this._ignoreCompositionChanges) {
-          this._positionHandles();
-        }
-        break;
-    }
-  },
-
-  /** Returns true if the provided element can be selected in text selection, false otherwise. */
-  canSelect: function sh_canSelect(aElement) {
-    return !(aElement instanceof Ci.nsIDOMHTMLButtonElement ||
-             aElement instanceof Ci.nsIDOMHTMLEmbedElement ||
-             aElement instanceof Ci.nsIDOMHTMLImageElement ||
-             aElement instanceof Ci.nsIDOMHTMLMediaElement) &&
-             aElement.style.MozUserSelect != 'none';
-  },
-
-  _getScrollPos: function sh_getScrollPos() {
-    // Get the current display position
-    let scrollX = {}, scrollY = {};
-    this._contentWindow.top.QueryInterface(Ci.nsIInterfaceRequestor).
-                            getInterface(Ci.nsIDOMWindowUtils).getScrollXY(false, scrollX, scrollY);
-    return {
-      X: scrollX.value,
-      Y: scrollY.value
-    };
-  },
-
-  /**
-   * Add a selection listener to monitor for external selection changes.
-   */
-  _addSelectionListener: function(selection) {
-    this._selectionPrivate = selection.QueryInterface(Ci.nsISelectionPrivate);
-    this._selectionPrivate.addSelectionListener(this);
-  },
-
-  /**
-   * The nsISelection object for an editable can change during DOM mutations,
-   * causing us to stop receiving selectionChange notifications.
-   *
-   * We can detect that after a layer-reflow event, and dynamically update the
-   * listener.
-   */
-  _updateSelectionListener: function() {
-    if (!(this._targetElement instanceof Ci.nsIDOMNSEditableElement)) {
-      return;
-    }
-
-    let selection = this._getSelection();
-    if (this._selectionPrivate != selection.QueryInterface(Ci.nsISelectionPrivate)) {
-      this._removeSelectionListener();
-      this._addSelectionListener(selection);
-    }
-  },
-
-  /**
-   * Remove the selection listener.
-   */
-  _removeSelectionListener: function() {
-    this._selectionPrivate.removeSelectionListener(this);
-    this._selectionPrivate = null;
-  },
-
-  /**
-   * Observe and react to programmatic SelectionChange notifications.
-   */
-  notifySelectionChanged: function sh_notifySelectionChanged(aDocument, aSelection, aReason) {
-    // Cancel any in-progress / deferred _closeSelection() action.
-    this._cancelDeferredCloseSelection();
-
-    // Ignore selectionChange notifications during handle movements
-    if (this._draggingHandles) {
-      return;
-    }
-
-    // If the selection was collapsed to Start or to End, always close it
-    if ((aReason & Ci.nsISelectionListener.COLLAPSETOSTART_REASON) ||
-        (aReason & Ci.nsISelectionListener.COLLAPSETOEND_REASON)) {
-      this._closeSelection();
-      return;
-    }
-
-    // If selected text no longer exists, schedule a deferred close action.
-    if (!aSelection.toString()) {
-      this._deferCloseSelection();
-      return;
-    }
-
-    // Update the selection handle positions.
-    this._positionHandles();
-  },
-
-  /*
-   * Called from browser.js when the user long taps on text or chooses
-   * the "Select Word" context menu item. Initializes SelectionHandler,
-   * starts a selection, and positions the text selection handles.
-   *
-   * @param aOptions list of options describing how to start selection
-   *                 Options include:
-   *                   mode - SELECT_ALL to select everything in the target
-   *                          element, or SELECT_AT_POINT to select a word.
-   *                   x    - The x-coordinate for SELECT_AT_POINT.
-   *                   y    - The y-coordinate for SELECT_AT_POINT.
-   */
-  startSelection: function sh_startSelection(aElement, aOptions = { mode: SelectionHandler.SELECT_ALL }) {
-    // Disable Native touchCarets if Gecko AccessibleCaret enabled.
-    if (this._accessibleCaretEnabled) {
-      return this.START_ERROR_SELECTIONCARETS_ENABLED;
-    }
-
-    // Clear out any existing active selection
-    this._closeSelection();
-
-    if (this._isNonTextInputElement(aElement)) {
-      return this.START_ERROR_NONTEXT_INPUT;
-    }
-
-    const focus = Services.focus.focusedWindow;
-    if (focus) {
-      // Make sure any previous focus is cleared.
-      Services.focus.clearFocus(focus);
-    }
-
-    this._initTargetInfo(aElement, this.TYPE_SELECTION);
-
-    // Perform the appropriate selection method, if we can't determine method, or it fails, return
-    let selectionResult = this._performSelection(aOptions);
-    if (selectionResult !== this.ERROR_NONE) {
-      this._deactivate();
-      return selectionResult;
-    }
-
-    // Double check results of successful selection operation
-    let selection = this._getSelection();
-    if (!selection ||
-        selection.rangeCount == 0 ||
-        selection.getRangeAt(0).collapsed ||
-        this._getSelectedText().length == 0) {
-      this._deactivate();
-      return this.START_ERROR_NO_SELECTION;
-    }
-
-    // Add a listener to end the selection if it's removed programatically
-    this._addSelectionListener(selection);
-    this._activeType = this.TYPE_SELECTION;
-
-    // Figure out the distance between the selection and the click
-    let scroll = this._getScrollPos();
-    let positions = this._getHandlePositions(scroll);
-
-    if (aOptions.mode == this.SELECT_AT_POINT &&
-        !this._selectionNearClick(scroll.X + aOptions.x, scroll.Y + aOptions.y, positions)) {
-        this._closeSelection();
-        return this.START_ERROR_PROXIMITY;
-    }
-
-    // Determine position and show handles, open actionbar
-    this._positionHandles(positions);
-    Messaging.sendRequest({
-      selectionID: this._selectionID,
-      type: "TextSelection:ShowHandles",
-      handles: [this.HANDLE_TYPE_ANCHOR, this.HANDLE_TYPE_FOCUS]
-    });
-    this._updateMenu();
-    return this.ERROR_NONE;
-  },
-
-  /*
-   * Called to perform a selection operation, given a target element, selection method, starting point etc.
-   */
-  _performSelection: function sh_performSelection(aOptions) {
-    if (aOptions.mode == this.SELECT_AT_POINT) {
-      // Clear any ranges selected outside SelectionHandler, by code such as Find-In-Page.
-      this._contentWindow.getSelection().removeAllRanges();
-      try {
-        if (!this._domWinUtils.selectAtPoint(aOptions.x, aOptions.y, Ci.nsIDOMWindowUtils.SELECT_WORDNOSPACE)) {
-          return this.START_ERROR_NO_WORD_SELECTED;
-        }
-      } catch (e) {
-        return this.START_ERROR_SELECT_WORD_FAILED;
-      }
-
-      // Perform additional phone-number "smart selection".
-      if (this._isPhoneNumber(this._getSelection().toString())) {
-        this._selectSmartPhoneNumber();
-      }
-
-      return this.ERROR_NONE;
-    }
-
-    // Only selectAll() assumed from this point.
-    if (aOptions.mode != this.SELECT_ALL) {
-      return this.START_ERROR_INVALID_MODE;
-    }
-
-    // HTMLPreElement is a #text node, SELECT_ALL implies entire paragraph
-    if (this._targetElement instanceof HTMLPreElement)  {
-      try {
-        this._domWinUtils.selectAtPoint(1, 1, Ci.nsIDOMWindowUtils.SELECT_PARAGRAPH);
-        return this.ERROR_NONE;
-      } catch (e) {
-        return this.START_ERROR_SELECT_ALL_PARAGRAPH_FAILED;
-      }
-    }
-
-    // Else default to selectALL Document
-    let editor = this._getEditor();
-    if (editor) {
-      editor.selectAll();
-    } else {
-      this._getSelectionController().selectAll();
-    }
-
-    // Selection is entire HTMLHtmlElement, remove any trailing document whitespace
-    let selection = this._getSelection();
-    let lastNode = selection.focusNode;
-    while (lastNode && lastNode.lastChild) {
-      lastNode = lastNode.lastChild;
-    }
-
-    if (lastNode instanceof Text) {
-      try {
-        selection.extend(lastNode, lastNode.length);
-      } catch (e) {
-        Cu.reportError("SelectionHandler.js: _performSelection() whitespace trim fails: lastNode[" + lastNode +
-          "] lastNode.length[" + lastNode.length + "]");
-      }
-    }
-
-    return this.ERROR_NONE;
-  },
-
-  /*
-   * Called to expand a selection that appears to represent a phone number. This enhances the basic
-   * SELECT_WORDNOSPACE logic employed in performSelection() in response to long-tap / selecting text.
-   */
-  _selectSmartPhoneNumber: function() {
-    this._extendPhoneNumberSelection("forward");
-    this._reversePhoneNumberSelectionDir();
-
-    this._extendPhoneNumberSelection("backward");
-    this._reversePhoneNumberSelectionDir();
-  },
-
-  /*
-   * Extend the current phone number selection in the requested direction.
-   */
-  _extendPhoneNumberSelection: function(direction) {
-    let selection = this._getSelection();
-
-    // Extend the phone number selection until we find a boundry.
-    while (true) {
-      // Save current focus position, and extend the selection.
-      let focusNode = selection.focusNode;
-      let focusOffset = selection.focusOffset;
-      selection.modify("extend", direction, "character");
-
-      // If the selection doesn't change, (can't extend further), we're done.
-      if (selection.focusNode == focusNode && selection.focusOffset == focusOffset) {
-        return;
-      }
-
-      // Don't extend past a valid phone number.
-      if (!this._isPhoneNumber(selection.toString().trim())) {
-        // Backout the undesired selection extend, and we're done.
-        selection.collapse(selection.anchorNode, selection.anchorOffset);
-        selection.extend(focusNode, focusOffset);
-        return;
-      }
-
-      // Don't extend the selection into a new container.
-      if (selection.focusNode != focusNode) {
-        let nextContainer = (selection.focusNode instanceof Text) ?
-          selection.focusNode.parentNode : selection.focusNode;
-        if (nextContainer.matches &&
-            nextContainer.matches(PHONE_NUMBER_CONTAINERS)) {
-          // Backout the undesired selection extend, and we're done.
-          selection.collapse(selection.anchorNode, selection.anchorOffset);
-          selection.extend(focusNode, focusOffset);
-          return
-        }
-      }
-    }
-  },
-
-  /*
-   * Reverse the the selection direction, swapping anchorNode <-+-> focusNode.
-   */
-  _reversePhoneNumberSelectionDir: function(direction) {
-    let selection = this._getSelection();
-
-    let anchorNode = selection.anchorNode;
-    let anchorOffset = selection.anchorOffset;
-    selection.collapse(selection.focusNode, selection.focusOffset);
-    selection.extend(anchorNode, anchorOffset);
-  },
-
-  /* Return true if the current selection (given by aPositions) is near to where the coordinates passed in */
-  _selectionNearClick: function(aX, aY, aPositions) {
-      let distance = 0;
-
-      // Check if the click was in the bounding box of the selection handles
-      if (aPositions[0].left < aX && aX < aPositions[1].left
-          && aPositions[0].top < aY && aY < aPositions[1].top) {
-        distance = 0;
-      } else {
-        // If it was outside, check the distance to the center of the selection
-        let selectposX = (aPositions[0].left + aPositions[1].left) / 2;
-        let selectposY = (aPositions[0].top + aPositions[1].top) / 2;
-
-        let dx = Math.abs(selectposX - aX);
-        let dy = Math.abs(selectposY - aY);
-        distance = dx + dy;
-      }
-
-      let maxSelectionDistance = Services.prefs.getIntPref("browser.ui.selection.distance");
-      return (distance < maxSelectionDistance);
-  },
-
-  /* Reads a value from an action. If the action defines the value as a function, will return the result of calling
-     the function. Otherwise, will return the value itself. If the value isn't defined for this action, will return a default */
-  _getValue: function(obj, name, defaultValue) {
-    if (!(name in obj))
-      return defaultValue;
-
-    if (typeof obj[name] == "function")
-      return obj[name](this._targetElement);
-
-    return obj[name];
-  },
-
-  addAction: function(action) {
-    if (!action.id)
-      action.id = uuidgen.generateUUID().toString()
-
-    if (this.actions[action.id])
-      throw "Action with id " + action.id + " already added";
-
-    // Update actions list and actionbar UI if active.
-    this.actions[action.id] = action;
-    this._updateMenu();
-    return action.id;
-  },
-
-  removeAction: function(id) {
-    // Update actions list and actionbar UI if active.
-    delete this.actions[id];
-    this._updateMenu();
-  },
-
-  _updateMenu: function() {
-    if (this._activeType == this.TYPE_NONE) {
-      return;
-    }
-
-    // Update actionbar UI.
-    let actions = [];
-    for (let type in this.actions) {
-      let action = this.actions[type];
-      if (action.selector.matches(this._targetElement)) {
-        let a = {
-          id: action.id,
-          label: this._getValue(action, "label", ""),
-          icon: this._getValue(action, "icon", "drawable://ic_status_logo"),
-          showAsAction: this._getValue(action, "showAsAction", true),
-          order: this._getValue(action, "order", 0)
-        };
-        actions.push(a);
-      }
-    }
-
-    actions.sort((a, b) => b.order - a.order);
-
-    Messaging.sendRequest({
-      type: "TextSelection:Update",
-      actions: actions
-    });
-  },
-
-  /*
-   * Actionbar methods.
-   */
-  actions: {
-    SELECT_ALL: {
-      label: Strings.browser.GetStringFromName("contextmenu.selectAll"),
-      id: "selectall_action",
-      icon: "drawable://ab_select_all",
-      action: function(aElement) {
-        // Use the public DOMNode for startSelection(), not any anonymous inner.
-        SelectionHandler.startSelection(SelectionHandler._targetDOMCaretNode);
-        UITelemetry.addEvent("action.1", "actionbar", null, "select_all");
-      },
-      order: 5,
-      selector: {
-        matches: function(aElement) {
-          return (aElement.textLength != 0);
-        }
-      }
-    },
-
-    CUT: {
-      label: Strings.browser.GetStringFromName("contextmenu.cut"),
-      id: "cut_action",
-      icon: "drawable://ab_cut",
-      action: function(aElement) {
-        let start = aElement.selectionStart;
-        let end   = aElement.selectionEnd;
-
-        SelectionHandler.copySelection();
-        aElement.value = aElement.value.substring(0, start) + aElement.value.substring(end)
-
-        // copySelection closes the selection. Show a caret where we just cut the text.
-        SelectionHandler.attachCaret(aElement);
-        UITelemetry.addEvent("action.1", "actionbar", null, "cut");
-      },
-      order: 4,
-      selector: {
-        matches: function(aElement) {
-          // Disallow cut for contentEditable elements (until Bug 1112276 is fixed).
-          return !aElement.isContentEditable && SelectionHandler.isElementEditableText(aElement) ?
-            SelectionHandler.isSelectionActive() : false;
-        }
-      }
-    },
-
-    COPY: {
-      label: Strings.browser.GetStringFromName("contextmenu.copy"),
-      id: "copy_action",
-      icon: "drawable://ab_copy",
-      action: function() {
-        SelectionHandler.copySelection();
-        UITelemetry.addEvent("action.1", "actionbar", null, "copy");
-      },
-      order: 3,
-      selector: {
-        matches: function(aElement) {
-          // Don't include "copy" for password fields.
-          if (aElement instanceof Ci.nsIDOMHTMLInputElement && (aElement.type === "password")) {
-            return false;
-          }
-          return SelectionHandler.isSelectionActive();
-        }
-      }
-    },
-
-    PASTE: {
-      label: Strings.browser.GetStringFromName("contextmenu.paste"),
-      id: "paste_action",
-      icon: "drawable://ab_paste",
-      action: function(aElement) {
-        if (aElement) {
-          let target = SelectionHandler._getEditor();
-          aElement.focus();
-          target.paste(Ci.nsIClipboard.kGlobalClipboard);
-          SelectionHandler._closeSelection();
-          UITelemetry.addEvent("action.1", "actionbar", null, "paste");
-        }
-      },
-      order: 2,
-      selector: {
-        matches: function(aElement) {
-          if (SelectionHandler.isElementEditableText(aElement)) {
-            let flavors = ["text/unicode"];
-            return Services.clipboard.hasDataMatchingFlavors(flavors, flavors.length, Ci.nsIClipboard.kGlobalClipboard);
-          }
-          return false;
-        }
-      }
-    },
-
-    SHARE: {
-      label: Strings.browser.GetStringFromName("contextmenu.share"),
-      id: "share_action",
-      icon: "drawable://ic_menu_share",
-      action: function() {
-        SelectionHandler.shareSelection();
-        UITelemetry.addEvent("action.1", "actionbar", null, "share");
-      },
-      selector: {
-        matches: function() {
-          if (!ParentalControls.isAllowed(ParentalControls.SHARE)) {
-            return false;
-          }
-
-          return SelectionHandler.isSelectionActive();
-        }
-      }
-    },
-
-    SEARCH_ADD: {
-      id: "search_add_action",
-      label: Strings.browser.GetStringFromName("contextmenu.addSearchEngine3"),
-      icon: "drawable://ab_add_search_engine",
-
-      selector: {
-        matches: function(element) {
-          if(!(element instanceof HTMLInputElement)) {
-            return false;
-          }
-          let form = element.form;
-          if (!form || element.type == "password") {
-            return false;
-          }
-
-          // These are the following types of forms we can create keywords for:
-          //
-          // method    encoding type        can create keyword
-          // GET       *                                   YES
-          //           *                                   YES
-          // POST      *                                   YES
-          // POST      application/x-www-form-urlencoded   YES
-          // POST      text/plain                          NO ( a little tricky to do)
-          // POST      multipart/form-data                 NO
-          // POST      everything else                     YES
-          let method = form.method.toUpperCase();
-          return (method == "GET" || method == "") ||
-                 (form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
-        },
-      },
-
-      action: function(element) {
-        UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
-        SearchEngines.addEngine(element);
-      },
-    },
-
-    SEARCH: {
-      label: function() {
-        return Strings.browser.formatStringFromName("contextmenu.search", [Services.search.defaultEngine.name], 1);
-      },
-      id: "search_action",
-      icon: "drawable://ab_search",
-      action: function() {
-        SelectionHandler.searchSelection();
-        SelectionHandler._closeSelection();
-        UITelemetry.addEvent("action.1", "actionbar", null, "search");
-      },
-      order: 1,
-      selector: {
-        matches: function() {
-          return SelectionHandler.isSelectionActive();
-        }
-      }
-    },
-
-    CALL: {
-      label: Strings.browser.GetStringFromName("contextmenu.call"),
-      id: "call_action",
-      icon: "drawable://phone",
-      action: function() {
-        SelectionHandler.callSelection();
-        UITelemetry.addEvent("action.1", "actionbar", null, "call");
-      },
-      order: 1,
-      selector: {
-        matches: function () {
-          return SelectionHandler._getSelectedPhoneNumber() != null;
-        }
-      }
-    }
-  },
-
-  /*
-   * Called by BrowserEventHandler when the user taps in a form input.
-   * Initializes SelectionHandler and positions the caret handle.
-   *
-   * @param aX, aY tap location in client coordinates.
-   */
-  attachCaret: function sh_attachCaret(aElement) {
-    // Disable Native touchCarets if Gecko AccessibleCaret enabled.
-    if (this._accessibleCaretEnabled) {
-      return this.ATTACH_ERROR_TOUCHCARET_ENABLED;
-    }
-
-    // Clear out any existing active selection
-    this._closeSelection();
-
-    // Ensure it isn't disabled, isn't handled by Android native dialog, and is editable text element
-    if (aElement.disabled || InputWidgetHelper.hasInputWidget(aElement) || !this.isElementEditableText(aElement)) {
-      return this.ATTACH_ERROR_INCOMPATIBLE;
-    }
-
-    this._initTargetInfo(aElement, this.TYPE_CURSOR);
-
-    // Caret-specific observer/listeners
-    BrowserApp.deck.addEventListener("keyup", this, false);
-    BrowserApp.deck.addEventListener("compositionupdate", this, false);
-    BrowserApp.deck.addEventListener("compositionend", this, false);
-
-    this._activeType = this.TYPE_CURSOR;
-
-    // Determine position and show caret, open actionbar
-    this._positionHandles();
-    Messaging.sendRequest({
-      selectionID: this._selectionID,
-      type: "TextSelection:ShowHandles",
-      handles: [this.HANDLE_TYPE_CARET]
-    });
-    this._updateMenu();
-
-    return this.ERROR_NONE;
-  },
-
-  /**
-   * <input> editables of type=number are special cases, bearing unique anonymous
-   * internal content to facilitate up/down arrow UI controls. We will maintain a
-   * reference to their public DOMNode, as well as their internal node, which
-   * holds reference to it's editor.
-   */
-  _setTargetElements: function(element) {
-    // Default, both values are the same.
-    this._targetDOMCaretNode = element;
-    this._targetElement = element;
-    if (element.type !== "number") {
-      return;
-    }
-
-    // Set the editor bearing anonymous inner <input> element.
-    let editorNode = Services.focus.focusedElement;
-    if (editorNode instanceof HTMLInputElement && editorNode.editor) {
-      this._targetElement = editorNode;
-    }
-    return;
-  },
-
-  // Target initialization for both TYPE_CURSOR and TYPE_SELECTION
-  _initTargetInfo: function sh_initTargetInfo(aElement, aSelectionType) {
-    if (aElement instanceof Ci.nsIDOMNSEditableElement) {
-      if (aSelectionType === this.TYPE_SELECTION) {
-        // Blur the targetElement to force IME code to undo previous style compositions
-        // (visible underlines / etc generated by autoCorrection, autoSuggestion)
-        aElement.blur();
-      }
-      // Ensure targetElement is now focused normally
-      aElement.focus();
-    }
-    this._setTargetElements(aElement);
-
-    this._selectionID = this._idService.generateUUID().toString();
-    this._stopDraggingHandles();
-    this._contentWindow = aElement.ownerDocument.defaultView;
-    this._targetIsRTL = (this._contentWindow.getComputedStyle(aElement, "").direction == "rtl");
-
-    this._addObservers();
-  },
-
-  _getSelection: function sh_getSelection() {
-    if (this._targetElement instanceof Ci.nsIDOMNSEditableElement)
-      return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.selection;
-    else
-      return this._contentWindow.getSelection();
-  },
-
-  _getSelectedText: function sh_getSelectedText() {
-    if (!this._contentWindow)
-      return "";
-
-    let selection = this._getSelection();
-    if (!selection)
-      return "";
-
-    if (this._targetElement instanceof Ci.nsIDOMHTMLTextAreaElement) {
-      return selection.QueryInterface(Ci.nsISelectionPrivate).
-        toStringWithFormat("text/plain", Ci.nsIDocumentEncoder.OutputPreformatted | Ci.nsIDocumentEncoder.OutputRaw, 0);
-    }
-
-    return selection.toString().trim();
-  },
-
-  _getEditor: function sh_getEditor() {
-    if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) {
-      return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor;
-    }
-    return this._contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
-                              .getInterface(Ci.nsIWebNavigation)
-                              .QueryInterface(Ci.nsIInterfaceRequestor)
-                              .getInterface(Ci.nsIEditingSession)
-                              .getEditorForWindow(this._contentWindow);
-  },
-
-  _getSelectionController: function sh_getSelectionController() {
-    if (this._targetElement instanceof Ci.nsIDOMNSEditableElement)
-      return this._targetElement.QueryInterface(Ci.nsIDOMNSEditableElement).editor.selectionController;
-    else
-      return this._contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).
-                                 getInterface(Ci.nsIWebNavigation).
-                                 QueryInterface(Ci.nsIInterfaceRequestor).
-                                 getInterface(Ci.nsISelectionDisplay).
-                                 QueryInterface(Ci.nsISelectionController);
-  },
-
-  // Used by the contextmenu "matches" functions in ClipboardHelper
-  isSelectionActive: function sh_isSelectionActive() {
-    return (this._activeType == this.TYPE_SELECTION);
-  },
-
-  isElementEditableText: function (aElement) {
-    return (((aElement instanceof HTMLInputElement &&
-              (aElement.mozIsTextField(false) || aElement.type === "number")) ||
-            (aElement instanceof HTMLTextAreaElement)) && !aElement.readOnly) ||
-            aElement.isContentEditable;
-  },
-
-  _isNonTextInputElement: function(aElement) {
-    return (aElement instanceof HTMLInputElement &&
-            !(aElement.mozIsTextField(false) || aElement.type === "number"));
-  },
-
-  /*
-   * Moves the selection as the user drags a handle.
-   * @param handleType: Specifies either the anchor or the focus handle.
-   * @param handlePt: selection point in client coordinates.
-   */
-  _moveSelection: function sh_moveSelection(handleType, handlePt) {
-    let isAnchorHandle = (handleType == this.HANDLE_TYPE_ANCHOR);
-
-    // Determine new caret position from handlePt, exit if user
-    // moved it offscreen.
-    let viewOffset = this._getViewOffset();
-    let ptX = handlePt.x - viewOffset.x;
-    let ptY = handlePt.y - viewOffset.y;
-    let cwd = this._contentWindow.document;
-    let caretPos = cwd.caretPositionFromPoint(ptX, ptY);
-    if (!caretPos) {
-      return;
-    }
-
-    // Constrain text selection within editable elements.
-    let targetIsEditable = this._targetElement instanceof Ci.nsIDOMNSEditableElement;
-    if (targetIsEditable && (caretPos.offsetNode != this._targetDOMCaretNode)) {
-      return;
-    }
-
-    // Update the Selection for editable elements. Selection Change
-    // logic is the same, regardless of RTL/LTR. Selection direction is
-    // maintained always forward (startOffset <= endOffset).
-    if (targetIsEditable) {
-      let start = this._dragStartAnchorOffset;
-      let end = this._dragStartFocusOffset;
-      if (isAnchorHandle) {
-        start = caretPos.offset;
-      } else {
-        end = caretPos.offset;
-      }
-      if (start > end) {
-        [start, end] = [end, start];
-      }
-      this._targetElement.setSelectionRange(start, end);
-      return;
-    }
-
-    // Update the Selection for non-editable elements. Selection Change
-    // logic is the same, regardless of RTL/LTR. Selection direction internally
-    // can finish reversed by user drag. ie: Forward is (a,o ---> f,o),
-    // and reversed is (a,o <--- f,o).
-    let selection = this._getSelection();
-    if (isAnchorHandle) {
-      let focusNode = selection.focusNode;
-      let focusOffset = selection.focusOffset;
-      selection.collapse(caretPos.offsetNode, caretPos.offset);
-      selection.extend(focusNode, focusOffset);
-    } else {
-      selection.extend(caretPos.offsetNode, caretPos.offset);
-    }
-  },
-
-  _moveCaret: function sh_moveCaret(aX, aY) {
-    // Get rect of text inside element
-    let range = document.createRange();
-    range.selectNodeContents(this._getEditor().rootElement);
-    let textBounds = range.getBoundingClientRect();
-
-    // Get rect of editor
-    let editorBounds = this._domWinUtils.sendQueryContentEvent(this._domWinUtils.QUERY_EDITOR_RECT, 0, 0, 0, 0,
-                                                               this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
-    // the return value from sendQueryContentEvent is in LayoutDevice pixels and we want CSS pixels, so
-    // divide by the pixel ratio
-    let editorRect = new Rect(editorBounds.left / window.devicePixelRatio,
-                              editorBounds.top / window.devicePixelRatio,
-                              editorBounds.width / window.devicePixelRatio,
-                              editorBounds.height / window.devicePixelRatio);
-
-    // Use intersection of the text rect and the editor rect
-    let rect = new Rect(textBounds.left, textBounds.top, textBounds.width, textBounds.height);
-    rect.restrictTo(editorRect);
-
-    // Clamp vertically and scroll if handle is at bounds. The top and bottom
-    // must be restricted by an additional pixel since clicking on the top
-    // edge of an input field moves the cursor to the beginning of that
-    // field's text (and clicking the bottom moves the cursor to the end).
-    if (aY < rect.y + 1) {
-      aY = rect.y + 1;
-      this._getSelectionController().scrollLine(false);
-    } else if (aY > rect.y + rect.height - 1) {
-      aY = rect.y + rect.height - 1;
-      this._getSelectionController().scrollLine(true);
-    }
-
-    // Clamp horizontally and scroll if handle is at bounds
-    if (aX < rect.x) {
-      aX = rect.x;
-      this._getSelectionController().scrollCharacter(false);
-    } else if (aX > rect.x + rect.width) {
-      aX = rect.x + rect.width;
-      this._getSelectionController().scrollCharacter(true);
-    }
-
-    this._domWinUtils.sendMouseEventToWindow("mousedown", aX, aY, 0, 0, 0, true);
-    this._domWinUtils.sendMouseEventToWindow("mouseup", aX, aY, 0, 0, 0, true);
-  },
-
-  copySelection: function sh_copySelection() {
-    let selectedText = this._getSelectedText();
-    if (selectedText.length) {
-      let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
-      clipboard.copyString(selectedText);
-      Snackbars.show(Strings.browser.GetStringFromName("selectionHelper.textCopied"), Snackbars.LENGTH_LONG);
-    }
-    this._closeSelection();
-  },
-
-  shareSelection: function sh_shareSelection() {
-    let selectedText = this._getSelectedText();
-    if (selectedText.length) {
-      Messaging.sendRequest({
-        type: "Share:Text",
-        text: selectedText
-      });
-    }
-    this._closeSelection();
-  },
-
-  searchSelection: function sh_searchSelection() {
-    let selectedText = this._getSelectedText();
-    if (selectedText.length) {
-      let req = Services.search.defaultEngine.getSubmission(selectedText);
-      let parent = BrowserApp.selectedTab;
-      let isPrivate = PrivateBrowsingUtils.isBrowserPrivate(parent.browser);
-      // Set current tab as parent of new tab, and set new tab as private if the parent is.
-      BrowserApp.addTab(req.uri.spec, {parentId: parent.id,
-                                       selected: true,
-                                       isPrivate: isPrivate});
-    }
-    this._closeSelection();
-  },
-
-  _phoneRegex: /^\+?[0-9\s,-.\(\)*#pw]{1,30}$/,
-
-  _getSelectedPhoneNumber: function sh_getSelectedPhoneNumber() {
-    return this._isPhoneNumber(this._getSelectedText().trim());
-  },
-
-  _isPhoneNumber: function sh_isPhoneNumber(selectedText) {
-    return (this._phoneRegex.test(selectedText) ? selectedText : null);
-  },
-
-  callSelection: function sh_callSelection() {
-    let selectedText = this._getSelectedPhoneNumber();
-    if (selectedText) {
-      BrowserApp.loadURI("tel:" + selectedText);
-    }
-    this._closeSelection();
-  },
-
-  /**
-   * Deferred _closeSelection() actions allow for brief periods where programmatic
-   * selection changes have effectively closed the selection, but we anticipate further
-   * activity that may restore it.
-   *
-   * At this point, we hide the UI handles, and stop responding to messages until
-   * either the final _closeSelection() is triggered, or until our Gecko selectionListener
-   * notices a subsequent programmatic selection that results in a new selection.
-   */
-  _deferCloseSelection: function() {
-    // Schedule the deferred _closeSelection() action.
-    this._deferCloseTimer = setTimeout((function() {
-      // Time is up! Close the selection.
-      this._deferCloseTimer = null;
-      this._closeSelection();
-    }).bind(this), DEFER_CLOSE_TRIGGER_MS);
-
-    // Hide any handles while deferClosed.
-    if (this._prevHandlePositions.length) {
-      let positions = this._prevHandlePositions;
-      for (let i in positions) {
-        positions[i].hidden = true;
-      }
-
-      Messaging.sendRequest({
-        type: "TextSelection:PositionHandles",
-        positions: positions,
-      });
-    }
-  },
-
-  /**
-   * Cancel any current deferred _closeSelection() action.
-   */
-  _cancelDeferredCloseSelection: function() {
-    if (this._deferCloseTimer) {
-      clearTimeout(this._deferCloseTimer);
-      this._deferCloseTimer = null;
-    }
-  },
-
-  /*
-   * Shuts SelectionHandler down.
-   */
-  _closeSelection: function sh_closeSelection() {
-    // Bail if there's no active selection
-    if (this._activeType == this.TYPE_NONE)
-      return;
-
-    if (this._activeType == this.TYPE_SELECTION)
-      this._clearSelection();
-
-    this._deactivate();
-  },
-
-  _clearSelection: function sh_clearSelection() {
-    // Cancel any in-progress / deferred _closeSelection() process.
-    this._cancelDeferredCloseSelection();
-
-    let selection = this._getSelection();
-    if (selection) {
-      // Remove our listener before we clear the selection
-      this._removeSelectionListener();
-
-      // Remove the selection. For editables, we clear selection without losing
-      // element focus. For non-editables, just clear all.
-      if (selection.rangeCount != 0) {
-        if (this.isElementEditableText(this._targetElement)) {
-          selection.collapseToStart();
-        } else {
-          selection.removeAllRanges();
-        }
-      }
-    }
-  },
-
-  _deactivate: function sh_deactivate() {
-    this._stopDraggingHandles();
-    // Hide handle/caret, close actionbar
-    Messaging.sendRequest({ type: "TextSelection:HideHandles" });
-
-    this._removeObservers();
-
-    // Only observed for caret positioning
-    if (this._activeType == this.TYPE_CURSOR) {
-      BrowserApp.deck.removeEventListener("keyup", this);
-      BrowserApp.deck.removeEventListener("compositionupdate", this);
-      BrowserApp.deck.removeEventListener("compositionend", this);
-    }
-
-    this._contentWindow = null;
-    this._targetElement = null;
-    this._targetDOMCaretNode = null;
-
-    this._targetIsRTL = false;
-    this._ignoreCompositionChanges = false;
-    this._prevHandlePositions = [];
-    this._prevTargetElementHasText = null;
-
-    this._activeType = this.TYPE_NONE;
-  },
-
-  _getViewOffset: function sh_getViewOffset() {
-    let offset = { x: 0, y: 0 };
-    let win = this._contentWindow;
-
-    // Recursively look through frames to compute the total position offset.
-    while (win.frameElement) {
-      let rect = win.frameElement.getBoundingClientRect();
-      offset.x += rect.left;
-      offset.y += rect.top;
-
-      win = win.parent;
-    }
-
-    return offset;
-  },
-
-  /*
-   * The direction of the Selection is ensured for editables while the user drags
-   * the handles (per "TextSelection:Move" event). For non-editables, we just let
-   * the user change direction, but fix it up at the end of handle movement (final
-   * "TextSelection:Position" event).
-   */
-  _ensureSelectionDirection: function() {
-    // Never needed at this time.
-    if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) {
-      return;
-    }
-
-    // Nothing needed if not reversed.
-    let qcEventResult = this._domWinUtils.sendQueryContentEvent(
-      this._domWinUtils.QUERY_SELECTED_TEXT, 0, 0, 0, 0);
-    if (!qcEventResult.reversed) {
-      return;
-    }
-
-    // Reverse the Selection.
-    let selection = this._getSelection();
-    let newFocusNode = selection.anchorNode;
-    let newFocusOffset = selection.anchorOffset;
-
-    selection.collapse(selection.focusNode, selection.focusOffset);
-    selection.extend(newFocusNode, newFocusOffset);
-  },
-
-  /*
-   * Updates the TYPE_SELECTION cache, with the handle anchor/focus point values
-   * of the current selection. Passed to Java for UI positioning only.
-   *
-   * Note that the anchor handle and focus handle can reference text in nodes
-   * with mixed direction. (ie a.direction = "rtl" while f.direction = "ltr").
-   */
-  _updateCacheForSelection: function() {
-    let selection = this._getSelection();
-    let rects = selection.getRangeAt(0).getClientRects();
-    if (rects.length == 0) {
-      // nsISelection object exists, but there's nothing actually selected
-      throw "Failed to update cache for invalid selection";
-    }
-
-    // Right-to-Left (ie: Hebrew) anchorPt is on right,
-    // Left-to-Right (ie: English) anchorPt is on left.
-    this._anchorIsRTL = this._isNodeRTL(selection.anchorNode);
-    let anchorIdx = 0;
-    this._cache.anchorPt = (this._anchorIsRTL) ?
-      new Point(rects[anchorIdx].right, rects[anchorIdx].bottom) :
-      new Point(rects[anchorIdx].left, rects[anchorIdx].bottom);
-
-    // Right-to-Left (ie: Hebrew) focusPt is on left,
-    // Left-to-Right (ie: English) focusPt is on right.
-    this._focusIsRTL = this._isNodeRTL(selection.focusNode);
-    let focusIdx = rects.length - 1;
-    this._cache.focusPt = (this._focusIsRTL) ?
-      new Point(rects[focusIdx].left, rects[focusIdx].bottom) :
-      new Point(rects[focusIdx].right, rects[focusIdx].bottom);
-  },
-
-  /*
-   * Return true if text associated with a node is RTL.
-   */
-  _isNodeRTL: function(node) {
-    // Find containing node that supports .direction attribute (needed
-    // when target node is #text for example).
-    while (node && !(node instanceof Element)) {
-      node = node.parentNode;
-    }
-
-    // Worst case, use original direction from _targetElement.
-    if (!node) {
-      return this._targetIsRTL;
-    }
-
-    let nodeWin = node.ownerDocument.defaultView;
-    let nodeStyle = nodeWin.getComputedStyle(node, "");
-    return (nodeStyle.direction == "rtl");
-  },
-
-  _getHandlePositions: function(scroll = this._getScrollPos()) {
-    // the checkHidden function tests to see if the given point is hidden inside an
-    // iframe/subdocument. this is so that if we select some text inside an iframe and
-    // scroll the iframe so the selection is out of view, we hide the handles rather
-    // than having them float on top of the main page content.
-    let checkHidden = function(x, y) {
-      return false;
-    };
-    if (this._contentWindow.frameElement) {
-      let bounds = this._contentWindow.frameElement.getBoundingClientRect();
-      checkHidden = function(x, y) {
-        return x < 0 || y < 0 || x > bounds.width || y > bounds.height;
-      };
-    }
-
-    if (this._activeType == this.TYPE_CURSOR) {
-      // The left and top properties returned are relative to the client area
-      // of the window, so we don't need to account for a sub-frame offset.
-      let cursor = this._domWinUtils.sendQueryContentEvent(this._domWinUtils.QUERY_CARET_RECT, this._targetElement.selectionEnd, 0, 0, 0,
-                                                           this._domWinUtils.QUERY_CONTENT_FLAG_USE_XP_LINE_BREAK);
-      // the return value from sendQueryContentEvent is in LayoutDevice pixels and we want CSS pixels, so
-      // divide by the pixel ratio
-      let x = cursor.left / window.devicePixelRatio;
-      let y = (cursor.top + cursor.height) / window.devicePixelRatio;
-      return [{ handle: this.HANDLE_TYPE_CARET,
-                left: x + scroll.X,
-                top: y + scroll.Y,
-                rtl: this._targetIsRTL,
-                hidden: checkHidden(x, y) }];
-    }
-
-    // Determine the handle screen coords
-    this._updateCacheForSelection();
-    let offset = this._getViewOffset();
-    return  [{ handle: this.HANDLE_TYPE_ANCHOR,
-               left: this._cache.anchorPt.x + offset.x + scroll.X,
-               top: this._cache.anchorPt.y + offset.y + scroll.Y,
-               rtl: this._anchorIsRTL,
-               hidden: checkHidden(this._cache.anchorPt.x, this._cache.anchorPt.y) },
-             { handle: this.HANDLE_TYPE_FOCUS,
-               left: this._cache.focusPt.x + offset.x + scroll.X,
-               top: this._cache.focusPt.y + offset.y + scroll.Y,
-               rtl: this._focusIsRTL,
-               hidden: checkHidden(this._cache.focusPt.x, this._cache.focusPt.y) }];
-  },
-
-  // Position handles, but avoid superfluous re-positioning (helps during
-  // "TextSelection:LayerReflow", "scroll" of top-level document, etc).
-  _positionHandlesOnChange: function() {
-    // Helper function to compare position messages
-    let samePositions = function(aPrev, aCurr) {
-      if (aPrev.length != aCurr.length) {
-        return false;
-      }
-      for (let i = 0; i < aPrev.length; i++) {
-        if (aPrev[i].left != aCurr[i].left ||
-            aPrev[i].top != aCurr[i].top ||
-            aPrev[i].rtl != aCurr[i].rtl ||
-            aPrev[i].hidden != aCurr[i].hidden) {
-          return false;
-        }
-      }
-      return true;
-    }
-
-    let positions = this._getHandlePositions();
-    if (!samePositions(this._prevHandlePositions, positions)) {
-      this._positionHandles(positions);
-    }
-  },
-
-  // Position handles, allow for re-position, in case user drags handle
-  // to invalid position, then releases, we can put it back where it started
-  // positions is an array of objects with data about handle positions,
-  // which we get from _getHandlePositions.
-  _positionHandles: function(positions = this._getHandlePositions()) {
-    Messaging.sendRequest({
-      type: "TextSelection:PositionHandles",
-      positions: positions,
-    });
-    this._prevHandlePositions = positions;
-
-    // Text state transitions (text <--> no text) will affect selection context and actionbar display
-    let currTargetElementHasText = (this._targetElement.textLength > 0);
-    if (currTargetElementHasText != this._prevTargetElementHasText) {
-      this._prevTargetElementHasText = currTargetElementHasText;
-      this._updateMenu();
-    }
-  },
-
-  subdocumentScrolled: function sh_subdocumentScrolled(aElement) {
-    // Ignore all but selectionListener notifications during deferred _closeSelection().
-    if (this._deferCloseTimer) {
-      return;
-    }
-
-    if (this._activeType == this.TYPE_NONE) {
-      return;
-    }
-    let scrollView = aElement.ownerDocument.defaultView;
-    let view = this._contentWindow;
-    while (true) {
-      if (view == scrollView) {
-        // The selection is in a view (or sub-view) of the view that scrolled.
-        // So we need to reposition the handles.
-        this._positionHandles();
-        break;
-      }
-      if (view == view.parent) {
-        break;
-      }
-      view = view.parent;
-    }
-  }
-};
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -149,17 +149,16 @@ lazilyLoadedBrowserScripts.forEach(funct
 
 var lazilyLoadedObserverScripts = [
   ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"],
   ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"],
   ["FindHelper", ["FindInPage:Opened", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"],
   ["PermissionsHelper", ["Permissions:Check", "Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"],
   ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"],
   ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"],
-  ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"],
   ["EmbedRT", ["GeckoView:ImportScript"], "chrome://browser/content/EmbedRT.js"],
   ["Reader", ["Reader:AddToCache", "Reader:RemoveFromCache"], "chrome://browser/content/Reader.js"],
   ["PrintHelper", ["Print:PDF"], "chrome://browser/content/PrintHelper.js"],
 ];
 
 lazilyLoadedObserverScripts.push(
 ["ActionBarHandler", ["TextSelection:Get", "TextSelection:Action", "TextSelection:End"],
   "chrome://browser/content/ActionBarHandler.js"]
@@ -2682,38 +2681,16 @@ var NativeWindow = {
 
         return;
       }
 
       // If no context-menu for long-press event, it may be meant to trigger text-selection.
       this.menus = null;
       Services.obs.notifyObservers(
         {target: this._target, x: event.clientX, y: event.clientY}, "context-menu-not-shown", "");
-
-      if (SelectionHandler.canSelect(this._target)) {
-        // If textSelection WORD is successful,
-        // consume / preventDefault the context menu event.
-        let selectionResult = SelectionHandler.startSelection(this._target,
-          { mode: SelectionHandler.SELECT_AT_POINT,
-            x: event.clientX,
-            y: event.clientY
-          }
-        );
-        if (selectionResult === SelectionHandler.ERROR_NONE) {
-          event.preventDefault();
-          return;
-        }
-
-        // If textSelection caret-attachment is successful,
-        // consume / preventDefault the context menu event.
-        if (SelectionHandler.attachCaret(this._target) === SelectionHandler.ERROR_NONE) {
-          event.preventDefault();
-          return;
-        }
-      }
     },
 
     // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url
     _getTitle: function(node) {
       if (node.hasAttribute && node.hasAttribute("title")) {
         return node.getAttribute("title");
       }
       return this._getUrl(node);
@@ -4609,31 +4586,17 @@ var BrowserEventHandler = {
   handleUserEvent: function(aTopic, aData) {
     switch (aTopic) {
 
       case "Gesture:ClickInZoomedView":
         this._clickInZoomedView = true;
         break;
 
       case "Gesture:SingleTap": {
-        let focusedElement = null;
-        try {
-          // If the element was previously focused, show the caret attached to it.
-          let element = this._highlightElement;
-          focusedElement = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser);
-          if (element && element == focusedElement) {
-            let result = SelectionHandler.attachCaret(element);
-            if (result !== SelectionHandler.ERROR_NONE) {
-              dump("Unexpected failure during caret attach: " + result);
-            }
-          }
-        } catch(e) {
-          Cu.reportError(e);
-        }
-
+        let focusedElement = BrowserApp.getFocusedInput(BrowserApp.selectedBrowser);
         let data = JSON.parse(aData);
         let {x, y} = data;
 
         if (this._inCluster && this._clickInZoomedView != true) {
           // If there is a focused element, the display of the zoomed view won't remove the focus.
           // In this case, the form assistant linked to the focused element will never be closed.
           // To avoid this situation, the focus is moved and the form assistant is closed.
           if (focusedElement) {
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -28,17 +28,16 @@ chrome.jar:
   content/browser.css                  (content/browser.css)
   content/browser.js                   (content/browser.js)
   content/geckoview.xul                (content/geckoview.xul)
   content/geckoview.js                 (content/geckoview.js)
   content/bindings/checkbox.xml        (content/bindings/checkbox.xml)
   content/bindings/settings.xml        (content/bindings/settings.xml)
   content/netError.xhtml               (content/netError.xhtml)
   content/SelectHelper.js              (content/SelectHelper.js)
-  content/SelectionHandler.js          (content/SelectionHandler.js)
   content/ActionBarHandler.js          (content/ActionBarHandler.js)
   content/EmbedRT.js                   (content/EmbedRT.js)
   content/InputWidgetHelper.js         (content/InputWidgetHelper.js)
   content/WebrtcUI.js                  (content/WebrtcUI.js)
   content/MemoryObserver.js            (content/MemoryObserver.js)
   content/ConsoleAPI.js                (content/ConsoleAPI.js)
   content/PluginHelper.js              (content/PluginHelper.js)
   content/PrintHelper.js               (content/PrintHelper.js)
--- a/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android-frontend/releng.manifest
@@ -30,26 +30,26 @@
 "filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
 "unpack": true
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "jcentral.tar.xz",
 "unpack": true,
-"digest": "66640e3f77a0f9c0ea52f66c53bee8db3c1a27ea4a11526d15706b9da6a0302cd2d5b088f9addca84f4a962022cba3b76829cb878c90cf9bebb3aab050b4aaa4",
-"size": 47315996
+"digest": "8e50f0993e129d3447b228d7da77d661d4ae3d490d791630dabb73e7d8021920f765317a258fd6e819aca48daaa8d0d86ec07cb6c30736199bbf2c4f92270cb5",
+"size": 47164284
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "gradle-dist.tar.xz",
 "unpack": true,
-"digest": "36f961f85b0be846cc9e72bfa0dd1f74e7da8ef785717ce4fd102fec977f21f8902c233b28a21c1ce3797eb2759c7a74c5f74e47bd8f13c1eec640f8d7bed4ac",
-"size": 51512016
+"digest": "e3cfe7f8259ad97722243d4e873d5a05c014bfc24d637427f89d804bf5073290229c778ea303142cf06c2dc79e0492f23521f57d3a73825f55b8db587317646f",
+"size": 51753660
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "dotgradle.tar.xz",
 "unpack": true,
 "digest": "9f082ccd71ad18991eb71fcad355c6990f50a72a09ab9b79696521485656083a72faf5a8d4714de9c4b901ee2319b6786a51964846bb7075061642a8505501c2",
 "size": 512
--- a/mobile/android/config/tooltool-manifests/android-x86/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android-x86/releng.manifest
@@ -45,26 +45,26 @@
 "filename": "gcc.tar.xz",
 "unpack": true
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "jcentral.tar.xz",
 "unpack": true,
-"digest": "66640e3f77a0f9c0ea52f66c53bee8db3c1a27ea4a11526d15706b9da6a0302cd2d5b088f9addca84f4a962022cba3b76829cb878c90cf9bebb3aab050b4aaa4",
-"size": 47315996
+"digest": "8e50f0993e129d3447b228d7da77d661d4ae3d490d791630dabb73e7d8021920f765317a258fd6e819aca48daaa8d0d86ec07cb6c30736199bbf2c4f92270cb5",
+"size": 47164284
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "gradle-dist.tar.xz",
 "unpack": true,
-"digest": "36f961f85b0be846cc9e72bfa0dd1f74e7da8ef785717ce4fd102fec977f21f8902c233b28a21c1ce3797eb2759c7a74c5f74e47bd8f13c1eec640f8d7bed4ac",
-"size": 51512016
+"digest": "e3cfe7f8259ad97722243d4e873d5a05c014bfc24d637427f89d804bf5073290229c778ea303142cf06c2dc79e0492f23521f57d3a73825f55b8db587317646f",
+"size": 51753660
 },
 {
 "size": 30899096,
 "visibility": "public",
 "digest": "ac9f5f95d11580d3dbeff87e80a585fe4d324b270dabb91b1165686acab47d99fa6651074ab0be09420239a5d6af38bb2c539506962a7b44e0ed4d080bba2953",
 "algorithm": "sha512",
 "filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
 "unpack": true
--- a/mobile/android/config/tooltool-manifests/android/releng.manifest
+++ b/mobile/android/config/tooltool-manifests/android/releng.manifest
@@ -55,26 +55,26 @@
 "filename": "java_home-1.7.0-openjdk-1.7.0.85.x86_64.tar.xz",
 "unpack": true
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "jcentral.tar.xz",
 "unpack": true,
-"digest": "66640e3f77a0f9c0ea52f66c53bee8db3c1a27ea4a11526d15706b9da6a0302cd2d5b088f9addca84f4a962022cba3b76829cb878c90cf9bebb3aab050b4aaa4",
-"size": 47315996
+"digest": "8e50f0993e129d3447b228d7da77d661d4ae3d490d791630dabb73e7d8021920f765317a258fd6e819aca48daaa8d0d86ec07cb6c30736199bbf2c4f92270cb5",
+"size": 47164284
 },
 {
 "algorithm": "sha512",
 "visibility": "public",
 "filename": "gradle-dist.tar.xz",
 "unpack": true,
-"digest": "36f961f85b0be846cc9e72bfa0dd1f74e7da8ef785717ce4fd102fec977f21f8902c233b28a21c1ce3797eb2759c7a74c5f74e47bd8f13c1eec640f8d7bed4ac",
-"size": 51512016
+"digest": "e3cfe7f8259ad97722243d4e873d5a05c014bfc24d637427f89d804bf5073290229c778ea303142cf06c2dc79e0492f23521f57d3a73825f55b8db587317646f",
+"size": 51753660
 },
 {
 "version": "rustc 1.11.0 (9b21dcd6a 2016-08-15) repack",
 "size": 97552448,
 "digest": "272438c1692a46998dc44f22bd1fe18da1be7af2e7fdcf6c52709366c80c73e30637f0c3864f45c64edf46ce6a905538c14b2313983be973f9f29a2f191ec89b",
 "algorithm": "sha512",
 "filename": "rustc.tar.xz",
 "unpack": true
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/build.gradle
@@ -0,0 +1,86 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/geckoview"
+
+apply plugin: 'android-sdk-manager' // Must come before 'com.android.*'.
+apply plugin: 'com.android.library'
+
+android {
+    compileSdkVersion 23
+    buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+    defaultConfig {
+        targetSdkVersion 23
+        minSdkVersion 15
+    }
+
+    buildTypes {
+        withGeckoBinaries {
+            initWith release
+        }
+        withoutGeckoBinaries { // For clarity and consistency throughout the tree.
+            initWith release
+        }
+    }
+
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_7
+        targetCompatibility JavaVersion.VERSION_1_7
+    }
+
+    dexOptions {
+        javaMaxHeapSize "2g"
+    }
+
+    lintOptions {
+        abortOnError false
+    }
+
+    sourceSets {
+        main {
+            java {
+                srcDir "${topsrcdir}/mobile/android/geckoview/src/thirdparty/java"
+
+                // TODO: support WebRTC.
+                // if (mozconfig.substs.MOZ_WEBRTC) {
+                //     srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/audio_device/android/java/src"
+                //     srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_capture/android/java/src"
+                //     srcDir "${topsrcdir}/media/webrtc/trunk/webrtc/modules/video_render/android/java/src"
+                // }
+
+                // TODO: don't use AppConstants.
+                srcDir "${project.buildDir}/generated/source/preprocessed_code" // See syncPreprocessedCode.
+            }
+
+            assets {
+            }
+        }
+    }
+}
+
+dependencies {
+    compile "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
+}
+
+task syncPreprocessedCode(type: Sync, dependsOn: rootProject.generateCodeAndResources) {
+    into("${project.buildDir}/generated/source/preprocessed_code")
+    from("${topobjdir}/mobile/android/base/generated/preprocessed") {
+        // AdjustConstants is included in the main app project.
+        exclude '**/AdjustConstants.java'
+    }
+}
+
+apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
+
+android.libraryVariants.all { variant ->
+    variant.preBuild.dependsOn syncPreprocessedCode
+
+    // Like 'debug', 'release', or 'withGeckoBinaries'.
+    def buildType = variant.buildType.name
+
+    // It would be most natural for :geckoview to always include the Gecko
+    // binaries, but that's difficult; see the notes in
+    // mobile/android/gradle/with_gecko_binaries.gradle.  Instead :app uses
+    // :geckoview:release and handles it's own Gecko binary inclusion.
+    if (buildType.equals('withGeckoBinaries')) {
+        configureVariantWithGeckoBinaries(variant)
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="org.mozilla.geckoview">
+
+    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
+    <!-- READ_EXTERNAL_STORAGE was added in API 16, and is only enforced in API
+         19+.  We declare it so that the bouncer APK and the main APK have the
+         same set of permissions. -->
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+    <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
+    <uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT"/>
+
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
+    <uses-permission android:name="android.permission.VIBRATE"/>
+
+    <uses-feature android:name="android.hardware.location" android:required="false"/>
+    <uses-feature android:name="android.hardware.location.gps" android:required="false"/>
+    <uses-feature android:name="android.hardware.touchscreen"/>
+
+    <!--#ifdef MOZ_WEBRTC-->
+    <!--<uses-permission android:name="android.permission.RECORD_AUDIO"/>-->
+    <!--<uses-feature android:name="android.hardware.audio.low_latency" android:required="false"/>-->
+    <!--<uses-feature android:name="android.hardware.camera.any" android:required="false"/>-->
+    <!--<uses-feature android:name="android.hardware.microphone" android:required="false"/>-->
+    <!--#endif-->
+
+    <uses-permission android:name="android.permission.CAMERA" />
+    <uses-feature android:name="android.hardware.camera" android:required="false"/>
+    <uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
+
+    <!-- App requires OpenGL ES 2.0 -->
+    <uses-feature android:glEsVersion="0x00020000" android:required="true" />
+
+</manifest>
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -690,47 +690,16 @@ class GeckoLayerClient implements LayerV
     public void panZoomStopped() {
         mToolbarAnimator.onPanZoomStopped();
     }
 
     Object getLock() {
         return this;
     }
 
-    /**
-     * Converts a point from layer view coordinates to layer coordinates. In other words, given a
-     * point measured in pixels from the top left corner of the layer view, returns the point in
-     * pixels measured from the last scroll position we sent to Gecko, in CSS pixels. Assuming the
-     * events being sent to Gecko are processed in FIFO order, this calculation should always be
-     * correct.
-     */
-    PointF convertViewPointToLayerPoint(PointF viewPoint) {
-        if (!mGeckoIsReady) {
-            return null;
-        }
-
-        ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
-        PointF origin = viewportMetrics.getOrigin();
-        float zoom = viewportMetrics.zoomFactor;
-        ImmutableViewportMetrics geckoViewport = mViewportMetrics;
-        PointF geckoOrigin = geckoViewport.getOrigin();
-        float geckoZoom = geckoViewport.zoomFactor;
-
-        // viewPoint + origin - offset gives the coordinate in device pixels from the top-left corner of the page.
-        // Divided by zoom, this gives us the coordinate in CSS pixels from the top-left corner of the page.
-        // geckoOrigin / geckoZoom is where Gecko thinks it is (scrollTo position) in CSS pixels from
-        // the top-left corner of the page. Subtracting the two gives us the offset of the viewPoint from
-        // the current Gecko coordinate in CSS pixels.
-        PointF layerPoint = new PointF(
-                ((viewPoint.x + origin.x) / zoom) - (geckoOrigin.x / geckoZoom),
-                ((viewPoint.y + origin.y) / zoom) - (geckoOrigin.y / geckoZoom));
-
-        return layerPoint;
-    }
-
     Matrix getMatrixForLayerRectToViewRect() {
         if (!mGeckoIsReady) {
             return null;
         }
 
         ImmutableViewportMetrics viewportMetrics = mViewportMetrics;
         PointF origin = viewportMetrics.getOrigin();
         float zoom = viewportMetrics.zoomFactor;
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/LayerView.java
@@ -345,20 +345,16 @@ public class LayerView extends FrameLayo
 
     public PanZoomController getPanZoomController() { return mPanZoomController; }
     public DynamicToolbarAnimator getDynamicToolbarAnimator() { return mToolbarAnimator; }
 
     public ImmutableViewportMetrics getViewportMetrics() {
         return mLayerClient.getViewportMetrics();
     }
 
-    public PointF convertViewPointToLayerPoint(PointF viewPoint) {
-        return mLayerClient.convertViewPointToLayerPoint(viewPoint);
-    }
-
     public Matrix getMatrixForLayerRectToViewRect() {
         return mLayerClient.getMatrixForLayerRectToViewRect();
     }
 
     public void setSurfaceBackgroundColor(int newColor) {
         if (mSurfaceView != null) {
             mSurfaceView.setBackgroundColor(newColor);
         }
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/build.gradle
@@ -0,0 +1,62 @@
+buildDir "${topobjdir}/gradle/build/mobile/android/geckoview_example"
+
+apply plugin: 'com.android.application'
+
+android {
+    compileSdkVersion 23
+    buildToolsVersion mozconfig.substs.ANDROID_BUILD_TOOLS_VERSION
+
+    defaultConfig {
+        applicationId "org.mozilla.geckoview_example"
+        minSdkVersion 15
+        targetSdkVersion 23
+        versionCode 1
+        versionName "1.0"
+        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+    }
+
+    // This is extremely frustrating, but the only way to do it automation for
+    // now.  Without this, we only get a "debugAndroidTest" configuration; we
+    // have no "withoutGeckoBinariesAndroidTest" configuration.
+    testBuildType "withoutGeckoBinaries"
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+        }
+        withGeckoBinaries { // For consistency with :geckoview project in Task Cluster invocations.
+            initWith debug
+        }
+        withoutGeckoBinaries { // Logical negation of withGeckoBinaries.
+            initWith debug
+        }
+    }
+}
+
+dependencies {
+    testCompile 'junit:junit:4.12'
+
+    compile 'com.android.support:support-annotations:23.0.1'
+
+    // Later versions (2.2.2, 0.5) requires newer support libraries, leading to
+    // "Conflict with dependency 'com.android.support:support-annotations'. Resolved versions for app (23.0.1) and test app (23.1.1) differ."
+    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1'
+    androidTestCompile 'com.android.support.test:runner:0.4.1'
+
+    compile project(':geckoview')
+}
+
+apply from: "${topsrcdir}/mobile/android/gradle/with_gecko_binaries.gradle"
+
+android.applicationVariants.all { variant ->
+    // Like 'debug', 'release', or 'withoutGeckoBinaries'.
+    def buildType = variant.buildType.name
+
+    // It would be most natural for :geckoview to always include the Gecko
+    // binaries, but that's difficult; see the notes in
+    // mobile/android/gradle/with_gecko_binaries.gradle.  Instead we handle our
+    // own Gecko binary inclusion.
+    if (!buildType.equals('withoutGeckoBinaries')) {
+        configureVariantWithGeckoBinaries(variant)
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Users/nalexander/.mozbuild/android-sdk-macosx/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+#   http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+#   public *;
+#}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/ApplicationTest.java
@@ -0,0 +1,13 @@
+package org.mozilla.geckoview_example;
+
+import android.app.Application;
+import android.test.ApplicationTestCase;
+
+/**
+ * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
+ */
+public class ApplicationTest extends ApplicationTestCase<Application> {
+    public ApplicationTest() {
+        super(Application.class);
+    }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/androidTest/java/org/mozilla/geckoview_example/GeckoViewActivityTest.java
@@ -0,0 +1,32 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.geckoview_example;
+
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static android.support.test.espresso.Espresso.onView;
+import static android.support.test.espresso.assertion.ViewAssertions.matches;
+import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static android.support.test.espresso.matcher.ViewMatchers.withId;
+
+@RunWith(AndroidJUnit4.class)
+public class GeckoViewActivityTest {
+
+    @Rule
+    public ActivityTestRule<GeckoViewActivity> mActivityRule = new ActivityTestRule(GeckoViewActivity.class);
+
+    @Test
+    public void testA() throws InterruptedException {
+        onView(withId(R.id.gecko_view))
+                .check(matches(isDisplayed()));
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="org.mozilla.geckoview_example">
+
+    <application android:allowBackup="true"
+                 android:label="@string/app_name"
+                 android:supportsRtl="true">
+
+        <uses-library android:name="android.test.runner" />
+
+        <activity android:name="org.mozilla.geckoview_example.GeckoViewActivity"
+                  android:label="GeckoViewActivity">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+    </application>
+
+</manifest>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -0,0 +1,142 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * 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/. */
+
+package org.mozilla.geckoview_example;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.Toast;
+
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoThread;
+import org.mozilla.gecko.GeckoView;
+import org.mozilla.gecko.PrefsHelper;
+
+public class GeckoViewActivity extends Activity {
+    private static final String LOGTAG = "GeckoViewActivity";
+
+    GeckoView mGeckoView;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.geckoview_activity);
+
+        mGeckoView = (GeckoView) findViewById(R.id.gecko_view);
+        mGeckoView.setChromeDelegate(new MyGeckoViewChrome());
+        mGeckoView.setContentDelegate(new MyGeckoViewContent());
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+
+        final GeckoProfile profile = GeckoProfile.get(getApplicationContext());
+
+        GeckoThread.init(profile, /* args */ null, /* action */ null, /* debugging */ false);
+        GeckoThread.launch();
+    }
+
+    private class MyGeckoViewChrome implements GeckoView.ChromeDelegate {
+        @Override
+        public void onReady(GeckoView view) {
+            Log.i(LOGTAG, "Gecko is ready");
+            // // Inject a script that adds some code to the content window
+            // mGeckoView.importScript("resource://android/assets/script.js");
+
+            // Set up remote debugging to a port number
+            PrefsHelper.setPref("layers.dump", true);
+            PrefsHelper.setPref("devtools.debugger.remote-port", 6000);
+            PrefsHelper.setPref("devtools.debugger.unix-domain-socket", "");
+            PrefsHelper.setPref("devtools.debugger.remote-enabled", true);
+
+            // The Gecko libraries have finished loading and we can use the rendering engine.
+            // Let's add a browser (required) and load a page into it.
+            // mGeckoView.addBrowser(getResources().getString(R.string.default_url));
+        }
+
+        @Override
+        public void onAlert(GeckoView view, GeckoView.Browser browser, String message, GeckoView.PromptResult result) {
+            Log.i(LOGTAG, "Alert!");
+            result.confirm();
+            Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show();
+        }
+
+        @Override
+        public void onConfirm(GeckoView view, GeckoView.Browser browser, String message, final GeckoView.PromptResult result) {
+            Log.i(LOGTAG, "Confirm!");
+            new AlertDialog.Builder(GeckoViewActivity.this)
+                .setTitle("javaScript dialog")
+                .setMessage(message)
+                .setPositiveButton(android.R.string.ok,
+                                   new DialogInterface.OnClickListener() {
+                                       public void onClick(DialogInterface dialog, int which) {
+                                           result.confirm();
+                                       }
+                                   })
+                .setNegativeButton(android.R.string.cancel,
+                                   new DialogInterface.OnClickListener() {
+                                       public void onClick(DialogInterface dialog, int which) {
+                                           result.cancel();
+                                       }
+                                   })
+                .create()
+                .show();
+        }
+
+        @Override
+        public void onPrompt(GeckoView view, GeckoView.Browser browser, String message, String defaultValue, GeckoView.PromptResult result) {
+            result.cancel();
+        }
+
+        @Override
+        public void onDebugRequest(GeckoView view, GeckoView.PromptResult result) {
+            Log.i(LOGTAG, "Remote Debug!");
+            result.confirm();
+        }
+
+        @Override
+        public void onScriptMessage(GeckoView view, Bundle data, GeckoView.MessageResult result) {
+            Log.i(LOGTAG, "Got Script Message: " + data.toString());
+            String type = data.getString("type");
+            if ("fetch".equals(type)) {
+                Bundle ret = new Bundle();
+                ret.putString("name", "Mozilla");
+                ret.putString("url", "https://mozilla.org");
+                result.success(ret);
+            }
+        }
+    }
+
+    private class MyGeckoViewContent implements GeckoView.ContentDelegate {
+        @Override
+        public void onPageStart(GeckoView view, GeckoView.Browser browser, String url) {
+
+        }
+
+        @Override
+        public void onPageStop(GeckoView view, GeckoView.Browser browser, boolean success) {
+
+        }
+
+        @Override
+        public void onPageShow(GeckoView view, GeckoView.Browser browser) {
+
+        }
+
+        @Override
+        public void onReceivedTitle(GeckoView view, GeckoView.Browser browser, String title) {
+            Log.i(LOGTAG, "Received a title: " + title);
+        }
+
+        @Override
+        public void onReceivedFavicon(GeckoView view, GeckoView.Browser browser, String url, int size) {
+            Log.i(LOGTAG, "Received a favicon URL: " + url);
+        }
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/layout/geckoview_activity.xml
@@ -0,0 +1,13 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="fill_parent"
+              android:layout_height="fill_parent"
+              android:orientation="vertical">
+
+    <org.mozilla.gecko.GeckoView
+        android:id="@+id/gecko_view"
+        android:layout_width="fill_parent"
+        android:layout_height="match_parent"
+        android:scrollbars="none"
+        />
+
+</LinearLayout>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="colorPrimary">#3F51B5</color>
+    <color name="colorPrimaryDark">#303F9F</color>
+    <color name="colorAccent">#FF4081</color>
+</resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">geckoview_example</string>
+</resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/geckoview_example/src/test/java/org/mozilla/geckoview_example/ExampleUnitTest.java
@@ -0,0 +1,15 @@
+package org.mozilla.geckoview_example;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * To work on unit tests, switch the Test Artifact in the Build Variants view.
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() throws Exception {
+        assertEquals(4, 2 + 2);
+    }
+}
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/gradle/with_gecko_binaries.gradle
@@ -0,0 +1,105 @@
+// We run fairly hard into a fundamental limitation of the Android Gradle
+// plugin.  There are many bugs filed about this, but
+// https://code.google.com/p/android/issues/detail?id=216978#c6 is a reason one.
+// The issue is that we need fine-grained control over when to include Gecko's
+// binary libraries into the GeckoView AAR and the Fennec APK, and that's hard
+// to achieve.  In particular:
+//
+// * :app:automation wants :geckoview to not include Gecko binaries (automation
+// * build, before package)
+//
+// * :geckoview:withLibraries wants :geckoview to include Gecko binaries
+// * (automation build, after package)
+//
+// * non-:app:automation wants :geckoview to include Gecko binaries (local
+// * build, always after package)
+//
+// publishNonDefault (see
+// http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Library-Publication)
+// is intended to address this, but doesn't handle our case.  That option always
+// builds *all* configurations, which fails when the required Gecko binaries
+// don't exist (automation build, before package).  So instead, we make both
+// :app and :geckoview both know how to include the Gecko binaries, and use a
+// non-default, non-published :geckoview:withGeckoBinaries configuration to
+// handle automation's needs.  Simple, right?
+
+// The omnijar inputs are listed as resource directory inputs to a dummy JAR.
+// That arrangement labels them nicely in IntelliJ.  See the comment in the
+// :omnijar project for more context.
+evaluationDependsOn(':omnijar')
+
+task buildOmnijar(type:Exec) {
+    dependsOn rootProject.generateCodeAndResources
+
+    // See comment in :omnijar project regarding interface mismatches here.
+    inputs.source project(':omnijar').sourceSets.main.resources.srcDirs
+
+    // Produce a single output file.
+    outputs.file "${topobjdir}/dist/fennec/assets/omni.ja"
+
+    workingDir "${topobjdir}"
+
+    commandLine mozconfig.substs.GMAKE
+    args '-C'
+    args "${topobjdir}/mobile/android/base"
+    args 'gradle-omnijar'
+
+    // Only show the output if something went wrong.
+    ignoreExitValue = true
+    standardOutput = new ByteArrayOutputStream()
+    errorOutput = standardOutput
+    doLast {
+        if (execResult.exitValue != 0) {
+            throw new GradleException("Process '${commandLine}' finished with non-zero exit value ${execResult.exitValue}:\n\n${standardOutput.toString()}")
+        }
+    }
+}
+
+task syncOmnijarFromDistDir(type: Sync) {
+    into("${project.buildDir}/generated/omnijar")
+    from("${topobjdir}/dist/fennec/assets") {
+        include 'omni.ja'
+    }
+}
+
+task checkLibsExistInDistDir<< {
+    if (syncLibsFromDistDir.source.empty) {
+        throw new GradleException("Required JNI libraries not found in ${topobjdir}/dist/fennec/lib.  Have you built and packaged?")
+    }
+}
+
+task syncLibsFromDistDir(type: Sync, dependsOn: checkLibsExistInDistDir) {
+    into("${project.buildDir}/generated/jniLibs")
+    from("${topobjdir}/dist/fennec/lib")
+}
+
+task checkAssetsExistInDistDir<< {
+    if (syncAssetsFromDistDir.source.empty) {
+        throw new GradleException("Required assets not found in ${topobjdir}/dist/fennec/assets.  Have you built and packaged?")
+    }
+}
+
+task syncAssetsFromDistDir(type: Sync, dependsOn: checkAssetsExistInDistDir) {
+    into("${project.buildDir}/generated/assets")
+    from("${topobjdir}/dist/fennec/assets") {
+        exclude 'omni.ja'
+    }
+}
+
+ext.configureVariantWithGeckoBinaries = { variant ->
+    // Like 'local' or 'localOld'; may be null.
+    def productFlavor = variant.productFlavors ? variant.productFlavors[0].name : ""
+    // Like 'debug' or 'release'.
+    def buildType = variant.buildType.name
+
+    syncOmnijarFromDistDir.dependsOn buildOmnijar
+    def generateAssetsTask = tasks.findByName("generate${productFlavor.capitalize()}${buildType.capitalize()}Assets")
+    generateAssetsTask.dependsOn syncOmnijarFromDistDir
+    generateAssetsTask.dependsOn syncLibsFromDistDir
+    generateAssetsTask.dependsOn syncAssetsFromDistDir
+
+    def sourceSet = productFlavor ? "${productFlavor}${buildType.capitalize()}" : buildType
+    android.sourceSets."${sourceSet}".assets.srcDir syncOmnijarFromDistDir.destinationDir
+    android.sourceSets."${sourceSet}".assets.srcDir syncAssetsFromDistDir.destinationDir
+    android.sourceSets."${sourceSet}".jniLibs.srcDir syncLibsFromDistDir.destinationDir
+}
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/home/TestHomeConfigPrefsBackendMigration.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/home/TestHomeConfigPrefsBackendMigration.java
@@ -76,17 +76,34 @@ public class TestHomeConfigPrefsBackendM
                 },
                 new PanelType[] {
                         // Last version: no migration exists yet, we only need to define a list
                         // of expected panels.
                 }
         ));
     }
 
-    private JSONArray createConfigsForList(Context context, PanelType[] panels, int defaultIndex) throws JSONException {
+    private JSONArray createDisabledConfigsForList(Context context,
+                                                   PanelType[] panels) throws JSONException {
+        final JSONArray jsonPanels = new JSONArray();
+
+        for (int i = 0; i < panels.length; i++) {
+            final PanelType panel = panels[i];
+
+            jsonPanels.put(HomeConfig.createBuiltinPanelConfig(context, panel,
+                    EnumSet.of(PanelConfig.Flags.DISABLED_PANEL)).toJSON());
+        }
+
+        return jsonPanels;
+
+    }
+
+
+    private JSONArray createConfigsForList(Context context, PanelType[] panels,
+                                           int defaultIndex) throws JSONException {
         if (defaultIndex < 0 || defaultIndex >= panels.length) {
             throw new IllegalArgumentException("defaultIndex must point to panel in the array");
         }
 
         final JSONArray jsonPanels = new JSONArray();
 
         for (int i = 0; i < panels.length; i++) {
             final PanelType panel = panels[i];
@@ -115,17 +132,27 @@ public class TestHomeConfigPrefsBackendM
             if (panelConfig.isDefault()) {
                 return panelConfig.getType();
             }
         }
 
         return null;
     }
 
-    private void checkListContainsExpectedPanels(JSONArray jsonPanels, PanelType[] expected) throws JSONException {
+    private void checkAllPanelsAreDisabled(JSONArray jsonPanels) throws JSONException {
+        for (int i = 0; i < jsonPanels.length(); i++) {
+            final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
+            final PanelConfig config = new PanelConfig(jsonPanelConfig);
+
+            assertTrue("Non disabled panel \"" + config.getType().name() + "\" found in list, excpected all panels to be disabled", config.isDisabled());
+        }
+    }
+
+    private void checkListContainsExpectedPanels(JSONArray jsonPanels,
+                                                 PanelType[] expected) throws JSONException {
         // Given the short lists we have here an ArraySet might be more appropriate, but it requires API >= 23.
         final Set<PanelType> expectedSet = new HashSet<>();
         for (PanelType panelType : expected) {
             expectedSet.add(panelType);
         }
 
         for (int i = 0; i < jsonPanels.length(); i++) {
             final JSONObject jsonPanelConfig = jsonPanels.getJSONObject(i);
@@ -147,17 +174,17 @@ public class TestHomeConfigPrefsBackendM
 
         final Pair<PanelType[], PanelType[]> finalConstellation = migrationConstellations.get(HomeConfigPrefsBackend.VERSION);
         assertNotNull("It looks like you added a HomeConfig migration, please add an appropriate entry to migrationConstellations",
                 finalConstellation);
 
         // We want to calculate the number of iterations here to make sure we cover all provided constellations.
         // Iterating over the array and manually checking for each version could result in constellations
         // being skipped if there are any gaps in the array
-        final  int firstTestedVersion = HomeConfigPrefsBackend.VERSION - (migrationConstellations.size() - 1);
+        final int firstTestedVersion = HomeConfigPrefsBackend.VERSION - (migrationConstellations.size() - 1);
 
         // The last constellation is only used for the counts / expected outputs, hence we start
         // with the second-last constellation
         for (int testVersion = HomeConfigPrefsBackend.VERSION - 1; testVersion >= firstTestedVersion; testVersion--) {
 
             final Pair<PanelType[], PanelType[]> currentConstellation = migrationConstellations.get(testVersion);
             assertNotNull("No constellation for version " + testVersion + " - you must provide a constellation for every version upgrade in the list",
                     currentConstellation);
@@ -191,9 +218,47 @@ public class TestHomeConfigPrefsBackendM
 
                 assertEquals("Number of panels after migration doesn't match expected count",
                         jsonPanels.length(), expectedOutputList.length);
 
                 checkListContainsExpectedPanels(jsonPanels, expectedOutputList);
             }
         }
     }
+
+    // Test that if all panels are disabled, the migration retains all panels as being disabled
+    // (in addition to correctly removing panels as necessary).
+    @Test
+    public void testMigrationRetainsAllPanelsHiddenAfter6() throws JSONException {
+        final Context context = RuntimeEnvironment.application;
+
+        final Pair<PanelType[], PanelType[]> finalConstellation = migrationConstellations.get(HomeConfigPrefsBackend.VERSION);
+        assertNotNull("It looks like you added a HomeConfig migration, please add an appropriate entry to migrationConstellations",
+                finalConstellation);
+
+        final int firstTestedVersion = HomeConfigPrefsBackend.VERSION - (migrationConstellations.size() - 1);
+
+        for (int testVersion = HomeConfigPrefsBackend.VERSION - 1; testVersion >= firstTestedVersion; testVersion--) {
+            final Pair<PanelType[], PanelType[]> currentConstellation = migrationConstellations.get(testVersion);
+            assertNotNull("No constellation for version " + testVersion + " - you must provide a constellation for every version upgrade in the list",
+                    currentConstellation);
+
+            final PanelType[] inputList = currentConstellation.first;
+
+            JSONArray jsonPanels = createDisabledConfigsForList(context, inputList);
+
+            jsonPanels = HomeConfigPrefsBackend.migratePrefsFromVersionToVersion(context, testVersion, testVersion + 1, jsonPanels, null);
+
+            // All panels should remain disabled after the migration
+            checkAllPanelsAreDisabled(jsonPanels);
+
+            // Duplicated from previous test:
+            // Verify that the panels remaining after the migration correspond to the input panels
+            // for the next migration
+            final PanelType[] expectedOutputList = migrationConstellations.get(testVersion + 1).first;
+
+            assertEquals("Number of panels after migration doesn't match expected count",
+                    jsonPanels.length(), expectedOutputList.length);
+
+            checkListContainsExpectedPanels(jsonPanels, expectedOutputList);
+        }
+    }
 }
--- a/mobile/android/thirdparty/build.gradle
+++ b/mobile/android/thirdparty/build.gradle
@@ -20,17 +20,16 @@ android {
         abortOnError false
     }
 
     sourceSets {
         main {
             manifest.srcFile 'AndroidManifest.xml'
             java {
                 srcDir '.'
-                srcDir "${topsrcdir}/mobile/android/geckoview/src/thirdparty/java"
 
                 if (!mozconfig.substs.MOZ_INSTALL_TRACKING) {
                     exclude 'com/adjust/**'
                 }
 
                 // Exclude LeakCanary: It will be added again via a gradle dependency. This version
                 // here is only the no-op library for mach-based builds.
                 exclude 'com/squareup/leakcanary/**'
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -417,16 +417,18 @@ pref("media.navigator.load_adapt.high_lo
 pref("media.navigator.load_adapt.low_load","0.40");
 pref("media.navigator.video.default_fps",30);
 pref("media.navigator.video.default_minfps",10);
 pref("media.navigator.video.use_remb", true);
 pref("media.navigator.video.use_tmmbr", false);
 pref("media.navigator.audio.use_fec", true);
 pref("media.navigator.video.red_ulpfec_enabled", false);
 
+pref("media.peerconnection.dtmf.enabled", false);
+
 pref("media.webrtc.debug.trace_mask", 0);
 pref("media.webrtc.debug.multi_log", false);
 pref("media.webrtc.debug.aec_log_dir", "");
 pref("media.webrtc.debug.log_file", "");
 pref("media.webrtc.debug.aec_dump_max_size", 4194304); // 4MB
 
 #ifdef MOZ_WIDGET_GONK
 pref("media.navigator.video.default_width", 320);
--- a/security/sandbox/linux/SandboxBrokerClient.cpp
+++ b/security/sandbox/linux/SandboxBrokerClient.cpp
@@ -30,23 +30,25 @@ SandboxBrokerClient::SandboxBrokerClient
 
 SandboxBrokerClient::~SandboxBrokerClient()
 {
   close(mFileDesc);
 }
 
 int
 SandboxBrokerClient::DoCall(const Request* aReq, const char* aPath,
-                            struct stat* aStat, bool expectFd)
+                            const char* aPath2, void* aResponseBuff,
+                            bool expectFd)
 {
   // Remap /proc/self to the actual pid, so that the broker can open
   // it.  This happens here instead of in the broker to follow the
   // principle of least privilege and keep the broker as simple as
   // possible.  (Note: when pid namespaces happen, this will also need
   // to remap the inner pid to the outer pid.)
+  // We only remap the first path.
   static const char kProcSelf[] = "/proc/self/";
   static const size_t kProcSelfLen = sizeof(kProcSelf) - 1;
   const char* path = aPath;
   // This buffer just needs to be large enough for any such path that
   // the policy would actually allow.  sizeof("/proc/2147483647/") == 18.
   char rewrittenPath[64];
   if (strncmp(aPath, kProcSelf, kProcSelfLen) == 0) {
     ssize_t len =
@@ -57,125 +59,189 @@ SandboxBrokerClient::DoCall(const Reques
         SANDBOX_LOG_ERROR("rewriting %s -> %s", aPath, rewrittenPath);
       }
       path = rewrittenPath;
     } else {
       SANDBOX_LOG_ERROR("not rewriting unexpectedly long path %s", aPath);
     }
   }
 
-  struct iovec ios[2];
+  struct iovec ios[3];
   int respFds[2];
 
   // Set up iovecs for request + path.
   ios[0].iov_base = const_cast<Request*>(aReq);
   ios[0].iov_len = sizeof(*aReq);
   ios[1].iov_base = const_cast<char*>(path);
-  ios[1].iov_len = strlen(path);
+  ios[1].iov_len = strlen(path) + 1;
+  if (aPath2 != nullptr) {
+    ios[2].iov_base = const_cast<char*>(aPath2);
+    ios[2].iov_len = strlen(aPath2) + 1;
+  } else {
+    ios[2].iov_base = 0;
+    ios[2].iov_len = 0;
+  }
   if (ios[1].iov_len > kMaxPathLen) {
     return -ENAMETOOLONG;
   }
+  if (ios[2].iov_len > kMaxPathLen) {
+    return -ENAMETOOLONG;
+  }
 
   // Create response socket and send request.
   if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, respFds) < 0) {
     return -errno;
   }
-  const ssize_t sent = SendWithFd(mFileDesc, ios, 2, respFds[1]);
+  const ssize_t sent = SendWithFd(mFileDesc, ios, 3, respFds[1]);
   const int sendErrno = errno;
   MOZ_ASSERT(sent < 0 ||
-	     static_cast<size_t>(sent) == ios[0].iov_len + ios[1].iov_len);
+             static_cast<size_t>(sent) == ios[0].iov_len
+                                        + ios[1].iov_len
+                                        + ios[2].iov_len);
   close(respFds[1]);
   if (sent < 0) {
     close(respFds[0]);
     return -sendErrno;
   }
 
   // Set up iovecs for response.
   Response resp;
   ios[0].iov_base = &resp;
   ios[0].iov_len = sizeof(resp);
-  if (aStat) {
-    ios[1].iov_base = aStat;
-    ios[1].iov_len = sizeof(*aStat);
+  if (aResponseBuff) {
+    ios[1].iov_base = aResponseBuff;
+    ios[1].iov_len = aReq->mBufSize;
   } else {
     ios[1].iov_base = nullptr;
     ios[1].iov_len = 0;
   }
 
   // Wait for response and return appropriately.
   int openedFd = -1;
-  const ssize_t recvd = RecvWithFd(respFds[0], ios, aStat ? 2 : 1,
+  const ssize_t recvd = RecvWithFd(respFds[0], ios, aResponseBuff ? 2 : 1,
                                    expectFd ? &openedFd : nullptr);
   const int recvErrno = errno;
   close(respFds[0]);
   if (recvd < 0) {
     return -recvErrno;
   }
   if (recvd == 0) {
     SANDBOX_LOG_ERROR("Unexpected EOF, op %d flags 0%o path %s",
                       aReq->mOp, aReq->mFlags, path);
     return -EIO;
   }
-  if (resp.mError != 0) {
-    // If the operation fails, the return payload will be empty;
-    // adjust the iov_len for the following assertion.
-    ios[1].iov_len = 0;
-  }
-  MOZ_ASSERT(static_cast<size_t>(recvd) == ios[0].iov_len + ios[1].iov_len);
-  if (resp.mError == 0) {
+  MOZ_ASSERT(static_cast<size_t>(recvd) <= ios[0].iov_len + ios[1].iov_len);
+  // Some calls such as readlink return a size if successful
+  if (resp.mError >= 0) {
     // Success!
     if (expectFd) {
       MOZ_ASSERT(openedFd >= 0);
       return openedFd;
     }
-    return 0;
+    return resp.mError;
   }
   if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
     // Keep in mind that "rejected" files can include ones that don't
     // actually exist, if it's something that's optional or part of a
     // search path (e.g., shared libraries).  In those cases, this
     // error message is expected.
     SANDBOX_LOG_ERROR("Rejected errno %d op %d flags 0%o path %s",
                       resp.mError, aReq->mOp, aReq->mFlags, path);
   }
   if (openedFd >= 0) {
     close(openedFd);
   }
-  return -resp.mError;
+  return resp.mError;
 }
 
 int
 SandboxBrokerClient::Open(const char* aPath, int aFlags)
 {
-  Request req = { SANDBOX_FILE_OPEN, aFlags };
-  int maybeFd = DoCall(&req, aPath, nullptr, true);
+  Request req = { SANDBOX_FILE_OPEN, aFlags, 0 };
+  int maybeFd = DoCall(&req, aPath, nullptr, nullptr, true);
   if (maybeFd >= 0) {
     // NSPR has opinions about file flags.  Fix O_CLOEXEC.
     if ((aFlags & O_CLOEXEC) == 0) {
       fcntl(maybeFd, F_SETFD, 0);
     }
   }
   return maybeFd;
 }
 
 int
 SandboxBrokerClient::Access(const char* aPath, int aMode)
 {
-  Request req = { SANDBOX_FILE_ACCESS, aMode };
-  return DoCall(&req, aPath, nullptr, false);
+  Request req = { SANDBOX_FILE_ACCESS, aMode, 0 };
+  return DoCall(&req, aPath, nullptr, nullptr, false);
+}
+
+int
+SandboxBrokerClient::Stat(const char* aPath, statstruct* aStat)
+{
+  Request req = { SANDBOX_FILE_STAT, 0, sizeof(statstruct) };
+  return DoCall(&req, aPath, nullptr, (void*)aStat, false);
+}
+
+int
+SandboxBrokerClient::LStat(const char* aPath, statstruct* aStat)
+{
+  Request req = { SANDBOX_FILE_STAT, O_NOFOLLOW, sizeof(statstruct) };
+  return DoCall(&req, aPath, nullptr, (void*)aStat, false);
+}
+
+int
+SandboxBrokerClient::Chmod(const char* aPath, int aMode)
+{
+  Request req = {SANDBOX_FILE_CHMOD, aMode, 0};
+  return DoCall(&req, aPath, nullptr, nullptr, false);
+}
+
+int
+SandboxBrokerClient::Link(const char* aOldPath, const char* aNewPath)
+{
+  Request req = {SANDBOX_FILE_LINK, 0, 0};
+  return DoCall(&req, aOldPath, aNewPath, nullptr, false);
 }
 
 int
-SandboxBrokerClient::Stat(const char* aPath, struct stat* aStat)
+SandboxBrokerClient::Symlink(const char* aOldPath, const char* aNewPath)
 {
-  Request req = { SANDBOX_FILE_STAT, 0 };
-  return DoCall(&req, aPath, aStat, false);
+  Request req = {SANDBOX_FILE_SYMLINK, 0, 0};
+  return DoCall(&req, aOldPath, aNewPath, nullptr, false);
+}
+
+int
+SandboxBrokerClient::Rename(const char* aOldPath, const char* aNewPath)
+{
+  Request req = {SANDBOX_FILE_RENAME, 0, 0};
+  return DoCall(&req, aOldPath, aNewPath, nullptr, false);
 }
 
 int
-SandboxBrokerClient::LStat(const char* aPath, struct stat* aStat)
+SandboxBrokerClient::Mkdir(const char* aPath, int aMode)
+{
+  Request req = {SANDBOX_FILE_MKDIR, aMode, 0};
+  return DoCall(&req, aPath, nullptr, nullptr, false);
+}
+
+int
+SandboxBrokerClient::Unlink(const char* aPath)
 {
-  Request req = { SANDBOX_FILE_STAT, O_NOFOLLOW };
-  return DoCall(&req, aPath, aStat, false);
+  Request req = {SANDBOX_FILE_UNLINK, 0, 0};
+  return DoCall(&req, aPath, nullptr, nullptr, false);
+}
+
+int
+SandboxBrokerClient::Rmdir(const char* aPath)
+{
+  Request req = {SANDBOX_FILE_RMDIR, 0, 0};
+  return DoCall(&req, aPath, nullptr, nullptr, false);
+}
+
+int
+SandboxBrokerClient::Readlink(const char* aPath, void* aBuff, size_t aSize)
+{
+  Request req = {SANDBOX_FILE_READLINK, 0, aSize};
+  return DoCall(&req, aPath, nullptr, aBuff, false);
 }
 
 } // namespace mozilla
 
--- a/security/sandbox/linux/SandboxBrokerClient.h
+++ b/security/sandbox/linux/SandboxBrokerClient.h
@@ -3,16 +3,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_SandboxBrokerClient_h
 #define mozilla_SandboxBrokerClient_h
 
 #include "broker/SandboxBrokerCommon.h"
+#include "broker/SandboxBrokerUtils.h"
 
 #include "mozilla/Attributes.h"
 
 // This is the client for the sandbox broker described in
 // broker/SandboxBroker.h; its constructor takes the file descriptor
 // returned by SandboxBroker::Create, passed to the child over IPC.
 //
 // The operations exposed here can be called from any thread and in
@@ -26,21 +27,32 @@ namespace mozilla {
 
 class SandboxBrokerClient final : private SandboxBrokerCommon {
  public:
   explicit SandboxBrokerClient(int aFd);
   ~SandboxBrokerClient();
 
   int Open(const char* aPath, int aFlags);
   int Access(const char* aPath, int aMode);
-  int Stat(const char* aPath, struct stat* aStat);
-  int LStat(const char* aPath, struct stat* aStat);
+  int Stat(const char* aPath, statstruct* aStat);
+  int LStat(const char* aPath, statstruct* aStat);
+  int Chmod(const char* aPath, int aMode);
+  int Link(const char* aPath, const char* aPath2);
+  int Mkdir(const char* aPath, int aMode);
+  int Symlink(const char* aOldPath, const char* aNewPath);
+  int Rename(const char* aOldPath, const char* aNewPath);
+  int Unlink(const char* aPath);
+  int Rmdir(const char* aPath);
+  int Readlink(const char* aPath, void* aBuf, size_t aBufSize);
 
  private:
   int mFileDesc;
 
-  int DoCall(const Request* aReq, const char* aPath, struct stat* aStat,
+  int DoCall(const Request* aReq,
+             const char* aPath,
+             const char* aPath2,
+             void *aReponseBuff,
              bool expectFd);
 };
 
 } // namespace mozilla
 
 #endif // mozilla_SandboxBrokerClient_h
--- a/security/sandbox/linux/SandboxFilter.cpp
+++ b/security/sandbox/linux/SandboxFilter.cpp
@@ -378,48 +378,103 @@ class ContentSandboxPolicy : public Sand
       return BlockedSyscallTrap(aArgs, nullptr);
     }
     return broker->Access(path, mode);
   }
 
   static intptr_t StatTrap(ArgsRef aArgs, void* aux) {
     auto broker = static_cast<SandboxBrokerClient*>(aux);
     auto path = reinterpret_cast<const char*>(aArgs.args[0]);
-    auto buf = reinterpret_cast<struct stat*>(aArgs.args[1]);
+    auto buf = reinterpret_cast<statstruct*>(aArgs.args[1]);
     return broker->Stat(path, buf);
   }
 
   static intptr_t LStatTrap(ArgsRef aArgs, void* aux) {
     auto broker = static_cast<SandboxBrokerClient*>(aux);
     auto path = reinterpret_cast<const char*>(aArgs.args[0]);
-    auto buf = reinterpret_cast<struct stat*>(aArgs.args[1]);
+    auto buf = reinterpret_cast<statstruct*>(aArgs.args[1]);
     return broker->LStat(path, buf);
   }
 
   static intptr_t StatAtTrap(ArgsRef aArgs, void* aux) {
     auto broker = static_cast<SandboxBrokerClient*>(aux);
     auto fd = static_cast<int>(aArgs.args[0]);
     auto path = reinterpret_cast<const char*>(aArgs.args[1]);
-    auto buf = reinterpret_cast<struct stat*>(aArgs.args[2]);
+    auto buf = reinterpret_cast<statstruct*>(aArgs.args[2]);
     auto flags = static_cast<int>(aArgs.args[3]);
     if (fd != AT_FDCWD && path[0] != '/') {
       SANDBOX_LOG_ERROR("unsupported fd-relative fstatat(%d, \"%s\", %p, %d)",
                         fd, path, buf, flags);
       return BlockedSyscallTrap(aArgs, nullptr);
     }
     if ((flags & ~AT_SYMLINK_NOFOLLOW) != 0) {
       SANDBOX_LOG_ERROR("unsupported flags %d in fstatat(%d, \"%s\", %p, %d)",
                         (flags & ~AT_SYMLINK_NOFOLLOW), fd, path, buf, flags);
       return BlockedSyscallTrap(aArgs, nullptr);
     }
     return (flags & AT_SYMLINK_NOFOLLOW) == 0
       ? broker->Stat(path, buf)
       : broker->LStat(path, buf);
   }
 
+  static intptr_t ChmodTrap(ArgsRef aArgs, void* aux) {
+    auto broker = static_cast<SandboxBrokerClient*>(aux);
+    auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+    auto mode = static_cast<mode_t>(aArgs.args[1]);
+    return broker->Chmod(path, mode);
+  }
+
+  static intptr_t LinkTrap(ArgsRef aArgs, void *aux) {
+    auto broker = static_cast<SandboxBrokerClient*>(aux);
+    auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+    auto path2 = reinterpret_cast<const char*>(aArgs.args[1]);
+    return broker->Link(path, path2);
+  }
+
+  static intptr_t SymlinkTrap(ArgsRef aArgs, void *aux) {
+    auto broker = static_cast<SandboxBrokerClient*>(aux);
+    auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+    auto path2 = reinterpret_cast<const char*>(aArgs.args[1]);
+    return broker->Symlink(path, path2);
+  }
+
+  static intptr_t RenameTrap(ArgsRef aArgs, void *aux) {
+    auto broker = static_cast<SandboxBrokerClient*>(aux);
+    auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+    auto path2 = reinterpret_cast<const char*>(aArgs.args[1]);
+    return broker->Rename(path, path2);
+  }
+
+  static intptr_t MkdirTrap(ArgsRef aArgs, void* aux) {
+    auto broker = static_cast<SandboxBrokerClient*>(aux);
+    auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+    auto mode = static_cast<mode_t>(aArgs.args[1]);
+    return broker->Mkdir(path, mode);
+  }
+
+  static intptr_t RmdirTrap(ArgsRef aArgs, void* aux) {
+    auto broker = static_cast<SandboxBrokerClient*>(aux);
+    auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+    return broker->Rmdir(path);
+  }
+
+  static intptr_t UnlinkTrap(ArgsRef aArgs, void* aux) {
+    auto broker = static_cast<SandboxBrokerClient*>(aux);
+    auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+    return broker->Unlink(path);
+  }
+
+  static intptr_t ReadlinkTrap(ArgsRef aArgs, void* aux) {
+    auto broker = static_cast<SandboxBrokerClient*>(aux);
+    auto path = reinterpret_cast<const char*>(aArgs.args[0]);
+    auto buf = reinterpret_cast<char*>(aArgs.args[1]);
+    auto size = static_cast<size_t>(aArgs.args[2]);
+    return broker->Readlink(path, buf, size);
+  }
+
   static intptr_t GetPPidTrap(ArgsRef aArgs, void* aux) {
     // In a pid namespace, getppid() will return 0. We will return 0 instead
     // of the real parent pid to see what breaks when we introduce the
     // pid namespace (Bug 1151624).
     return 0;
   }
 
 public:
@@ -508,16 +563,32 @@ public:
       case __NR_faccessat:
         return Trap(AccessAtTrap, mBroker);
       CASES_FOR_stat:
         return Trap(StatTrap, mBroker);
       CASES_FOR_lstat:
         return Trap(LStatTrap, mBroker);
       CASES_FOR_fstatat:
         return Trap(StatAtTrap, mBroker);
+      case __NR_chmod:
+        return Trap(ChmodTrap, mBroker);
+      case __NR_link:
+        return Trap(LinkTrap, mBroker);
+      case __NR_mkdir:
+        return Trap(MkdirTrap, mBroker);
+      case __NR_symlink:
+        return Trap(SymlinkTrap, mBroker);
+      case __NR_rename:
+        return Trap(RenameTrap, mBroker);
+      case __NR_rmdir:
+        return Trap(RmdirTrap, mBroker);
+      case __NR_unlink:
+        return Trap(UnlinkTrap, mBroker);
+      case __NR_readlink:
+        return Trap(ReadlinkTrap, mBroker);
       }
     } else {
       // No broker; allow the syscalls directly.  )-:
       switch(sysno) {
       case __NR_open:
       case __NR_openat:
       case __NR_access:
       case __NR_faccessat:
@@ -530,34 +601,26 @@ public:
 
     switch (sysno) {
 #ifdef DESKTOP
     case __NR_getppid:
       return Trap(GetPPidTrap, nullptr);
 
       // Filesystem syscalls that need more work to determine who's
       // using them, if they need to be, and what we intend to about it.
-    case __NR_mkdir:
-    case __NR_rmdir:
     case __NR_getcwd:
     CASES_FOR_statfs:
     CASES_FOR_fstatfs:
-    case __NR_chmod:
-    case __NR_rename:
-    case __NR_symlink:
     case __NR_quotactl:
-    case __NR_link:
-    case __NR_unlink:
     CASES_FOR_fchown:
     case __NR_fchmod:
     case __NR_flock:
 #endif
       return Allow();
 
-    case __NR_readlink:
     case __NR_readlinkat:
 #ifdef DESKTOP
       // Bug 1290896
       return Allow();
 #else
       // Workaround for bug 964455:
       return Error(EINVAL);
 #endif
--- a/security/sandbox/linux/broker/SandboxBroker.cpp
+++ b/security/sandbox/linux/broker/SandboxBroker.cpp
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "SandboxBroker.h"
 #include "SandboxInfo.h"
 #include "SandboxLogging.h"
+#include "SandboxBrokerUtils.h"
 
 #include <dirent.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <sys/socket.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <unistd.h>
@@ -26,16 +27,17 @@
 #endif
 
 #include "mozilla/Assertions.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/Move.h"
 #include "mozilla/NullPtr.h"
 #include "mozilla/Sprintf.h"
 #include "mozilla/ipc/FileDescriptor.h"
+#include "sandbox/linux/system_headers/linux_syscalls.h"
 
 namespace mozilla {
 
 // This constructor signals failure by setting mFileDesc and aClientFd to -1.
 SandboxBroker::SandboxBroker(UniquePtr<const Policy> aPolicy, int aChildPid,
                              int& aClientFd)
   : mChildPid(aChildPid), mPolicy(Move(aPolicy))
 {
@@ -278,16 +280,35 @@ SandboxBroker::Policy::Lookup(const nsAC
   }
 
   // Strip away the RECURSIVE flag as it doesn't
   // necessarily apply to aPath.
   return allPerms & ~RECURSIVE;
 }
 
 static bool
+AllowOperation(int aReqFlags, int aPerms)
+{
+  int needed = 0;
+  if (aReqFlags & R_OK) {
+    needed |= SandboxBroker::MAY_READ;
+  }
+  if (aReqFlags & W_OK) {
+    needed |= SandboxBroker::MAY_WRITE;
+  }
+  // We don't really allow executing anything,
+  // so in true unix tradition we hijack this
+  // for directories.
+  if (aReqFlags & X_OK) {
+    needed |= SandboxBroker::MAY_CREATE;
+  }
+  return (aPerms & needed) == needed;
+}
+
+static bool
 AllowAccess(int aReqFlags, int aPerms)
 {
   if (aReqFlags & ~(R_OK|W_OK|F_OK)) {
     return false;
   }
   int needed = 0;
   if (aReqFlags & R_OK) {
     needed |= SandboxBroker::MAY_READ;
@@ -338,22 +359,51 @@ AllowOpen(int aReqFlags, int aPerms)
   }
   if (aReqFlags & O_CREAT) {
     needed |= SandboxBroker::MAY_CREATE;
   }
   return (aPerms & needed) == needed;
 }
 
 static int
-DoStat(const char* aPath, struct stat* aStat, int aFlags)
+DoStat(const char* aPath, void* aBuff, int aFlags)
+{
+ if (aFlags & O_NOFOLLOW) {
+    return lstatsyscall(aPath, (statstruct*)aBuff);
+  }
+  return statsyscall(aPath, (statstruct*)aBuff);
+}
+
+static int
+DoLink(const char* aPath, const char* aPath2,
+       SandboxBrokerCommon::Operation aOper)
 {
-  if (aFlags & O_NOFOLLOW) {
-    return lstat(aPath, aStat);
+  if (aOper == SandboxBrokerCommon::Operation::SANDBOX_FILE_LINK) {
+    return link(aPath, aPath2);
+  } else if (aOper == SandboxBrokerCommon::Operation::SANDBOX_FILE_SYMLINK) {
+    return symlink(aPath, aPath2);
   }
-  return stat(aPath, aStat);
+  MOZ_CRASH("SandboxBroker: Unknown link operation");
+}
+
+size_t
+SandboxBroker::ConvertToRealPath(char* aPath, size_t aBufSize, size_t aPathLen)
+{
+  if (strstr(aPath, "..") != NULL) {
+    char* result = realpath(aPath, NULL);
+    if (result != NULL) {
+      strncpy(aPath, result, aBufSize);
+      aPath[aBufSize - 1] = '\0';
+      free(result);
+      // Size changed, but guaranteed to be 0 terminated
+      aPathLen = strlen(aPath);
+    }
+    // ValidatePath will handle failure to translate
+  }
+  return aPathLen;
 }
 
 void
 SandboxBroker::ThreadMain(void)
 {
   char threadName[16];
   SprintfLiteral(threadName, "FS Broker %d", mChildPid);
   PlatformThread::SetName(threadName);
@@ -374,27 +424,37 @@ SandboxBroker::ThreadMain(void)
   if (syscall(nr_setregid, getgid(), AID_APP + mChildPid) != 0 ||
       syscall(nr_setreuid, getuid(), AID_APP + mChildPid) != 0) {
     MOZ_CRASH("SandboxBroker: failed to drop privileges");
   }
 #endif
 
   while (true) {
     struct iovec ios[2];
+    // We will receive the path strings in 1 buffer and split them back up.
+    char recvBuf[2 * (kMaxPathLen + 1)];
     char pathBuf[kMaxPathLen + 1];
+    char pathBuf2[kMaxPathLen + 1];
     size_t pathLen;
-    struct stat statBuf;
+    size_t pathLen2;
+    char respBuf[kMaxPathLen + 1]; // Also serves as struct stat
     Request req;
     Response resp;
     int respfd;
 
+    // Make sure stat responses fit in the response buffer
+    MOZ_ASSERT((kMaxPathLen + 1) > sizeof(struct stat));
+
+    // This makes our string handling below a bit less error prone.
+    memset(recvBuf, 0, sizeof(recvBuf));
+
     ios[0].iov_base = &req;
     ios[0].iov_len = sizeof(req);
-    ios[1].iov_base = pathBuf;
-    ios[1].iov_len = kMaxPathLen;
+    ios[1].iov_base = recvBuf;
+    ios[1].iov_len = sizeof(recvBuf);
 
     const ssize_t recvd = RecvWithFd(mFileDesc, ios, 2, &respfd);
     if (recvd == 0) {
       if (SandboxInfo::Get().Test(SandboxInfo::kVerbose)) {
         SANDBOX_LOG_ERROR("EOF from pid %d", mChildPid);
       }
       break;
     }
@@ -416,117 +476,256 @@ SandboxBroker::ThreadMain(void)
     if (respfd == -1) {
       SANDBOX_LOG_ERROR("no response fd from pid %d", mChildPid);
       shutdown(mFileDesc, SHUT_RD);
       break;
     }
 
     // Initialize the response with the default failure.
     memset(&resp, 0, sizeof(resp));
-    memset(&statBuf, 0, sizeof(statBuf));
-    resp.mError = EACCES;
+    memset(&respBuf, 0, sizeof(respBuf));
+    resp.mError = -EACCES;
     ios[0].iov_base = &resp;
     ios[0].iov_len = sizeof(resp);
     ios[1].iov_base = nullptr;
     ios[1].iov_len = 0;
     int openedFd = -1;
 
-    // Look up the pathname.
-    pathLen = recvd - sizeof(req);
-    // It shouldn't be possible for recvmsg to violate this assertion,
-    // but one more predictable branch shouldn't have much perf impact:
-    MOZ_RELEASE_ASSERT(pathLen <= kMaxPathLen);
-    pathBuf[pathLen] = '\0';
-    int perms = 0;
-    if (!memchr(pathBuf, '\0', pathLen)) {
+    // Clear permissions
+    int perms;
+
+    // Find end of first string, make sure the buffer is still
+    // 0 terminated.
+    size_t recvBufLen = static_cast<size_t>(recvd) - sizeof(req);
+    if (recvBufLen > 0 && recvBuf[recvBufLen - 1] != 0) {
+      SANDBOX_LOG_ERROR("corrupted path buffer from pid %d", mChildPid);
+      shutdown(mFileDesc, SHUT_RD);
+      break;
+    }
+
+    // First path should fit in maximum path length buffer.
+    size_t first_len = strlen(recvBuf);
+    if (first_len <= kMaxPathLen) {
+      strcpy(pathBuf, recvBuf);
+      // Skip right over the terminating 0, and try to copy in the
+      // second path, if any. If there's no path, this will hit a
+      // 0 immediately (we nulled the buffer before receiving).
+      // We do not assume the second path is 0-terminated, this is
+      // enforced below.
+      strncpy(pathBuf2, recvBuf + first_len + 1, kMaxPathLen + 1);
+
+      // First string is guaranteed to be 0-terminated.
+      pathLen = first_len;
+
+      // Look up the first pathname but first translate relative paths.
+      pathLen = ConvertToRealPath(pathBuf, sizeof(pathBuf), pathLen);
       perms = mPolicy->Lookup(nsDependentCString(pathBuf, pathLen));
+
+      // Same for the second path.
+      pathLen2 = strnlen(pathBuf2, kMaxPathLen);
+      if (pathLen2 > 0) {
+        // Force 0 termination.
+        pathBuf[pathLen2] = '\0';
+        pathLen2 = ConvertToRealPath(pathBuf2, sizeof(pathBuf2), pathLen2);
+        int perms2 = mPolicy->Lookup(nsDependentCString(pathBuf2, pathLen2));
+
+        // Take the intersection of the permissions for both paths.
+        perms &= perms2;
+      }
+    } else {
+      // Failed to receive intelligible paths.
+      perms = 0;
     }
 
     // And now perform the operation if allowed.
     if (perms & CRASH_INSTEAD) {
       // This is somewhat nonmodular, but it works.
-      resp.mError = ENOSYS;
+      resp.mError = -ENOSYS;
     } else if (permissive || perms & MAY_ACCESS) {
       // If the operation was only allowed because of permissive mode, log it.
       if (permissive && !(perms & MAY_ACCESS)) {
-        AuditDenial(req.mOp, req.mFlags, pathBuf);
+        AuditPermissive(req.mOp, req.mFlags, perms, pathBuf);
       }
 
       switch(req.mOp) {
       case SANDBOX_FILE_OPEN:
         if (permissive || AllowOpen(req.mFlags, perms)) {
           // Permissions for O_CREAT hardwired to 0600; if that's
           // ever a problem we can change the protocol (but really we
           // should be trying to remove uses of MAY_CREATE, not add
           // new ones).
           openedFd = open(pathBuf, req.mFlags | kRequiredOpenFlags, 0600);
           if (openedFd >= 0) {
             resp.mError = 0;
           } else {
-            resp.mError = errno;
+            resp.mError = -errno;
           }
+        } else {
+          AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
         }
         break;
 
       case SANDBOX_FILE_ACCESS:
         if (permissive || AllowAccess(req.mFlags, perms)) {
           // This can't use access() itself because that uses the ruid
           // and not the euid.  In theory faccessat() with AT_EACCESS
           // would work, but Linux doesn't actually implement the
           // flags != 0 case; glibc has a hack which doesn't even work
           // in this case so it'll ignore the flag, and Bionic just
           // passes through the syscall and always ignores the flags.
           //
           // Instead, because we've already checked the requested
           // r/w/x bits against the policy, just return success if the
           // file exists and hope that's close enough.
-          if (stat(pathBuf, &statBuf) == 0) {
+          if (stat(pathBuf, (struct stat*)&respBuf) == 0) {
             resp.mError = 0;
           } else {
-            resp.mError = errno;
+            resp.mError = -errno;
           }
+        } else {
+          AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
         }
         break;
 
       case SANDBOX_FILE_STAT:
-        if (DoStat(pathBuf, &statBuf, req.mFlags) == 0) {
+        if (DoStat(pathBuf, (struct stat*)&respBuf, req.mFlags) == 0) {
           resp.mError = 0;
-          ios[1].iov_base = &statBuf;
-          ios[1].iov_len = sizeof(statBuf);
+          ios[1].iov_base = &respBuf;
+          ios[1].iov_len = req.mBufSize;
+        } else {
+          resp.mError = -errno;
+        }
+        break;
+
+      case SANDBOX_FILE_CHMOD:
+        if (permissive || AllowOperation(W_OK, perms)) {
+          if (chmod(pathBuf, req.mFlags) == 0) {
+            resp.mError = 0;
+          } else {
+            resp.mError = -errno;
+          }
+        } else {
+          AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+        }
+        break;
+
+      case SANDBOX_FILE_LINK:
+      case SANDBOX_FILE_SYMLINK:
+        if (permissive || AllowOperation(W_OK, perms)) {
+          if (DoLink(pathBuf, pathBuf2, req.mOp) == 0) {
+            resp.mError = 0;
+          } else {
+            resp.mError = -errno;
+          }
+        } else {
+          AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+        }
+        break;
+
+      case SANDBOX_FILE_RENAME:
+        if (permissive || AllowOperation(W_OK, perms)) {
+          if (rename(pathBuf, pathBuf2) == 0) {
+            resp.mError = 0;
+          } else {
+            resp.mError = -errno;
+          }
         } else {
-          resp.mError = errno;
+          AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+        }
+        break;
+
+      case SANDBOX_FILE_MKDIR:
+        if (permissive || AllowOperation(W_OK | X_OK, perms)) {
+          if (mkdir(pathBuf, req.mFlags) == 0) {
+            resp.mError = 0;
+          } else {
+            resp.mError = -errno;
+          }
+        } else {
+          AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+        }
+        break;
+
+      case SANDBOX_FILE_UNLINK:
+        if (permissive || AllowOperation(W_OK, perms)) {
+          if (unlink(pathBuf) == 0) {
+            resp.mError = 0;
+          } else {
+            resp.mError = -errno;
+          }
+        } else {
+          AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+        }
+        break;
+
+      case SANDBOX_FILE_RMDIR:
+        if (permissive || AllowOperation(W_OK | X_OK, perms)) {
+          if (rmdir(pathBuf) == 0) {
+            resp.mError = 0;
+          } else {
+            resp.mError = -errno;
+          }
+        } else {
+          AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
+        }
+        break;
+
+      case SANDBOX_FILE_READLINK:
+        if (permissive || AllowOperation(R_OK, perms)) {
+          ssize_t respSize = readlink(pathBuf, (char*)&respBuf, sizeof(respBuf));
+          if (respSize >= 0) {
+            resp.mError = respSize;
+            ios[1].iov_base = &respBuf;
+            ios[1].iov_len = respSize;
+          } else {
+            resp.mError = -errno;
+          }
+        } else {
+          AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
         }
         break;
       }
     } else {
       MOZ_ASSERT(perms == 0);
+      AuditDenial(req.mOp, req.mFlags, perms, pathBuf);
     }
 
     const size_t numIO = ios[1].iov_len > 0 ? 2 : 1;
     DebugOnly<const ssize_t> sent = SendWithFd(respfd, ios, numIO, openedFd);
     close(respfd);
     MOZ_ASSERT(sent < 0 ||
                static_cast<size_t>(sent) == ios[0].iov_len + ios[1].iov_len);
 
     if (openedFd >= 0) {
       close(openedFd);
     }
   }
 }
 
 void
-SandboxBroker::AuditDenial(int aOp, int aFlags, const char* aPath)
+SandboxBroker::AuditPermissive(int aOp, int aFlags, int aPerms, const char* aPath)
 {
   MOZ_RELEASE_ASSERT(SandboxInfo::Get().Test(SandboxInfo::kPermissive));
 
   struct stat statBuf;
 
   if (lstat(aPath, &statBuf) == 0) {
     // Path exists, set errno to 0 to indicate "success".
     errno = 0;
   }
 
-  SANDBOX_LOG_ERROR("SandboxBroker: denied op=%d rflags=%o path=%s for pid=%d" \
-                    " permissive=1 error=\"%s\"", aOp, aFlags, aPath, mChildPid,
-                    strerror(errno));
+  SANDBOX_LOG_ERROR("SandboxBroker: would have denied op=%d rflags=%o perms=%d path=%s for pid=%d" \
+                    " permissive=1 error=\"%s\"", aOp, aFlags, aPerms,
+                    aPath, mChildPid, strerror(errno));
 }
 
+void
+SandboxBroker::AuditDenial(int aOp, int aFlags, int aPerms, const char* aPath)
+{
+#ifdef DEBUG
+  SANDBOX_LOG_ERROR("SandboxBroker: denied op=%d rflags=%o perms=%d path=%s for pid=%d" \
+                    " error=\"%s\"", aOp, aFlags, aPerms, aPath, mChildPid,
+                    strerror(errno));
+#endif
+}
+
+
 } // namespace mozilla
--- a/security/sandbox/linux/broker/SandboxBroker.h
+++ b/security/sandbox/linux/broker/SandboxBroker.h
@@ -112,17 +112,20 @@ class SandboxBroker final
   PlatformThreadHandle mThread;
   int mFileDesc;
   const int mChildPid;
   const UniquePtr<const Policy> mPolicy;
 
   SandboxBroker(UniquePtr<const Policy> aPolicy, int aChildPid,
                 int& aClientFd);
   void ThreadMain(void) override;
-  void AuditDenial(int aOp, int aFlags, const char* aPath);
+  void AuditPermissive(int aOp, int aFlags, int aPerms, const char* aPath);
+  void AuditDenial(int aOp, int aFlags, int aPerms, const char* aPath);
+  // Remap relative paths to absolute paths.
+  size_t ConvertToRealPath(char* aPath, size_t aBufSize, size_t aPathLen);
 
   // Holding a UniquePtr should disallow copying, but to make that explicit:
   SandboxBroker(const SandboxBroker&) = delete;
   void operator=(const SandboxBroker&) = delete;
 };
 
 } // namespace mozilla
 
--- a/security/sandbox/linux/broker/SandboxBrokerCommon.h
+++ b/security/sandbox/linux/broker/SandboxBrokerCommon.h
@@ -1,16 +1,16 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
-#ifndef mozilla_SandboxBrokerTypes_h
-#define mozilla_SandboxBrokerTypes_h
+#ifndef mozilla_SandboxBrokerCommon_h
+#define mozilla_SandboxBrokerCommon_h
 
 #include <sys/types.h>
 
 struct iovec;
 
 // This file defines the protocol between the filesystem broker,
 // described in SandboxBroker.h, and its client, described in
 // ../SandboxBrokerClient.h; and it defines some utility functions
@@ -24,28 +24,39 @@ struct iovec;
 namespace mozilla {
 
 class SandboxBrokerCommon {
 public:
   enum Operation {
     SANDBOX_FILE_OPEN,
     SANDBOX_FILE_ACCESS,
     SANDBOX_FILE_STAT,
+    SANDBOX_FILE_CHMOD,
+    SANDBOX_FILE_LINK,
+    SANDBOX_FILE_SYMLINK,
+    SANDBOX_FILE_MKDIR,
+    SANDBOX_FILE_RENAME,
+    SANDBOX_FILE_RMDIR,
+    SANDBOX_FILE_UNLINK,
+    SANDBOX_FILE_READLINK,
   };
 
   struct Request {
     Operation mOp;
     // For open, flags; for access, "mode"; for stat, O_NOFOLLOW for lstat.
     int mFlags;
+    // Size of return value buffer, if any
+    size_t mBufSize;
     // The rest of the packet is the pathname.
     // SCM_RIGHTS for response socket attached.
   };
 
   struct Response {
-    int mError; // errno, or 0 for no error
+    // Syscall result, -errno if failure, or 0 for no error
+    int mError;
     // Followed by struct stat for stat/lstat.
     // SCM_RIGHTS attached for successful open.
   };
 
   // This doesn't need to be the system's maximum path length, just
   // the largest path that would be allowed by any policy.  (It's used
   // to size a stack-allocated buffer.)
   static const size_t kMaxPathLen = 4096;
@@ -53,9 +64,9 @@ public:
   static ssize_t RecvWithFd(int aFd, const iovec* aIO, size_t aNumIO,
                             int* aPassedFdPtr);
   static ssize_t SendWithFd(int aFd, const iovec* aIO, size_t aNumIO,
                             int aPassedFd);
 };
 
 } // namespace mozilla
 
-#endif // mozilla_SandboxBrokerTypes_h
+#endif // mozilla_SandboxBrokerCommon_h
--- a/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp
+++ b/security/sandbox/linux/broker/SandboxBrokerPolicyFactory.cpp
@@ -8,16 +8,17 @@
 #include "SandboxInfo.h"
 
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/Preferences.h"
 #include "nsPrintfCString.h"
 #include "nsString.h"
 #include "nsThreadUtils.h"
 #include "nsXULAppAPI.h"
+#include "SpecialSystemDirectory.h"
 
 #ifdef ANDROID
 #include "cutils/properties.h"
 #endif
 
 namespace mozilla {
 
 /* static */ bool
@@ -35,22 +36,25 @@ SandboxBrokerPolicyFactory::IsSystemSupp
   // automatically regardless of the device.
   if (SandboxInfo::Get().Test(SandboxInfo::kPermissive)) {
     return true;
   }
 #endif
   return false;
 }
 
-#if defined(MOZ_CONTENT_SANDBOX) && defined(MOZ_WIDGET_GONK)
+#if defined(MOZ_CONTENT_SANDBOX)
 namespace {
 static const int rdonly = SandboxBroker::MAY_READ;
 static const int wronly = SandboxBroker::MAY_WRITE;
 static const int rdwr = rdonly | wronly;
+static const int rdwrcr = rdwr | SandboxBroker::MAY_CREATE;
+#if defined(MOZ_WIDGET_GONK)
 static const int wrlog = wronly | SandboxBroker::MAY_CREATE;
+#endif
 }
 #endif
 
 SandboxBrokerPolicyFactory::SandboxBrokerPolicyFactory()
 {
   // Policy entries that are the same in every process go here, and
   // are cached over the lifetime of the factory.
 #if defined(MOZ_CONTENT_SANDBOX) && defined(MOZ_WIDGET_GONK)
@@ -106,50 +110,78 @@ SandboxBrokerPolicyFactory::SandboxBroke
   // Bug 1198401: timezones.  Yes, we need both of these; see bug.
   policy->AddTree(rdonly, "/system/usr/share/zoneinfo");
   policy->AddTree(rdonly, "/system//usr/share/zoneinfo");
 
   policy->AddPath(rdonly, "/data/local/tmp/profiler.options",
                   SandboxBroker::Policy::AddAlways); // bug 1029337
 
   mCommonContentPolicy.reset(policy);
+#elif defined(MOZ_CONTENT_SANDBOX)
+  SandboxBroker::Policy* policy = new SandboxBroker::Policy;
+  policy->AddDir(rdonly, "/");
+  policy->AddDir(rdwrcr, "/dev/shm");
+  // Add write permissions on the temporary directory. This can come
+  // from various environment variables (TMPDIR,TMP,TEMP,...) so
+  // make sure to use the full logic.
+  nsCOMPtr<nsIFile> tmpDir;
+  nsresult rv = GetSpecialSystemDirectory(OS_TemporaryDirectory,
+                                          getter_AddRefs(tmpDir));
+  if (NS_SUCCEEDED(rv)) {
+    nsAutoCString tmpPath;
+    rv = tmpDir->GetNativePath(tmpPath);
+    if (NS_SUCCEEDED(rv)) {
+      policy->AddDir(rdwrcr, tmpPath.get());
+    }
+  }
+  // If the above fails at any point, fall back to a very good guess.
+  if (NS_FAILED(rv)) {
+    policy->AddDir(rdwrcr, "/tmp");
+  }
+  mCommonContentPolicy.reset(policy);
 #endif
 }
 
 #ifdef MOZ_CONTENT_SANDBOX
 UniquePtr<SandboxBroker::Policy>
 SandboxBrokerPolicyFactory::GetContentPolicy(int aPid)
 {
-  // Allow overriding "unsupported"ness with a pref, for testing.
-  if (!IsSystemSupported() &&
-      Preferences::GetInt("security.sandbox.content.level") <= 1) {
+  // Policy entries that vary per-process (currently the only reason
+  // that can happen is because they contain the pid) are added here.
+
+  MOZ_ASSERT(NS_IsMainThread());
+  // File broker usage is controlled through a pref.
+  if (Preferences::GetInt("security.sandbox.content.level") <= 1) {
     return nullptr;
   }
 
-  // Policy entries that vary per-process (currently the only reason
-  // that can happen is because they contain the pid) are added here.
+  MOZ_ASSERT(mCommonContentPolicy);
 #if defined(MOZ_WIDGET_GONK)
-  MOZ_ASSERT(NS_IsMainThread());
-  MOZ_ASSERT(mCommonContentPolicy);
+  // Allow overriding "unsupported"ness with a pref, for testing.
+  if (!IsSystemSupported()) {
+    return nullptr;
+  }
   UniquePtr<SandboxBroker::Policy>
     policy(new SandboxBroker::Policy(*mCommonContentPolicy));
 
   // Bug 1029337: where the profiler writes the data.
   nsPrintfCString profilerLogPath("/data/local/tmp/profile_%d_%d.txt",
                                   GeckoProcessType_Content, aPid);
   policy->AddPath(wrlog, profilerLogPath.get());
 
   // Bug 1198550: the profiler's replacement for dl_iterate_phdr
   policy->AddPath(rdonly, nsPrintfCString("/proc/%d/maps", aPid).get());
 
   // Bug 1198552: memory reporting.
   policy->AddPath(rdonly, nsPrintfCString("/proc/%d/statm", aPid).get());
   policy->AddPath(rdonly, nsPrintfCString("/proc/%d/smaps", aPid).get());
 
   return policy;
-#else // MOZ_WIDGET_GONK
-  // Not implemented for desktop yet.
-  return nullptr;
+#else
+  UniquePtr<SandboxBroker::Policy>
+    policy(new SandboxBroker::Policy(*mCommonContentPolicy));
+  // Return the common policy.
+  return policy;
 #endif
 }
 
 #endif // MOZ_CONTENT_SANDBOX
 } // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/security/sandbox/linux/broker/SandboxBrokerUtils.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_SandboxBrokerUtils_h
+#define mozilla_SandboxBrokerUtils_h
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include "sandbox/linux/system_headers/linux_syscalls.h"
+
+// On 32-bit Linux, stat calls are translated by libc into stat64
+// calls. We'll intercept those and handle them in the stat functions
+// but must be sure to use the right structure layout.
+
+#if defined(__NR_stat64)
+typedef struct stat64 statstruct;
+#define statsyscall stat64
+#define lstatsyscall lstat64
+#elif defined(__NR_stat)
+typedef struct stat statstruct;
+#define statsyscall stat
+#define lstatsyscall lstat
+#else
+#error Missing stat syscall include.
+#endif
+
+#endif // mozilla_SandboxBrokerUtils_h
--- a/security/sandbox/linux/gtest/TestBroker.cpp
+++ b/security/sandbox/linux/gtest/TestBroker.cpp
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "gtest/gtest.h"
 
 #include "broker/SandboxBroker.h"
+#include "broker/SandboxBrokerUtils.h"
 #include "SandboxBrokerClient.h"
 
 #include <errno.h>
 #include <fcntl.h>
 #include <pthread.h>
 #include <stdlib.h>
 #include <sched.h>
 #include <semaphore.h>
@@ -26,17 +27,17 @@
 #include "mozilla/UniquePtr.h"
 #include "mozilla/ipc/FileDescriptor.h"
 
 namespace mozilla {
 
 static const int MAY_ACCESS = SandboxBroker::MAY_ACCESS;
 static const int MAY_READ = SandboxBroker::MAY_READ;
 static const int MAY_WRITE = SandboxBroker::MAY_WRITE;
-//static const int MAY_CREATE = SandboxBroker::MAY_CREATE;
+static const int MAY_CREATE = SandboxBroker::MAY_CREATE;
 static const auto AddAlways = SandboxBroker::Policy::AddAlways;
 
 class SandboxBrokerTest : public ::testing::Test
 {
   UniquePtr<SandboxBroker> mServer;
   UniquePtr<SandboxBrokerClient> mClient;
 
   UniquePtr<const SandboxBroker::Policy> GetPolicy() const;
@@ -49,22 +50,46 @@ class SandboxBrokerTest : public ::testi
 
 protected:
   int Open(const char* aPath, int aFlags) {
     return mClient->Open(aPath, aFlags);
   }
   int Access(const char* aPath, int aMode) {
     return mClient->Access(aPath, aMode);
   }
-  int Stat(const char* aPath, struct stat* aStat) {
+  int Stat(const char* aPath, statstruct* aStat) {
     return mClient->Stat(aPath, aStat);
   }
-  int LStat(const char* aPath, struct stat* aStat) {
+  int LStat(const char* aPath, statstruct* aStat) {
     return mClient->LStat(aPath, aStat);
   }
+  int Chmod(const char* aPath, int aMode) {
+    return mClient->Chmod(aPath, aMode);
+  }
+  int Link(const char* aPath, const char* bPath) {
+    return mClient->Link(aPath, bPath);
+  }
+  int Mkdir(const char* aPath, int aMode) {
+    return mClient->Mkdir(aPath, aMode);
+  }
+  int Symlink(const char* aPath, const char* bPath) {
+    return mClient->Symlink(aPath, bPath);
+  }
+  int Rename(const char* aPath, const char* bPath) {
+    return mClient->Rename(aPath, bPath);
+  }
+  int Rmdir(const char* aPath) {
+    return mClient->Rmdir(aPath);
+  }
+  int Unlink(const char* aPath) {
+    return mClient->Unlink(aPath);
+  }
+  ssize_t Readlink(const char* aPath, char* aBuff, size_t aSize) {
+    return mClient->Readlink(aPath, aBuff, aSize);
+  }
 
   virtual void SetUp() {
     ipc::FileDescriptor fd;
 
     mServer = SandboxBroker::Create(GetPolicy(), getpid(), fd);
     ASSERT_NE(mServer, nullptr);
     ASSERT_TRUE(fd.IsValid());
     auto rawFD = fd.ClonePlatformHandle();
@@ -100,16 +125,19 @@ UniquePtr<const SandboxBroker::Policy>
 SandboxBrokerTest::GetPolicy() const
 {
   UniquePtr<SandboxBroker::Policy> policy(new SandboxBroker::Policy());
 
   policy->AddPath(MAY_READ | MAY_WRITE, "/dev/null", AddAlways);
   policy->AddPath(MAY_READ, "/dev/zero", AddAlways);
   policy->AddPath(MAY_READ, "/var/empty/qwertyuiop", AddAlways);
   policy->AddPath(MAY_ACCESS, "/proc/self", AddAlways); // Warning: Linux-specific.
+  policy->AddPath(MAY_READ | MAY_WRITE, "/tmp", AddAlways);
+  policy->AddPath(MAY_READ | MAY_WRITE | MAY_CREATE, "/tmp/blublu", AddAlways);
+  policy->AddPath(MAY_READ | MAY_WRITE | MAY_CREATE, "/tmp/blublublu", AddAlways);
 
   return Move(policy);
 }
 
 TEST_F(SandboxBrokerTest, OpenForRead)
 {
   int fd;
 
@@ -177,44 +205,201 @@ TEST_F(SandboxBrokerTest, Access)
   EXPECT_EQ(0, Access("/proc/self", F_OK));
   EXPECT_EQ(-EACCES, Access("/proc/self", R_OK));
 
   EXPECT_EQ(-EACCES, Access("/proc/self/stat", F_OK));
 }
 
 TEST_F(SandboxBrokerTest, Stat)
 {
-  struct stat brokeredStat, realStat;
-  ASSERT_EQ(0, stat("/dev/null", &realStat)) << "Shouldn't ever fail!";
+  statstruct realStat, brokeredStat;
+  ASSERT_EQ(0, statsyscall("/dev/null", &realStat)) << "Shouldn't ever fail!";
   EXPECT_EQ(0, Stat("/dev/null", &brokeredStat));
   EXPECT_EQ(realStat.st_ino, brokeredStat.st_ino);
   EXPECT_EQ(realStat.st_rdev, brokeredStat.st_rdev);
 
   EXPECT_EQ(-ENOENT, Stat("/var/empty/qwertyuiop", &brokeredStat));
   EXPECT_EQ(-EACCES, Stat("/dev", &brokeredStat));
 
   EXPECT_EQ(0, Stat("/proc/self", &brokeredStat));
   EXPECT_TRUE(S_ISDIR(brokeredStat.st_mode));
 }
 
 TEST_F(SandboxBrokerTest, LStat)
 {
-  struct stat brokeredStat, realStat;
-  ASSERT_EQ(0, lstat("/dev/null", &realStat));
+  statstruct realStat, brokeredStat;
+  ASSERT_EQ(0, lstatsyscall("/dev/null", &realStat));
   EXPECT_EQ(0, LStat("/dev/null", &brokeredStat));
   EXPECT_EQ(realStat.st_ino, brokeredStat.st_ino);
   EXPECT_EQ(realStat.st_rdev, brokeredStat.st_rdev);
 
   EXPECT_EQ(-ENOENT, LStat("/var/empty/qwertyuiop", &brokeredStat));
   EXPECT_EQ(-EACCES, LStat("/dev", &brokeredStat));
 
   EXPECT_EQ(0, LStat("/proc/self", &brokeredStat));
   EXPECT_TRUE(S_ISLNK(brokeredStat.st_mode));
 }
 
+static void PrePostTestCleanup(void)
+{
+  unlink("/tmp/blublu");
+  rmdir("/tmp/blublu");
+  unlink("/tmp/nope");
+  rmdir("/tmp/nope");
+  unlink("/tmp/blublublu");
+  rmdir("/tmp/blublublu");
+}
+
+TEST_F(SandboxBrokerTest, Chmod)
+{
+  PrePostTestCleanup();
+
+  int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT);
+  ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed.";
+  close(fd);
+  // Set read only. SandboxBroker enforces 0600 mode flags.
+  ASSERT_EQ(0, Chmod("/tmp/blublu", S_IRUSR));
+  // SandboxBroker doesn't use real access(), it just checks against
+  // the policy. So it can't see the change in permisions here.
+  // This won't work:
+  // EXPECT_EQ(-EACCES, Access("/tmp/blublu", W_OK));
+  statstruct realStat;
+  EXPECT_EQ(0, statsyscall("/tmp/blublu", &realStat));
+  EXPECT_EQ((mode_t)S_IRUSR, realStat.st_mode & 0777);
+
+  ASSERT_EQ(0, Chmod("/tmp/blublu", S_IRUSR | S_IWUSR));
+  EXPECT_EQ(0, statsyscall("/tmp/blublu", &realStat));
+  EXPECT_EQ((mode_t)(S_IRUSR | S_IWUSR), realStat.st_mode & 0777);
+  EXPECT_EQ(0, unlink("/tmp/blublu"));
+
+  PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Link)
+{
+  PrePostTestCleanup();
+
+  int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT);
+  ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed.";
+  close(fd);
+  ASSERT_EQ(0, Link("/tmp/blublu", "/tmp/blublublu"));
+  EXPECT_EQ(0, Access("/tmp/blublublu", F_OK));
+  // Not whitelisted target path
+  EXPECT_EQ(-EACCES, Link("/tmp/blublu", "/tmp/nope"));
+  EXPECT_EQ(0, unlink("/tmp/blublublu"));
+  EXPECT_EQ(0, unlink("/tmp/blublu"));
+
+  PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Symlink)
+{
+  PrePostTestCleanup();
+
+  int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT);
+  ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed.";
+  close(fd);
+  ASSERT_EQ(0, Symlink("/tmp/blublu", "/tmp/blublublu"));
+  EXPECT_EQ(0, Access("/tmp/blublublu", F_OK));
+  statstruct aStat;
+  ASSERT_EQ(0, lstatsyscall("/tmp/blublublu", &aStat));
+  EXPECT_EQ((mode_t)S_IFLNK, aStat.st_mode & S_IFMT);
+  // Not whitelisted target path
+  EXPECT_EQ(-EACCES, Symlink("/tmp/blublu", "/tmp/nope"));
+  EXPECT_EQ(0, unlink("/tmp/blublublu"));
+  EXPECT_EQ(0, unlink("/tmp/blublu"));
+
+  PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Mkdir)
+{
+  PrePostTestCleanup();
+
+  ASSERT_EQ(0, mkdir("/tmp/blublu", 0600))
+    << "Creating dir /tmp/blublu failed.";
+  EXPECT_EQ(0, Access("/tmp/blublu", F_OK));
+  // Not whitelisted target path
+  EXPECT_EQ(-EACCES, Mkdir("/tmp/nope", 0600))
+    << "Creating dir without MAY_CREATE succeed.";
+  EXPECT_EQ(0, rmdir("/tmp/blublu"));
+
+  PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Rename)
+{
+  PrePostTestCleanup();
+
+  ASSERT_EQ(0, mkdir("/tmp/blublu", 0600))
+    << "Creating dir /tmp/blublu failed.";
+  EXPECT_EQ(0, Access("/tmp/blublu", F_OK));
+  ASSERT_EQ(0, Rename("/tmp/blublu", "/tmp/blublublu"));
+  EXPECT_EQ(0, Access("/tmp/blublublu", F_OK));
+  EXPECT_EQ(-ENOENT , Access("/tmp/blublu", F_OK));
+  // Not whitelisted target path
+  EXPECT_EQ(-EACCES, Rename("/tmp/blublublu", "/tmp/nope"))
+    << "Renaming dir without write access succeed.";
+  EXPECT_EQ(0, rmdir("/tmp/blublublu"));
+
+  PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Rmdir)
+{
+  PrePostTestCleanup();
+
+  ASSERT_EQ(0, mkdir("/tmp/blublu", 0600))
+    << "Creating dir /tmp/blublu failed.";
+  EXPECT_EQ(0, Access("/tmp/blublu", F_OK));
+  ASSERT_EQ(0, Rmdir("/tmp/blublu"));
+  EXPECT_EQ(-ENOENT, Access("/tmp/blublu", F_OK));
+  // Bypass sandbox to create a non-deletable dir
+  ASSERT_EQ(0, mkdir("/tmp/nope", 0600));
+  EXPECT_EQ(-EACCES, Rmdir("/tmp/nope"));
+
+  PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Unlink)
+{
+  PrePostTestCleanup();
+
+  int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT);
+  ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed.";
+  close(fd);
+  EXPECT_EQ(0, Access("/tmp/blublu", F_OK));
+  EXPECT_EQ(0, Unlink("/tmp/blublu"));
+  EXPECT_EQ(-ENOENT , Access("/tmp/blublu", F_OK));
+  // Bypass sandbox to write a non-deletable file
+  fd = open("/tmp/nope", O_WRONLY | O_CREAT, 0600);
+  ASSERT_GE(fd, 0) << "Opening /tmp/nope for writing failed.";
+  close(fd);
+  EXPECT_EQ(-EACCES, Unlink("/tmp/nope"));
+
+  PrePostTestCleanup();
+}
+
+TEST_F(SandboxBrokerTest, Readlink)
+{
+  PrePostTestCleanup();
+
+  int fd = Open("/tmp/blublu", O_WRONLY | O_CREAT);
+  ASSERT_GE(fd, 0) << "Opening /tmp/blublu for writing failed.";
+  close(fd);
+  ASSERT_EQ(0, Symlink("/tmp/blublu", "/tmp/blublublu"));
+  EXPECT_EQ(0, Access("/tmp/blublublu", F_OK));
+  char linkBuff[256];
+  EXPECT_EQ(11, Readlink("/tmp/blublublu", linkBuff, sizeof(linkBuff)));
+  linkBuff[11] = '\0';
+  EXPECT_EQ(0, strcmp(linkBuff, "/tmp/blublu"));
+
+  PrePostTestCleanup();
+}
+
 TEST_F(SandboxBrokerTest, MultiThreadOpen) {
   RunOnManyThreads<SandboxBrokerTest,
                    &SandboxBrokerTest::MultiThreadOpenWorker>();
 }
 void SandboxBrokerTest::MultiThreadOpenWorker() {
   static const int kNumLoops = 10000;
 
   for (int i = 1; i <= kNumLoops; ++i) {
@@ -232,23 +417,23 @@ void SandboxBrokerTest::MultiThreadOpenW
 }
 
 TEST_F(SandboxBrokerTest, MultiThreadStat) {
   RunOnManyThreads<SandboxBrokerTest,
                    &SandboxBrokerTest::MultiThreadStatWorker>();
 }
 void SandboxBrokerTest::MultiThreadStatWorker() {
   static const int kNumLoops = 7500;
-  struct stat nullStat, zeroStat, selfStat;
+  statstruct nullStat, zeroStat, selfStat;
   dev_t realNullDev, realZeroDev;
   ino_t realSelfInode;
 
-  ASSERT_EQ(0, stat("/dev/null", &nullStat)) << "Shouldn't ever fail!";
-  ASSERT_EQ(0, stat("/dev/zero", &zeroStat)) << "Shouldn't ever fail!";
-  ASSERT_EQ(0, lstat("/proc/self", &selfStat)) << "Shouldn't ever fail!";
+  ASSERT_EQ(0, statsyscall("/dev/null", &nullStat)) << "Shouldn't ever fail!";
+  ASSERT_EQ(0, statsyscall("/dev/zero", &zeroStat)) << "Shouldn't ever fail!";
+  ASSERT_EQ(0, lstatsyscall("/proc/self", &selfStat)) << "Shouldn't ever fail!";
   ASSERT_TRUE(S_ISLNK(selfStat.st_mode)) << "Shouldn't ever fail!";
   realNullDev = nullStat.st_rdev;
   realZeroDev = zeroStat.st_rdev;
   realSelfInode = selfStat.st_ino;
   for (int i = 1; i <= kNumLoops; ++i) {
     ASSERT_EQ(0, Stat("/dev/null", &nullStat))
       << "Loop " << i << "/" << kNumLoops;
     ASSERT_EQ(0, Stat("/dev/zero", &zeroStat))
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -606,23 +606,23 @@ Collection.prototype = {
   // index
   get sort() { return this._sort; },
   set sort(value) {
     this._sort = value;
     this._rebuildURL();
   },
 
   // Set information about the batch for this request.
-  get batch() { return _batch; },
+  get batch() { return this._batch; },
   set batch(value) {
     this._batch = value;
     this._rebuildURL();
   },
 
-  get commit() { return _commit; },
+  get commit() { return this._commit; },
   set commit(value) {
     this._commit = value && true;
     this._rebuildURL();
   },
 
   set recordHandler(onRecord) {
     // Save this because onProgress is called with this as the ChannelListener
     let coll = this;
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/head_errorhandler_common.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/engines.js");
+
+// Common code for test_errorhandler_{1,2}.js -- pulled out to make it less
+// monolithic and take less time to execute.
+const EHTestsCommon = {
+
+  service_unavailable(request, response) {
+    let body = "Service Unavailable";
+    response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
+    response.setHeader("Retry-After", "42");
+    response.bodyOutputStream.write(body, body.length);
+  },
+
+  sync_httpd_setup() {
+    let global = new ServerWBO("global", {
+      syncID: Service.syncID,
+      storageVersion: STORAGE_VERSION,
+      engines: {clients: {version: Service.clientsEngine.version,
+                          syncID: Service.clientsEngine.syncID},
+                catapult: {version: Service.engineManager.get("catapult").version,
+                           syncID: Service.engineManager.get("catapult").syncID}}
+    });
+    let clientsColl = new ServerCollection({}, true);
+
+    // Tracking info/collections.
+    let collectionsHelper = track_collections_helper();
+    let upd = collectionsHelper.with_updated_collection;
+
+    let handler_401 = httpd_handler(401, "Unauthorized");
+    return httpd_setup({
+      // Normal server behaviour.
+      "/1.1/johndoe/storage/meta/global": upd("meta", global.handler()),
+      "/1.1/johndoe/info/collections": collectionsHelper.handler,
+      "/1.1/johndoe/storage/crypto/keys":
+        upd("crypto", (new ServerWBO("keys")).handler()),
+      "/1.1/johndoe/storage/clients": upd("clients", clientsColl.handler()),
+
+      // Credentials are wrong or node reallocated.
+      "/1.1/janedoe/storage/meta/global": handler_401,
+      "/1.1/janedoe/info/collections": handler_401,
+
+      // Maintenance or overloaded (503 + Retry-After) at info/collections.
+      "/maintenance/1.1/broken.info/info/collections": EHTestsCommon.service_unavailable,
+
+      // Maintenance or overloaded (503 + Retry-After) at meta/global.
+      "/maintenance/1.1/broken.meta/storage/meta/global": EHTestsCommon.service_unavailable,
+      "/maintenance/1.1/broken.meta/info/collections": collectionsHelper.handler,
+
+      // Maintenance or overloaded (503 + Retry-After) at crypto/keys.
+      "/maintenance/1.1/broken.keys/storage/meta/global": upd("meta", global.handler()),
+      "/maintenance/1.1/broken.keys/info/collections": collectionsHelper.handler,
+      "/maintenance/1.1/broken.keys/storage/crypto/keys": EHTestsCommon.service_unavailable,
+
+      // Maintenance or overloaded (503 + Retry-After) at wiping collection.
+      "/maintenance/1.1/broken.wipe/info/collections": collectionsHelper.handler,
+      "/maintenance/1.1/broken.wipe/storage/meta/global": upd("meta", global.handler()),
+      "/maintenance/1.1/broken.wipe/storage/crypto/keys":
+        upd("crypto", (new ServerWBO("keys")).handler()),
+      "/maintenance/1.1/broken.wipe/storage": EHTestsCommon.service_unavailable,
+      "/maintenance/1.1/broken.wipe/storage/clients": upd("clients", clientsColl.handler()),
+      "/maintenance/1.1/broken.wipe/storage/catapult": EHTestsCommon.service_unavailable
+    });
+  },
+
+  CatapultEngine: (function() {
+    function CatapultEngine() {
+      SyncEngine.call(this, "Catapult", Service);
+    }
+    CatapultEngine.prototype = {
+      __proto__: SyncEngine.prototype,
+      exception: null, // tests fill this in
+      _sync: function _sync() {
+        if (this.exception) {
+          throw this.exception;
+        }
+      }
+    };
+
+    return CatapultEngine;
+  }()),
+
+
+  generateCredentialsChangedFailure() {
+    // Make sync fail due to changed credentials. We simply re-encrypt
+    // the keys with a different Sync Key, without changing the local one.
+    let newSyncKeyBundle = new SyncKeyBundle("johndoe", "23456234562345623456234562");
+    let keys = Service.collectionKeys.asWBO();
+    keys.encrypt(newSyncKeyBundle);
+    keys.upload(Service.resource(Service.cryptoKeysURL));
+  },
+
+  setUp(server) {
+    return configureIdentity({ username: "johndoe" }).then(
+      () => {
+        Service.serverURL  = server.baseURI + "/";
+        Service.clusterURL = server.baseURI + "/";
+      }
+    ).then(
+      () => EHTestsCommon.generateAndUploadKeys()
+    );
+  },
+
+  generateAndUploadKeys() {
+    generateNewKeys(Service.collectionKeys);
+    let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
+    serverKeys.encrypt(Service.identity.syncKeyBundle);
+    return serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success;
+  }
+};
deleted file mode 100644
--- a/services/sync/tests/unit/test_errorhandler.js
+++ /dev/null
@@ -1,1956 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
-   http://creativecommons.org/publicdomain/zero/1.0/ */
-
-Cu.import("resource://services-sync/engines/clients.js");
-Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/policies.js");
-Cu.import("resource://services-sync/service.js");
-Cu.import("resource://services-sync/status.js");
-Cu.import("resource://services-sync/util.js");
-Cu.import("resource://testing-common/services/sync/utils.js");
-Cu.import("resource://gre/modules/FileUtils.jsm");
-
-var fakeServer = new SyncServer();
-fakeServer.start();
-
-do_register_cleanup(function() {
-  return new Promise(resolve => {
-    fakeServer.stop(resolve);
-  });
-});
-
-var fakeServerUrl = "http://localhost:" + fakeServer.port;
-
-const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
-
-const PROLONGED_ERROR_DURATION =
-  (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') * 2) * 1000;
-
-const NON_PROLONGED_ERROR_DURATION =
-  (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') / 2) * 1000;
-
-Service.engineManager.clear();
-
-function setLastSync(lastSyncValue) {
-  Svc.Prefs.set("lastSync", (new Date(Date.now() - lastSyncValue)).toString());
-}
-
-function CatapultEngine() {
-  SyncEngine.call(this, "Catapult", Service);
-}
-CatapultEngine.prototype = {
-  __proto__: SyncEngine.prototype,
-  exception: null, // tests fill this in
-  _sync: function _sync() {
-    if (this.exception) {
-      throw this.exception;
-    }
-  }
-};
-
-var engineManager = Service.engineManager;
-engineManager.register(CatapultEngine);
-
-// This relies on Service/ErrorHandler being a singleton. Fixing this will take
-// a lot of work.
-var errorHandler = Service.errorHandler;
-
-function run_test() {
-  initTestLogging("Trace");
-
-  Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
-  Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
-  Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
-
-  ensureLegacyIdentityManager();
-
-  run_next_test();
-}
-
-function generateCredentialsChangedFailure() {
-  // Make sync fail due to changed credentials. We simply re-encrypt
-  // the keys with a different Sync Key, without changing the local one.
-  let newSyncKeyBundle = new SyncKeyBundle("johndoe", "23456234562345623456234562");
-  let keys = Service.collectionKeys.asWBO();
-  keys.encrypt(newSyncKeyBundle);
-  keys.upload(Service.resource(Service.cryptoKeysURL));
-}
-
-function service_unavailable(request, response) {
-  let body = "Service Unavailable";
-  response.setStatusLine(request.httpVersion, 503, "Service Unavailable");
-  response.setHeader("Retry-After", "42");
-  response.bodyOutputStream.write(body, body.length);
-}
-
-function sync_httpd_setup() {
-  let global = new ServerWBO("global", {
-    syncID: Service.syncID,
-    storageVersion: STORAGE_VERSION,
-    engines: {clients: {version: Service.clientsEngine.version,
-                        syncID: Service.clientsEngine.syncID},
-              catapult: {version: engineManager.get("catapult").version,
-                         syncID: engineManager.get("catapult").syncID}}
-  });
-  let clientsColl = new ServerCollection({}, true);
-
-  // Tracking info/collections.
-  let collectionsHelper = track_collections_helper();
-  let upd = collectionsHelper.with_updated_collection;
-
-  let handler_401 = httpd_handler(401, "Unauthorized");
-  return httpd_setup({
-    // Normal server behaviour.
-    "/1.1/johndoe/storage/meta/global": upd("meta", global.handler()),
-    "/1.1/johndoe/info/collections": collectionsHelper.handler,
-    "/1.1/johndoe/storage/crypto/keys":
-      upd("crypto", (new ServerWBO("keys")).handler()),
-    "/1.1/johndoe/storage/clients": upd("clients", clientsColl.handler()),
-
-    // Credentials are wrong or node reallocated.
-    "/1.1/janedoe/storage/meta/global": handler_401,
-    "/1.1/janedoe/info/collections": handler_401,
-
-    // Maintenance or overloaded (503 + Retry-After) at info/collections.
-    "/maintenance/1.1/broken.info/info/collections": service_unavailable,
-
-    // Maintenance or overloaded (503 + Retry-After) at meta/global.
-    "/maintenance/1.1/broken.meta/storage/meta/global": service_unavailable,
-    "/maintenance/1.1/broken.meta/info/collections": collectionsHelper.handler,
-
-    // Maintenance or overloaded (503 + Retry-After) at crypto/keys.
-    "/maintenance/1.1/broken.keys/storage/meta/global": upd("meta", global.handler()),
-    "/maintenance/1.1/broken.keys/info/collections": collectionsHelper.handler,
-    "/maintenance/1.1/broken.keys/storage/crypto/keys": service_unavailable,
-
-    // Maintenance or overloaded (503 + Retry-After) at wiping collection.
-    "/maintenance/1.1/broken.wipe/info/collections": collectionsHelper.handler,
-    "/maintenance/1.1/broken.wipe/storage/meta/global": upd("meta", global.handler()),
-    "/maintenance/1.1/broken.wipe/storage/crypto/keys":
-      upd("crypto", (new ServerWBO("keys")).handler()),
-    "/maintenance/1.1/broken.wipe/storage": service_unavailable,
-    "/maintenance/1.1/broken.wipe/storage/clients": upd("clients", clientsColl.handler()),
-    "/maintenance/1.1/broken.wipe/storage/catapult": service_unavailable
-  });
-}
-
-function setUp(server) {
-  return configureIdentity({username: "johndoe"}).then(
-    () => {
-      Service.serverURL  = server.baseURI + "/";
-      Service.clusterURL = server.baseURI + "/";
-    }
-  ).then(
-    () => generateAndUploadKeys()
-  );
-}
-
-function generateAndUploadKeys() {
-  generateNewKeys(Service.collectionKeys);
-  let serverKeys = Service.collectionKeys.asWBO("crypto", "keys");
-  serverKeys.encrypt(Service.identity.syncKeyBundle);
-  return serverKeys.upload(Service.resource(Service.cryptoKeysURL)).success;
-}
-
-function clean() {
-  Service.startOver();
-  Status.resetSync();
-  Status.resetBackoff();
-  errorHandler.didReportProlongedError = false;
-}
-
-add_identity_test(this, function* test_401_logout() {
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  // By calling sync, we ensure we're logged in.
-  yield sync_and_validate_telem();
-  do_check_eq(Status.sync, SYNC_SUCCEEDED);
-  do_check_true(Service.isLoggedIn);
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:service:sync:error", onSyncError);
-  function onSyncError() {
-    _("Got weave:service:sync:error in first sync.");
-    Svc.Obs.remove("weave:service:sync:error", onSyncError);
-
-    // Wait for the automatic next sync.
-    function onLoginError() {
-      _("Got weave:service:login:error in second sync.");
-      Svc.Obs.remove("weave:service:login:error", onLoginError);
-
-      let expected = isConfiguredWithLegacyIdentity() ?
-                     LOGIN_FAILED_LOGIN_REJECTED : LOGIN_FAILED_NETWORK_ERROR;
-
-      do_check_eq(Status.login, expected);
-      do_check_false(Service.isLoggedIn);
-
-      // Clean up.
-      Utils.nextTick(function () {
-        Service.startOver();
-        server.stop(deferred.resolve);
-      });
-    }
-    Svc.Obs.add("weave:service:login:error", onLoginError);
-  }
-
-  // Make sync fail due to login rejected.
-  yield configureIdentity({username: "janedoe"});
-  Service._updateCachedURLs();
-
-  _("Starting first sync.");
-  let ping = yield sync_and_validate_telem(true);
-  deepEqual(ping.failureReason, { name: "httperror", code: 401 });
-  _("First sync done.");
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_credentials_changed_logout() {
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  // By calling sync, we ensure we're logged in.
-  yield sync_and_validate_telem();
-  do_check_eq(Status.sync, SYNC_SUCCEEDED);
-  do_check_true(Service.isLoggedIn);
-
-  generateCredentialsChangedFailure();
-
-  let ping = yield sync_and_validate_telem(true);
-  equal(ping.status.sync, CREDENTIALS_CHANGED);
-  deepEqual(ping.failureReason, {
-    name: "unexpectederror",
-    error: "Error: Aborting sync, remote setup failed"
-  });
-
-  do_check_eq(Status.sync, CREDENTIALS_CHANGED);
-  do_check_false(Service.isLoggedIn);
-
-  // Clean up.
-  Service.startOver();
-  let deferred = Promise.defer();
-  server.stop(deferred.resolve);
-  yield deferred.promise;
-});
-
-add_identity_test(this, function test_no_lastSync_pref() {
-  // Test reported error.
-  Status.resetSync();
-  errorHandler.dontIgnoreErrors = true;
-  Status.sync = CREDENTIALS_CHANGED;
-  do_check_true(errorHandler.shouldReportError());
-
-  // Test unreported error.
-  Status.resetSync();
-  errorHandler.dontIgnoreErrors = true;
-  Status.login = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(errorHandler.shouldReportError());
-
-});
-
-add_identity_test(this, function test_shouldReportError() {
-  Status.login = MASTER_PASSWORD_LOCKED;
-  do_check_false(errorHandler.shouldReportError());
-
-  // Give ourselves a clusterURL so that the temporary 401 no-error situation
-  // doesn't come into play.
-  Service.serverURL  = fakeServerUrl;
-  Service.clusterURL = fakeServerUrl;
-
-  // Test dontIgnoreErrors, non-network, non-prolonged, login error reported
-  Status.resetSync();
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = true;
-  Status.login = LOGIN_FAILED_NO_PASSWORD;
-  do_check_true(errorHandler.shouldReportError());
-
-  // Test dontIgnoreErrors, non-network, non-prolonged, sync error reported
-  Status.resetSync();
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = true;
-  Status.sync = CREDENTIALS_CHANGED;
-  do_check_true(errorHandler.shouldReportError());
-
-  // Test dontIgnoreErrors, non-network, prolonged, login error reported
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = true;
-  Status.login = LOGIN_FAILED_NO_PASSWORD;
-  do_check_true(errorHandler.shouldReportError());
-
-  // Test dontIgnoreErrors, non-network, prolonged, sync error reported
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = true;
-  Status.sync = CREDENTIALS_CHANGED;
-  do_check_true(errorHandler.shouldReportError());
-
-  // Test dontIgnoreErrors, network, non-prolonged, login error reported
-  Status.resetSync();
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = true;
-  Status.login = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(errorHandler.shouldReportError());
-
-  // Test dontIgnoreErrors, network, non-prolonged, sync error reported
-  Status.resetSync();
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = true;
-  Status.sync = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(errorHandler.shouldReportError());
-
-  // Test dontIgnoreErrors, network, prolonged, login error reported
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = true;
-  Status.login = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(errorHandler.shouldReportError());
-
-  // Test dontIgnoreErrors, network, prolonged, sync error reported
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = true;
-  Status.sync = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(errorHandler.shouldReportError());
-
-  // Test non-network, prolonged, login error reported
-  do_check_false(errorHandler.didReportProlongedError);
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  Status.login = LOGIN_FAILED_NO_PASSWORD;
-  do_check_true(errorHandler.shouldReportError());
-  do_check_true(errorHandler.didReportProlongedError);
-
-  // Second time with prolonged error and without resetting
-  // didReportProlongedError, sync error should not be reported.
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  Status.login = LOGIN_FAILED_NO_PASSWORD;
-  do_check_false(errorHandler.shouldReportError());
-  do_check_true(errorHandler.didReportProlongedError);
-
-  // Test non-network, prolonged, sync error reported
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  errorHandler.didReportProlongedError = false;
-  Status.sync = CREDENTIALS_CHANGED;
-  do_check_true(errorHandler.shouldReportError());
-  do_check_true(errorHandler.didReportProlongedError);
-  errorHandler.didReportProlongedError = false;
-
-  // Test network, prolonged, login error reported
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  Status.login = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(errorHandler.shouldReportError());
-  do_check_true(errorHandler.didReportProlongedError);
-  errorHandler.didReportProlongedError = false;
-
-  // Test network, prolonged, sync error reported
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  Status.sync = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_true(errorHandler.shouldReportError());
-  do_check_true(errorHandler.didReportProlongedError);
-  errorHandler.didReportProlongedError = false;
-
-  // Test non-network, non-prolonged, login error reported
-  Status.resetSync();
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  Status.login = LOGIN_FAILED_NO_PASSWORD;
-  do_check_true(errorHandler.shouldReportError());
-  do_check_false(errorHandler.didReportProlongedError);
-
-  // Test non-network, non-prolonged, sync error reported
-  Status.resetSync();
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  Status.sync = CREDENTIALS_CHANGED;
-  do_check_true(errorHandler.shouldReportError());
-  do_check_false(errorHandler.didReportProlongedError);
-
-  // Test network, non-prolonged, login error reported
-  Status.resetSync();
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  Status.login = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_false(errorHandler.shouldReportError());
-  do_check_false(errorHandler.didReportProlongedError);
-
-  // Test network, non-prolonged, sync error reported
-  Status.resetSync();
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  Status.sync = LOGIN_FAILED_NETWORK_ERROR;
-  do_check_false(errorHandler.shouldReportError());
-  do_check_false(errorHandler.didReportProlongedError);
-
-  // Test server maintenance, sync errors are not reported
-  Status.resetSync();
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  Status.sync = SERVER_MAINTENANCE;
-  do_check_false(errorHandler.shouldReportError());
-  do_check_false(errorHandler.didReportProlongedError);
-
-  // Test server maintenance, login errors are not reported
-  Status.resetSync();
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  Status.login = SERVER_MAINTENANCE;
-  do_check_false(errorHandler.shouldReportError());
-  do_check_false(errorHandler.didReportProlongedError);
-
-  // Test prolonged, server maintenance, sync errors are reported
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  Status.sync = SERVER_MAINTENANCE;
-  do_check_true(errorHandler.shouldReportError());
-  do_check_true(errorHandler.didReportProlongedError);
-  errorHandler.didReportProlongedError = false;
-
-  // Test prolonged, server maintenance, login errors are reported
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = false;
-  Status.login = SERVER_MAINTENANCE;
-  do_check_true(errorHandler.shouldReportError());
-  do_check_true(errorHandler.didReportProlongedError);
-  errorHandler.didReportProlongedError = false;
-
-  // Test dontIgnoreErrors, server maintenance, sync errors are reported
-  Status.resetSync();
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = true;
-  Status.sync = SERVER_MAINTENANCE;
-  do_check_true(errorHandler.shouldReportError());
-  // dontIgnoreErrors means we don't set didReportProlongedError
-  do_check_false(errorHandler.didReportProlongedError);
-
-  // Test dontIgnoreErrors, server maintenance, login errors are reported
-  Status.resetSync();
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = true;
-  Status.login = SERVER_MAINTENANCE;
-  do_check_true(errorHandler.shouldReportError());
-  do_check_false(errorHandler.didReportProlongedError);
-
-  // Test dontIgnoreErrors, prolonged, server maintenance,
-  // sync errors are reported
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = true;
-  Status.sync = SERVER_MAINTENANCE;
-  do_check_true(errorHandler.shouldReportError());
-  do_check_false(errorHandler.didReportProlongedError);
-
-  // Test dontIgnoreErrors, prolonged, server maintenance,
-  // login errors are reported
-  Status.resetSync();
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.dontIgnoreErrors = true;
-  Status.login = SERVER_MAINTENANCE;
-  do_check_true(errorHandler.shouldReportError());
-  do_check_false(errorHandler.didReportProlongedError);
-});
-
-add_identity_test(this, function* test_shouldReportError_master_password() {
-  _("Test error ignored due to locked master password");
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  // Monkey patch Service.verifyLogin to imitate
-  // master password being locked.
-  Service._verifyLogin = Service.verifyLogin;
-  Service.verifyLogin = function () {
-    Status.login = MASTER_PASSWORD_LOCKED;
-    return false;
-  };
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  Service.sync();
-  do_check_false(errorHandler.shouldReportError());
-
-  // Clean up.
-  Service.verifyLogin = Service._verifyLogin;
-  clean();
-  let deferred = Promise.defer();
-  server.stop(deferred.resolve);
-  yield deferred.promise;
-});
-
-// Test that even if we don't have a cluster URL, a login failure due to
-// authentication errors is always reported.
-add_identity_test(this, function test_shouldReportLoginFailureWithNoCluster() {
-  // Ensure no clusterURL - any error not specific to login should not be reported.
-  Service.serverURL  = "";
-  Service.clusterURL = "";
-
-  // Test explicit "login rejected" state.
-  Status.resetSync();
-  // If we have a LOGIN_REJECTED state, we always report the error.
-  Status.login = LOGIN_FAILED_LOGIN_REJECTED;
-  do_check_true(errorHandler.shouldReportError());
-  // But any other status with a missing clusterURL is treated as a mid-sync
-  // 401 (ie, should be treated as a node reassignment)
-  Status.login = LOGIN_SUCCEEDED;
-  do_check_false(errorHandler.shouldReportError());
-});
-
-// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
-// an fxaccounts environment?
-add_task(function* test_login_syncAndReportErrors_non_network_error() {
-  // Test non-network errors are reported
-  // when calling syncAndReportErrors
-  let server = sync_httpd_setup();
-  yield setUp(server);
-  Service.identity.basicPassword = null;
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:login:error", onSyncError);
-    do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_sync_syncAndReportErrors_non_network_error() {
-  // Test non-network errors are reported
-  // when calling syncAndReportErrors
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  // By calling sync, we ensure we're logged in.
-  Service.sync();
-  do_check_eq(Status.sync, SYNC_SUCCEEDED);
-  do_check_true(Service.isLoggedIn);
-
-  generateCredentialsChangedFailure();
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
-    do_check_eq(Status.sync, CREDENTIALS_CHANGED);
-    // If we clean this tick, telemetry won't get the right error
-    server.stop(() => {
-      clean();
-      deferred.resolve();
-    });
-  });
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  let ping = yield wait_for_ping(() => errorHandler.syncAndReportErrors(), true);
-  equal(ping.status.sync, CREDENTIALS_CHANGED);
-  deepEqual(ping.failureReason, {
-    name: "unexpectederror",
-    error: "Error: Aborting sync, remote setup failed"
-  });
-  yield deferred.promise;
-});
-
-// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
-// an fxaccounts environment?
-add_task(function* test_login_syncAndReportErrors_prolonged_non_network_error() {
-  // Test prolonged, non-network errors are
-  // reported when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-  Service.identity.basicPassword = null;
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:login:error", onSyncError);
-    do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_sync_syncAndReportErrors_prolonged_non_network_error() {
-  // Test prolonged, non-network errors are
-  // reported when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  // By calling sync, we ensure we're logged in.
-  Service.sync();
-  do_check_eq(Status.sync, SYNC_SUCCEEDED);
-  do_check_true(Service.isLoggedIn);
-
-  generateCredentialsChangedFailure();
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
-    do_check_eq(Status.sync, CREDENTIALS_CHANGED);
-    // If we clean this tick, telemetry won't get the right error
-    server.stop(() => {
-      clean();
-      deferred.resolve();
-    });
-  });
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  let ping = yield wait_for_ping(() => errorHandler.syncAndReportErrors(), true);
-  equal(ping.status.sync, CREDENTIALS_CHANGED);
-  deepEqual(ping.failureReason, {
-    name: "unexpectederror",
-    error: "Error: Aborting sync, remote setup failed"
-  });
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_login_syncAndReportErrors_network_error() {
-  // Test network errors are reported when calling syncAndReportErrors.
-  yield configureIdentity({username: "broken.wipe"});
-  Service.serverURL  = fakeServerUrl;
-  Service.clusterURL = fakeServerUrl;
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:login:error", onSyncError);
-    do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
-
-    clean();
-    deferred.resolve();
-  });
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-
-add_test(function test_sync_syncAndReportErrors_network_error() {
-  // Test network errors are reported when calling syncAndReportErrors.
-  Services.io.offline = true;
-
-  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
-    do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
-
-    Services.io.offline = false;
-    clean();
-    run_next_test();
-  });
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-});
-
-add_identity_test(this, function* test_login_syncAndReportErrors_prolonged_network_error() {
-  // Test prolonged, network errors are reported
-  // when calling syncAndReportErrors.
-  yield configureIdentity({username: "johndoe"});
-
-  Service.serverURL  = fakeServerUrl;
-  Service.clusterURL = fakeServerUrl;
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:login:error", onSyncError);
-    do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
-
-    clean();
-    deferred.resolve();
-  });
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_test(function test_sync_syncAndReportErrors_prolonged_network_error() {
-  // Test prolonged, network errors are reported
-  // when calling syncAndReportErrors.
-  Services.io.offline = true;
-
-  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
-    do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
-
-    Services.io.offline = false;
-    clean();
-    run_next_test();
-  });
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-});
-
-add_task(function* test_login_prolonged_non_network_error() {
-  // Test prolonged, non-network errors are reported
-  let server = sync_httpd_setup();
-  yield setUp(server);
-  Service.identity.basicPassword = null;
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:login:error", onSyncError);
-    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
-    do_check_true(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_task(function* test_sync_prolonged_non_network_error() {
-  // Test prolonged, non-network errors are reported
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  // By calling sync, we ensure we're logged in.
-  Service.sync();
-  do_check_eq(Status.sync, SYNC_SUCCEEDED);
-  do_check_true(Service.isLoggedIn);
-
-  generateCredentialsChangedFailure();
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
-    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
-    do_check_true(errorHandler.didReportProlongedError);
-    server.stop(() => {
-      clean();
-      deferred.resolve();
-    });
-  });
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-
-  let ping = yield sync_and_validate_telem(true);
-  equal(ping.status.sync, PROLONGED_SYNC_FAILURE);
-  deepEqual(ping.failureReason, {
-    name: "unexpectederror",
-    error: "Error: Aborting sync, remote setup failed"
-  });
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_login_prolonged_network_error() {
-  // Test prolonged, network errors are reported
-  yield configureIdentity({username: "johndoe"});
-  Service.serverURL  = fakeServerUrl;
-  Service.clusterURL = fakeServerUrl;
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:login:error", onSyncError);
-    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
-    do_check_true(errorHandler.didReportProlongedError);
-
-    clean();
-    deferred.resolve();
-  });
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_test(function test_sync_prolonged_network_error() {
-  // Test prolonged, network errors are reported
-  Services.io.offline = true;
-
-  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
-    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
-    do_check_true(errorHandler.didReportProlongedError);
-
-    Services.io.offline = false;
-    clean();
-    run_next_test();
-  });
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  Service.sync();
-});
-
-add_task(function* test_login_non_network_error() {
-  // Test non-network errors are reported
-  let server = sync_httpd_setup();
-  yield setUp(server);
-  Service.identity.basicPassword = null;
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:login:error", onSyncError);
-    do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_task(function* test_sync_non_network_error() {
-  // Test non-network errors are reported
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  // By calling sync, we ensure we're logged in.
-  Service.sync();
-  do_check_eq(Status.sync, SYNC_SUCCEEDED);
-  do_check_true(Service.isLoggedIn);
-
-  generateCredentialsChangedFailure();
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
-    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
-    do_check_eq(Status.sync, CREDENTIALS_CHANGED);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_login_network_error() {
-  yield configureIdentity({username: "johndoe"});
-  Service.serverURL  = fakeServerUrl;
-  Service.clusterURL = fakeServerUrl;
-
-  let deferred = Promise.defer();
-  // Test network errors are not reported.
-  Svc.Obs.add("weave:ui:clear-error", function onClearError() {
-    Svc.Obs.remove("weave:ui:clear-error", onClearError);
-
-    do_check_eq(Status.login, LOGIN_FAILED_NETWORK_ERROR);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    Services.io.offline = false;
-    clean();
-    deferred.resolve()
-  });
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_test(function test_sync_network_error() {
-  // Test network errors are not reported.
-  Services.io.offline = true;
-
-  Svc.Obs.add("weave:ui:sync:finish", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:sync:finish", onUIUpdate);
-    do_check_eq(Status.sync, LOGIN_FAILED_NETWORK_ERROR);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    Services.io.offline = false;
-    clean();
-    run_next_test();
-  });
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  Service.sync();
-});
-
-add_identity_test(this, function* test_sync_server_maintenance_error() {
-  // Test server maintenance errors are not reported.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  const BACKOFF = 42;
-  let engine = engineManager.get("catapult");
-  engine.enabled = true;
-  engine.exception = {status: 503,
-                      headers: {"retry-after": BACKOFF}};
-
-  function onSyncError() {
-    do_throw("Shouldn't get here!");
-  }
-  Svc.Obs.add("weave:ui:sync:error", onSyncError);
-
-  do_check_eq(Status.service, STATUS_OK);
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:sync:finish", function onSyncFinish() {
-    Svc.Obs.remove("weave:ui:sync:finish", onSyncFinish);
-
-    do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
-    do_check_eq(Status.sync, SERVER_MAINTENANCE);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
-    server.stop(() => {
-      clean();
-      deferred.resolve();
-    })
-  });
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  let ping = yield sync_and_validate_telem(true);
-  equal(ping.status.sync, SERVER_MAINTENANCE);
-  deepEqual(ping.engines.find(e => e.failureReason).failureReason, { name: "httperror", code: 503 })
-
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_info_collections_login_server_maintenance_error() {
-  // Test info/collections server maintenance errors are not reported.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  Service.username = "broken.info";
-  yield configureIdentity({username: "broken.info"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  function onUIUpdate() {
-    do_throw("Shouldn't experience UI update!");
-  }
-  Svc.Obs.add("weave:ui:login:error", onUIUpdate);
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
-    Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
-
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_meta_global_login_server_maintenance_error() {
-  // Test meta/global server maintenance errors are not reported.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  yield configureIdentity({username: "broken.meta"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  function onUIUpdate() {
-    do_throw("Shouldn't get here!");
-  }
-  Svc.Obs.add("weave:ui:login:error", onUIUpdate);
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
-    Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
-
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_crypto_keys_login_server_maintenance_error() {
-  // Test crypto/keys server maintenance errors are not reported.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  yield configureIdentity({username: "broken.keys"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  // Force re-download of keys
-  Service.collectionKeys.clear();
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  function onUIUpdate() {
-    do_throw("Shouldn't get here!");
-  }
-  Svc.Obs.add("weave:ui:login:error", onUIUpdate);
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:clear-error", function onLoginFinish() {
-    Svc.Obs.remove("weave:ui:clear-error", onLoginFinish);
-
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_task(function* test_sync_prolonged_server_maintenance_error() {
-  // Test prolonged server maintenance errors are reported.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  const BACKOFF = 42;
-  let engine = engineManager.get("catapult");
-  engine.enabled = true;
-  engine.exception = {status: 503,
-                      headers: {"retry-after": BACKOFF}};
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
-    do_check_eq(Status.service, SYNC_FAILED);
-    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
-    do_check_true(errorHandler.didReportProlongedError);
-
-    server.stop(() => {
-      clean();
-      deferred.resolve();
-    });
-  });
-
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  let ping = yield sync_and_validate_telem(true);
-  deepEqual(ping.status.sync, PROLONGED_SYNC_FAILURE);
-  deepEqual(ping.engines.find(e => e.failureReason).failureReason,
-            { name: "httperror", code: 503 });
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_info_collections_login_prolonged_server_maintenance_error(){
-  // Test info/collections prolonged server maintenance errors are reported.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  yield configureIdentity({username: "broken.info"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, SYNC_FAILED);
-    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
-    do_check_true(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_meta_global_login_prolonged_server_maintenance_error(){
-  // Test meta/global prolonged server maintenance errors are reported.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  yield configureIdentity({username: "broken.meta"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, SYNC_FAILED);
-    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
-    do_check_true(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_download_crypto_keys_login_prolonged_server_maintenance_error(){
-  // Test crypto/keys prolonged server maintenance errors are reported.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  yield configureIdentity({username: "broken.keys"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-  // Force re-download of keys
-  Service.collectionKeys.clear();
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, SYNC_FAILED);
-    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
-    do_check_true(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_upload_crypto_keys_login_prolonged_server_maintenance_error(){
-  // Test crypto/keys prolonged server maintenance errors are reported.
-  let server = sync_httpd_setup();
-
-  // Start off with an empty account, do not upload a key.
-  yield configureIdentity({username: "broken.keys"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, SYNC_FAILED);
-    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
-    do_check_true(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_wipeServer_login_prolonged_server_maintenance_error(){
-  // Test that we report prolonged server maintenance errors that occur whilst
-  // wiping the server.
-  let server = sync_httpd_setup();
-
-  // Start off with an empty account, do not upload a key.
-  yield configureIdentity({username: "broken.wipe"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, SYNC_FAILED);
-    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
-    do_check_true(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  Service.sync();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_wipeRemote_prolonged_server_maintenance_error(){
-  // Test that we report prolonged server maintenance errors that occur whilst
-  // wiping all remote devices.
-  let server = sync_httpd_setup();
-
-  server.registerPathHandler("/1.1/broken.wipe/storage/catapult", service_unavailable);
-  yield configureIdentity({username: "broken.wipe"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-  generateAndUploadKeys();
-
-  let engine = engineManager.get("catapult");
-  engine.exception = null;
-  engine.enabled = true;
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, SYNC_FAILED);
-    do_check_eq(Status.sync, PROLONGED_SYNC_FAILURE);
-    do_check_eq(Svc.Prefs.get("firstSync"), "wipeRemote");
-    do_check_true(errorHandler.didReportProlongedError);
-    server.stop(() => {
-      clean();
-      deferred.resolve();
-    });
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  Svc.Prefs.set("firstSync", "wipeRemote");
-  setLastSync(PROLONGED_ERROR_DURATION);
-  let ping = yield sync_and_validate_telem(true);
-  deepEqual(ping.failureReason, { name: "httperror", code: 503 });
-  yield deferred.promise;
-});
-
-add_task(function* test_sync_syncAndReportErrors_server_maintenance_error() {
-  // Test server maintenance errors are reported
-  // when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  const BACKOFF = 42;
-  let engine = engineManager.get("catapult");
-  engine.enabled = true;
-  engine.exception = {status: 503,
-                      headers: {"retry-after": BACKOFF}};
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
-    do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
-    do_check_eq(Status.sync, SERVER_MAINTENANCE);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_info_collections_login_syncAndReportErrors_server_maintenance_error() {
-  // Test info/collections server maintenance errors are reported
-  // when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  yield configureIdentity({username: "broken.info"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_meta_global_login_syncAndReportErrors_server_maintenance_error() {
-  // Test meta/global server maintenance errors are reported
-  // when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  yield configureIdentity({username: "broken.meta"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_download_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
-  // Test crypto/keys server maintenance errors are reported
-  // when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  yield configureIdentity({username: "broken.keys"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-  // Force re-download of keys
-  Service.collectionKeys.clear();
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_upload_crypto_keys_login_syncAndReportErrors_server_maintenance_error() {
-  // Test crypto/keys server maintenance errors are reported
-  // when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-
-  // Start off with an empty account, do not upload a key.
-  yield configureIdentity({username: "broken.keys"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_wipeServer_login_syncAndReportErrors_server_maintenance_error() {
-  // Test crypto/keys server maintenance errors are reported
-  // when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-
-  // Start off with an empty account, do not upload a key.
-  yield configureIdentity({username: "broken.wipe"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_wipeRemote_syncAndReportErrors_server_maintenance_error(){
-  // Test that we report prolonged server maintenance errors that occur whilst
-  // wiping all remote devices.
-  let server = sync_httpd_setup();
-
-  yield configureIdentity({username: "broken.wipe"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-  generateAndUploadKeys();
-
-  let engine = engineManager.get("catapult");
-  engine.exception = null;
-  engine.enabled = true;
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, SYNC_FAILED);
-    do_check_eq(Status.sync, SERVER_MAINTENANCE);
-    do_check_eq(Svc.Prefs.get("firstSync"), "wipeRemote");
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  Svc.Prefs.set("firstSync", "wipeRemote");
-  setLastSync(NON_PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_task(function* test_sync_syncAndReportErrors_prolonged_server_maintenance_error() {
-  // Test prolonged server maintenance errors are
-  // reported when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  const BACKOFF = 42;
-  let engine = engineManager.get("catapult");
-  engine.enabled = true;
-  engine.exception = {status: 503,
-                      headers: {"retry-after": BACKOFF}};
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:sync:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:sync:error", onUIUpdate);
-    do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
-    do_check_eq(Status.sync, SERVER_MAINTENANCE);
-    // syncAndReportErrors means dontIgnoreErrors, which means
-    // didReportProlongedError not touched.
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_info_collections_login_syncAndReportErrors_prolonged_server_maintenance_error() {
-  // Test info/collections server maintenance errors are reported
-  // when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  yield configureIdentity({username: "broken.info"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    // syncAndReportErrors means dontIgnoreErrors, which means
-    // didReportProlongedError not touched.
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_meta_global_login_syncAndReportErrors_prolonged_server_maintenance_error() {
-  // Test meta/global server maintenance errors are reported
-  // when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  yield configureIdentity({username: "broken.meta"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    // syncAndReportErrors means dontIgnoreErrors, which means
-    // didReportProlongedError not touched.
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_download_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
-  // Test crypto/keys server maintenance errors are reported
-  // when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-  yield setUp(server);
-
-  yield configureIdentity({username: "broken.keys"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-  // Force re-download of keys
-  Service.collectionKeys.clear();
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    // syncAndReportErrors means dontIgnoreErrors, which means
-    // didReportProlongedError not touched.
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_upload_crypto_keys_login_syncAndReportErrors_prolonged_server_maintenance_error() {
-  // Test crypto/keys server maintenance errors are reported
-  // when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-
-  // Start off with an empty account, do not upload a key.
-  yield configureIdentity({username: "broken.keys"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    // syncAndReportErrors means dontIgnoreErrors, which means
-    // didReportProlongedError not touched.
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_identity_test(this, function* test_wipeServer_login_syncAndReportErrors_prolonged_server_maintenance_error() {
-  // Test crypto/keys server maintenance errors are reported
-  // when calling syncAndReportErrors.
-  let server = sync_httpd_setup();
-
-  // Start off with an empty account, do not upload a key.
-  yield configureIdentity({username: "broken.wipe"});
-  Service.serverURL = server.baseURI + "/maintenance/";
-  Service.clusterURL = server.baseURI + "/maintenance/";
-
-  let backoffInterval;
-  Svc.Obs.add("weave:service:backoff:interval", function observe(subject, data) {
-    Svc.Obs.remove("weave:service:backoff:interval", observe);
-    backoffInterval = subject;
-  });
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:ui:login:error", function onUIUpdate() {
-    Svc.Obs.remove("weave:ui:login:error", onUIUpdate);
-    do_check_true(Status.enforceBackoff);
-    do_check_eq(backoffInterval, 42);
-    do_check_eq(Status.service, LOGIN_FAILED);
-    do_check_eq(Status.login, SERVER_MAINTENANCE);
-    // syncAndReportErrors means dontIgnoreErrors, which means
-    // didReportProlongedError not touched.
-    do_check_false(errorHandler.didReportProlongedError);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_false(Status.enforceBackoff);
-  do_check_eq(Status.service, STATUS_OK);
-
-  setLastSync(PROLONGED_ERROR_DURATION);
-  errorHandler.syncAndReportErrors();
-  yield deferred.promise;
-});
-
-add_task(function* test_sync_engine_generic_fail() {
-  let server = sync_httpd_setup();
-
-let engine = engineManager.get("catapult");
-  engine.enabled = true;
-  engine.sync = function sync() {
-    Svc.Obs.notify("weave:engine:sync:error", ENGINE_UNKNOWN_FAIL, "catapult");
-  };
-
-  let log = Log.repository.getLogger("Sync.ErrorHandler");
-  Svc.Prefs.set("log.appender.file.logOnError", true);
-
-  do_check_eq(Status.engines["catapult"], undefined);
-
-  let deferred = Promise.defer();
-  // Don't wait for reset-file-log until the sync is underway.
-  // This avoids us catching a delayed notification from an earlier test.
-  Svc.Obs.add("weave:engine:sync:finish", function onEngineFinish() {
-    Svc.Obs.remove("weave:engine:sync:finish", onEngineFinish);
-
-    log.info("Adding reset-file-log observer.");
-    Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
-      Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
-
-      // Put these checks here, not after sync(), so that we aren't racing the
-      // log handler... which resets everything just a few lines below!
-      _("Status.engines: " + JSON.stringify(Status.engines));
-      do_check_eq(Status.engines["catapult"], ENGINE_UNKNOWN_FAIL);
-      do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
-
-      // Test Error log was written on SYNC_FAILED_PARTIAL.
-      let entries = logsdir.directoryEntries;
-      do_check_true(entries.hasMoreElements());
-      let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
-      do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
-
-      clean();
-
-      let syncErrors = sumHistogram("WEAVE_ENGINE_SYNC_ERRORS", { key: "catapult" });
-      do_check_true(syncErrors, 1);
-
-      server.stop(() => {
-        clean();
-        deferred.resolve();
-      });
-    });
-  });
-
-  do_check_true(yield setUp(server));
-  let ping = yield sync_and_validate_telem(true);
-  deepEqual(ping.status.service, SYNC_FAILED_PARTIAL);
-  deepEqual(ping.engines.find(e => e.status).status, ENGINE_UNKNOWN_FAIL);
-
-  yield deferred.promise;
-});
-
-add_test(function test_logs_on_sync_error_despite_shouldReportError() {
-  _("Ensure that an error is still logged when weave:service:sync:error " +
-    "is notified, despite shouldReportError returning false.");
-
-  let log = Log.repository.getLogger("Sync.ErrorHandler");
-  Svc.Prefs.set("log.appender.file.logOnError", true);
-  log.info("TESTING");
-
-  // Ensure that we report no error.
-  Status.login = MASTER_PASSWORD_LOCKED;
-  do_check_false(errorHandler.shouldReportError());
-
-  Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
-    Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
-
-    // Test that error log was written.
-    let entries = logsdir.directoryEntries;
-    do_check_true(entries.hasMoreElements());
-    let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
-    do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
-
-    clean();
-    run_next_test();
-  });
-  Svc.Obs.notify("weave:service:sync:error", {});
-});
-
-add_test(function test_logs_on_login_error_despite_shouldReportError() {
-  _("Ensure that an error is still logged when weave:service:login:error " +
-    "is notified, despite shouldReportError returning false.");
-
-  let log = Log.repository.getLogger("Sync.ErrorHandler");
-  Svc.Prefs.set("log.appender.file.logOnError", true);
-  log.info("TESTING");
-
-  // Ensure that we report no error.
-  Status.login = MASTER_PASSWORD_LOCKED;
-  do_check_false(errorHandler.shouldReportError());
-
-  Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
-    Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
-
-    // Test that error log was written.
-    let entries = logsdir.directoryEntries;
-    do_check_true(entries.hasMoreElements());
-    let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
-    do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
-
-    clean();
-    run_next_test();
-  });
-  Svc.Obs.notify("weave:service:login:error", {});
-});
-
-// This test should be the last one since it monkeypatches the engine object
-// and we should only have one engine object throughout the file (bug 629664).
-add_task(function* test_engine_applyFailed() {
-  let server = sync_httpd_setup();
-
-  let engine = engineManager.get("catapult");
-  engine.enabled = true;
-  delete engine.exception;
-  engine.sync = function sync() {
-    Svc.Obs.notify("weave:engine:sync:applied", {newFailed:1}, "catapult");
-  };
-
-  let log = Log.repository.getLogger("Sync.ErrorHandler");
-  Svc.Prefs.set("log.appender.file.logOnError", true);
-
-  let deferred = Promise.defer();
-  Svc.Obs.add("weave:service:reset-file-log", function onResetFileLog() {
-    Svc.Obs.remove("weave:service:reset-file-log", onResetFileLog);
-
-    do_check_eq(Status.engines["catapult"], ENGINE_APPLY_FAIL);
-    do_check_eq(Status.service, SYNC_FAILED_PARTIAL);
-
-    // Test Error log was written on SYNC_FAILED_PARTIAL.
-    let entries = logsdir.directoryEntries;
-    do_check_true(entries.hasMoreElements());
-    let logfile = entries.getNext().QueryInterface(Ci.nsILocalFile);
-    do_check_true(logfile.leafName.startsWith("error-sync-"), logfile.leafName);
-
-    clean();
-    server.stop(deferred.resolve);
-  });
-
-  do_check_eq(Status.engines["catapult"], undefined);
-  do_check_true(yield setUp(server));
-  Service.sync();
-  yield deferred.promise;
-});
new file mode 100644
--- /dev/null
+++ b/services/sync/tests/unit/test_errorhandler_1.js
@@ -0,0 +1,913 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-sync/engines/clients.js");
+Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-sync/engines.js");
+Cu.import("resource://services-sync/keys.js");
+Cu.import("resource://services-sync/policies.js");
+Cu.import("resource://services-sync/service.js");
+Cu.import("resource://services-sync/status.js");
+Cu.import("resource://services-sync/util.js");
+Cu.import("resource://testing-common/services/sync/utils.js");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+
+var fakeServer = new SyncServer();
+fakeServer.start();
+
+do_register_cleanup(function() {
+  return new Promise(resolve => {
+    fakeServer.stop(resolve);
+  });
+});
+
+var fakeServerUrl = "http://localhost:" + fakeServer.port;
+
+const logsdir = FileUtils.getDir("ProfD", ["weave", "logs"], true);
+
+const PROLONGED_ERROR_DURATION =
+  (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') * 2) * 1000;
+
+const NON_PROLONGED_ERROR_DURATION =
+  (Svc.Prefs.get('errorhandler.networkFailureReportTimeout') / 2) * 1000;
+
+Service.engineManager.clear();
+
+function setLastSync(lastSyncValue) {
+  Svc.Prefs.set("lastSync", (new Date(Date.now() - lastSyncValue)).toString());
+}
+
+var engineManager = Service.engineManager;
+engineManager.register(EHTestsCommon.CatapultEngine);
+
+// This relies on Service/ErrorHandler being a singleton. Fixing this will take
+// a lot of work.
+var errorHandler = Service.errorHandler;
+
+function run_test() {
+  initTestLogging("Trace");
+
+  Log.repository.getLogger("Sync.Service").level = Log.Level.Trace;
+  Log.repository.getLogger("Sync.SyncScheduler").level = Log.Level.Trace;
+  Log.repository.getLogger("Sync.ErrorHandler").level = Log.Level.Trace;
+
+  ensureLegacyIdentityManager();
+
+  run_next_test();
+}
+
+
+function clean() {
+  Service.startOver();
+  Status.resetSync();
+  Status.resetBackoff();
+  errorHandler.didReportProlongedError = false;
+}
+
+add_identity_test(this, function* test_401_logout() {
+  let server = EHTestsCommon.sync_httpd_setup();
+  yield EHTestsCommon.setUp(server);
+
+  // By calling sync, we ensure we're logged in.
+  yield sync_and_validate_telem();
+  do_check_eq(Status.sync, SYNC_SUCCEEDED);
+  do_check_true(Service.isLoggedIn);
+
+  let deferred = Promise.defer();
+  Svc.Obs.add("weave:service:sync:error", onSyncError);
+  function onSyncError() {
+    _("Got weave:service:sync:error in first sync.");
+    Svc.Obs.remove("weave:service:sync:error", onSyncError);
+
+    // Wait for the automatic next sync.
+    function onLoginError() {
+      _("Got weave:service:login:error in second sync.");
+      Svc.Obs.remove("weave:service:login:error", onLoginError);
+
+      let expected = isConfiguredWithLegacyIdentity() ?
+                     LOGIN_FAILED_LOGIN_REJECTED : LOGIN_FAILED_NETWORK_ERROR;
+
+      do_check_eq(Status.login, expected);
+      do_check_false(Service.isLoggedIn);
+
+      // Clean up.
+      Utils.nextTick(function () {
+        Service.startOver();
+        server.stop(deferred.resolve);
+      });
+    }
+    Svc.Obs.add("weave:service:login:error", onLoginError);
+  }
+
+  // Make sync fail due to login rejected.
+  yield configureIdentity({username: "janedoe"});
+  Service._updateCachedURLs();
+
+  _("Starting first sync.");
+  let ping = yield sync_and_validate_telem(true);
+  deepEqual(ping.failureReason, { name: "httperror", code: 401 });
+  _("First sync done.");
+  yield deferred.promise;
+});
+
+add_identity_test(this, function* test_credentials_changed_logout() {
+  let server = EHTestsCommon.sync_httpd_setup();
+  yield EHTestsCommon.setUp(server);
+
+  // By calling sync, we ensure we're logged in.
+  yield sync_and_validate_telem();
+  do_check_eq(Status.sync, SYNC_SUCCEEDED);
+  do_check_true(Service.isLoggedIn);
+
+  EHTestsCommon.generateCredentialsChangedFailure();
+
+  let ping = yield sync_and_validate_telem(true);
+  equal(ping.status.sync, CREDENTIALS_CHANGED);
+  deepEqual(ping.failureReason, {
+    name: "unexpectederror",
+    error: "Error: Aborting sync, remote setup failed"
+  });
+
+  do_check_eq(Status.sync, CREDENTIALS_CHANGED);
+  do_check_false(Service.isLoggedIn);
+
+  // Clean up.
+  Service.startOver();
+  let deferred = Promise.defer();
+  server.stop(deferred.resolve);
+  yield deferred.promise;
+});
+
+add_identity_test(this, function test_no_lastSync_pref() {
+  // Test reported error.
+  Status.resetSync();
+  errorHandler.dontIgnoreErrors = true;
+  Status.sync = CREDENTIALS_CHANGED;
+  do_check_true(errorHandler.shouldReportError());
+
+  // Test unreported error.
+  Status.resetSync();
+  errorHandler.dontIgnoreErrors = true;
+  Status.login = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(errorHandler.shouldReportError());
+
+});
+
+add_identity_test(this, function test_shouldReportError() {
+  Status.login = MASTER_PASSWORD_LOCKED;
+  do_check_false(errorHandler.shouldReportError());
+
+  // Give ourselves a clusterURL so that the temporary 401 no-error situation
+  // doesn't come into play.
+  Service.serverURL  = fakeServerUrl;
+  Service.clusterURL = fakeServerUrl;
+
+  // Test dontIgnoreErrors, non-network, non-prolonged, login error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = true;
+  Status.login = LOGIN_FAILED_NO_PASSWORD;
+  do_check_true(errorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, non-network, non-prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = true;
+  Status.sync = CREDENTIALS_CHANGED;
+  do_check_true(errorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, non-network, prolonged, login error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = true;
+  Status.login = LOGIN_FAILED_NO_PASSWORD;
+  do_check_true(errorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, non-network, prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = true;
+  Status.sync = CREDENTIALS_CHANGED;
+  do_check_true(errorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, network, non-prolonged, login error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = true;
+  Status.login = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(errorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, network, non-prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = true;
+  Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(errorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, network, prolonged, login error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = true;
+  Status.login = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(errorHandler.shouldReportError());
+
+  // Test dontIgnoreErrors, network, prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = true;
+  Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(errorHandler.shouldReportError());
+
+  // Test non-network, prolonged, login error reported
+  do_check_false(errorHandler.didReportProlongedError);
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  Status.login = LOGIN_FAILED_NO_PASSWORD;
+  do_check_true(errorHandler.shouldReportError());
+  do_check_true(errorHandler.didReportProlongedError);
+
+  // Second time with prolonged error and without resetting
+  // didReportProlongedError, sync error should not be reported.
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  Status.login = LOGIN_FAILED_NO_PASSWORD;
+  do_check_false(errorHandler.shouldReportError());
+  do_check_true(errorHandler.didReportProlongedError);
+
+  // Test non-network, prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  errorHandler.didReportProlongedError = false;
+  Status.sync = CREDENTIALS_CHANGED;
+  do_check_true(errorHandler.shouldReportError());
+  do_check_true(errorHandler.didReportProlongedError);
+  errorHandler.didReportProlongedError = false;
+
+  // Test network, prolonged, login error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  Status.login = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(errorHandler.shouldReportError());
+  do_check_true(errorHandler.didReportProlongedError);
+  errorHandler.didReportProlongedError = false;
+
+  // Test network, prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_true(errorHandler.shouldReportError());
+  do_check_true(errorHandler.didReportProlongedError);
+  errorHandler.didReportProlongedError = false;
+
+  // Test non-network, non-prolonged, login error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  Status.login = LOGIN_FAILED_NO_PASSWORD;
+  do_check_true(errorHandler.shouldReportError());
+  do_check_false(errorHandler.didReportProlongedError);
+
+  // Test non-network, non-prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  Status.sync = CREDENTIALS_CHANGED;
+  do_check_true(errorHandler.shouldReportError());
+  do_check_false(errorHandler.didReportProlongedError);
+
+  // Test network, non-prolonged, login error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  Status.login = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_false(errorHandler.shouldReportError());
+  do_check_false(errorHandler.didReportProlongedError);
+
+  // Test network, non-prolonged, sync error reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  Status.sync = LOGIN_FAILED_NETWORK_ERROR;
+  do_check_false(errorHandler.shouldReportError());
+  do_check_false(errorHandler.didReportProlongedError);
+
+  // Test server maintenance, sync errors are not reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  Status.sync = SERVER_MAINTENANCE;
+  do_check_false(errorHandler.shouldReportError());
+  do_check_false(errorHandler.didReportProlongedError);
+
+  // Test server maintenance, login errors are not reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  Status.login = SERVER_MAINTENANCE;
+  do_check_false(errorHandler.shouldReportError());
+  do_check_false(errorHandler.didReportProlongedError);
+
+  // Test prolonged, server maintenance, sync errors are reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  Status.sync = SERVER_MAINTENANCE;
+  do_check_true(errorHandler.shouldReportError());
+  do_check_true(errorHandler.didReportProlongedError);
+  errorHandler.didReportProlongedError = false;
+
+  // Test prolonged, server maintenance, login errors are reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = false;
+  Status.login = SERVER_MAINTENANCE;
+  do_check_true(errorHandler.shouldReportError());
+  do_check_true(errorHandler.didReportProlongedError);
+  errorHandler.didReportProlongedError = false;
+
+  // Test dontIgnoreErrors, server maintenance, sync errors are reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = true;
+  Status.sync = SERVER_MAINTENANCE;
+  do_check_true(errorHandler.shouldReportError());
+  // dontIgnoreErrors means we don't set didReportProlongedError
+  do_check_false(errorHandler.didReportProlongedError);
+
+  // Test dontIgnoreErrors, server maintenance, login errors are reported
+  Status.resetSync();
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = true;
+  Status.login = SERVER_MAINTENANCE;
+  do_check_true(errorHandler.shouldReportError());
+  do_check_false(errorHandler.didReportProlongedError);
+
+  // Test dontIgnoreErrors, prolonged, server maintenance,
+  // sync errors are reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = true;
+  Status.sync = SERVER_MAINTENANCE;
+  do_check_true(errorHandler.shouldReportError());
+  do_check_false(errorHandler.didReportProlongedError);
+
+  // Test dontIgnoreErrors, prolonged, server maintenance,
+  // login errors are reported
+  Status.resetSync();
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.dontIgnoreErrors = true;
+  Status.login = SERVER_MAINTENANCE;
+  do_check_true(errorHandler.shouldReportError());
+  do_check_false(errorHandler.didReportProlongedError);
+});
+
+add_identity_test(this, function* test_shouldReportError_master_password() {
+  _("Test error ignored due to locked master password");
+  let server = EHTestsCommon.sync_httpd_setup();
+  yield EHTestsCommon.setUp(server);
+
+  // Monkey patch Service.verifyLogin to imitate
+  // master password being locked.
+  Service._verifyLogin = Service.verifyLogin;
+  Service.verifyLogin = function () {
+    Status.login = MASTER_PASSWORD_LOCKED;
+    return false;
+  };
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  Service.sync();
+  do_check_false(errorHandler.shouldReportError());
+
+  // Clean up.
+  Service.verifyLogin = Service._verifyLogin;
+  clean();
+  let deferred = Promise.defer();
+  server.stop(deferred.resolve);
+  yield deferred.promise;
+});
+
+// Test that even if we don't have a cluster URL, a login failure due to
+// authentication errors is always reported.
+add_identity_test(this, function test_shouldReportLoginFailureWithNoCluster() {
+  // Ensure no clusterURL - any error not specific to login should not be reported.
+  Service.serverURL  = "";
+  Service.clusterURL = "";
+
+  // Test explicit "login rejected" state.
+  Status.resetSync();
+  // If we have a LOGIN_REJECTED state, we always report the error.
+  Status.login = LOGIN_FAILED_LOGIN_REJECTED;
+  do_check_true(errorHandler.shouldReportError());
+  // But any other status with a missing clusterURL is treated as a mid-sync
+  // 401 (ie, should be treated as a node reassignment)
+  Status.login = LOGIN_SUCCEEDED;
+  do_check_false(errorHandler.shouldReportError());
+});
+
+// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
+// an fxaccounts environment?
+add_task(function* test_login_syncAndReportErrors_non_network_error() {
+  // Test non-network errors are reported
+  // when calling syncAndReportErrors
+  let server = EHTestsCommon.sync_httpd_setup();
+  yield EHTestsCommon.setUp(server);
+  Service.identity.basicPassword = null;
+
+  let deferred = Promise.defer();
+  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:login:error", onSyncError);
+    do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
+
+    clean();
+    server.stop(deferred.resolve);
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  errorHandler.syncAndReportErrors();
+  yield deferred.promise;
+});
+
+add_identity_test(this, function* test_sync_syncAndReportErrors_non_network_error() {
+  // Test non-network errors are reported
+  // when calling syncAndReportErrors
+  let server = EHTestsCommon.sync_httpd_setup();
+  yield EHTestsCommon.setUp(server);
+
+  // By calling sync, we ensure we're logged in.
+  Service.sync();
+  do_check_eq(Status.sync, SYNC_SUCCEEDED);
+  do_check_true(Service.isLoggedIn);
+
+  EHTestsCommon.generateCredentialsChangedFailure();
+
+  let deferred = Promise.defer();
+  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
+    do_check_eq(Status.sync, CREDENTIALS_CHANGED);
+    // If we clean this tick, telemetry won't get the right error
+    server.stop(() => {
+      clean();
+      deferred.resolve();
+    });
+  });
+
+  setLastSync(NON_PROLONGED_ERROR_DURATION);
+  let ping = yield wait_for_ping(() => errorHandler.syncAndReportErrors(), true);
+  equal(ping.status.sync, CREDENTIALS_CHANGED);
+  deepEqual(ping.failureReason, {
+    name: "unexpectederror",
+    error: "Error: Aborting sync, remote setup failed"
+  });
+  yield deferred.promise;
+});
+
+// XXX - how to arrange for 'Service.identity.basicPassword = null;' in
+// an fxaccounts environment?
+add_task(function* test_login_syncAndReportErrors_prolonged_non_network_error() {
+  // Test prolonged, non-network errors are
+  // reported when calling syncAndReportErrors.
+  let server = EHTestsCommon.sync_httpd_setup();
+  yield EHTestsCommon.setUp(server);
+  Service.identity.basicPassword = null;
+
+  let deferred = Promise.defer();
+  Svc.Obs.add("weave:ui:login:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:login:error", onSyncError);
+    do_check_eq(Status.login, LOGIN_FAILED_NO_PASSWORD);
+
+    clean();
+    server.stop(deferred.resolve);
+  });
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  errorHandler.syncAndReportErrors();
+  yield deferred.promise;
+});
+
+add_identity_test(this, function* test_sync_syncAndReportErrors_prolonged_non_network_error() {
+  // Test prolonged, non-network errors are
+  // reported when calling syncAndReportErrors.
+  let server = EHTestsCommon.sync_httpd_setup();
+  yield EHTestsCommon.setUp(server);
+
+  // By calling sync, we ensure we're logged in.
+  Service.sync();
+  do_check_eq(Status.sync, SYNC_SUCCEEDED);
+  do_check_true(Service.isLoggedIn);
+
+  EHTestsCommon.generateCredentialsChangedFailure();
+
+  let deferred = Promise.defer();
+  Svc.Obs.add("weave:ui:sync:error", function onSyncError() {
+    Svc.Obs.remove("weave:ui:sync:error", onSyncError);
+    do_check_eq(Status.sync, CREDENTIALS_CHANGED);
+    // If we clean this tick, telemetry won't get the right error
+    server.stop(() => {
+      clean();
+      deferred.resolve();
+    });
+  });
+
+  setLastSync(PROLONGED_ERROR_DURATION);
+  let ping = yield wait_for_ping(() => errorHandler.syncAndReportErrors(), true);