merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 15 Apr 2016 11:39:35 +0200
changeset 331078 755a175b3cc735b575a9e3dd4fcfcd4c7a63695c
parent 331033 10f66b3164570b2183333262fa91a16004cbb908 (current diff)
parent 331077 3e85a84c7d68632e1098cc9374787dc8f54ce24f (diff)
child 331224 afd82f887093e5e9e4015115ca5795ec82a6f732
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
devtools/client/main.js
mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -691,16 +691,19 @@ html|*#fullscreen-warning:not([hidden]) 
 }
 html|*#fullscreen-warning[onscreen] {
   transform: translate(-50%, 50px);
 }
 html|*#fullscreen-warning[ontop] {
   /* Use -10px to hide the border and border-radius on the top */
   transform: translate(-50%, -10px);
 }
+#main-window[OSXLionFullscreen] html|*#fullscreen-warning[ontop] {
+  transform: translate(-50%, 80px);
+}
 
 html|*#fullscreen-domain-text,
 html|*#fullscreen-generic-text {
   word-wrap: break-word;
   /* We must specify a min-width, otherwise word-wrap:break-word doesn't work. Bug 630864. */
   min-width: 1px
 }
 html|*#fullscreen-domain-text:not([hidden]) + html|*#fullscreen-generic-text {
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2371,35 +2371,46 @@ function URLBarSetURI(aURI) {
   }
 
   gURLBar.value = value;
   gURLBar.valueIsTyped = !valid;
   SetPageProxyState(valid ? "valid" : "invalid");
 }
 
 function losslessDecodeURI(aURI) {
-  if (aURI.schemeIs("moz-action"))
+  let scheme = aURI.scheme;
+  if (scheme == "moz-action")
     throw new Error("losslessDecodeURI should never get a moz-action URI");
 
   var value = aURI.spec;
 
+  let decodeASCIIOnly = !["https", "http", "file", "ftp"].includes(scheme);
   // Try to decode as UTF-8 if there's no encoding sequence that we would break.
-  if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value))
-    try {
-      value = decodeURI(value)
-                // 1. decodeURI decodes %25 to %, which creates unintended
-                //    encoding sequences. Re-encode it, unless it's part of
-                //    a sequence that survived decodeURI, i.e. one for:
-                //    ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
-                //    (RFC 3987 section 3.2)
-                // 2. Re-encode whitespace so that it doesn't get eaten away
-                //    by the location bar (bug 410726).
-                .replace(/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)|[\r\n\t]/ig,
-                         encodeURIComponent);
-    } catch (e) {}
+  if (!/%25(?:3B|2F|3F|3A|40|26|3D|2B|24|2C|23)/i.test(value)) {
+    if (decodeASCIIOnly) {
+      // This only decodes ascii characters (hex) 20-7e, except 25 (%).
+      // This avoids both cases stipulated below (%-related issues, and \r, \n
+      // and \t, which would be %0d, %0a and %09, respectively) as well as any
+      // non-US-ascii characters.
+      value = value.replace(/%(2[0-4]|2[6-9a-f]|[3-6][0-9a-f]|7[0-9a-e])/g, decodeURI);
+    } else {
+      try {
+        value = decodeURI(value)
+                  // 1. decodeURI decodes %25 to %, which creates unintended
+                  //    encoding sequences. Re-encode it, unless it's part of
+                  //    a sequence that survived decodeURI, i.e. one for:
+                  //    ';', '/', '?', ':', '@', '&', '=', '+', '$', ',', '#'
+                  //    (RFC 3987 section 3.2)
+                  // 2. Re-encode whitespace so that it doesn't get eaten away
+                  //    by the location bar (bug 410726).
+                  .replace(/%(?!3B|2F|3F|3A|40|26|3D|2B|24|2C|23)|[\r\n\t]/ig,
+                           encodeURIComponent);
+      } catch (e) {}
+    }
+  }
 
   // Encode invisible characters (C0/C1 control characters, U+007F [DEL],
   // U+00A0 [no-break space], line and paragraph separator,
   // object replacement character) (bug 452979, bug 909264)
   value = value.replace(/[\u0000-\u001f\u007f-\u00a0\u2028\u2029\ufffc]/g,
                         encodeURIComponent);
 
   // Encode default ignorable characters (bug 546013)
--- a/browser/base/content/test/general/browser_urlbarCopying.js
+++ b/browser/base/content/test/general/browser_urlbarCopying.js
@@ -115,37 +115,37 @@ var tests = [
   },
   {
     copyVal: "<example.com/?\xf7>\xf7",
     copyExpected: "http://example.com/?\xf7"
   },
 
   // data: and javsacript: URIs shouldn't be encoded
   {
-    loadURL: "javascript:('%C3%A9')",
-    expectedURL: "javascript:('\xe9')",
-    copyExpected: "javascript:('\xe9')"
+    loadURL: "javascript:('%C3%A9%20%25%50')",
+    expectedURL: "javascript:('%C3%A9 %25P')",
+    copyExpected: "javascript:('%C3%A9 %25P')"
   },
   {
-    copyVal: "<javascript:(>'\xe9')",
+    copyVal: "<javascript:(>'%C3%A9 %25P')",
     copyExpected: "javascript:("
   },
 
   {
-    loadURL: "data:text/html,(%C3%A9)",
-    expectedURL: "data:text/html,(\xe9)",
-    copyExpected: "data:text/html,(\xe9)"
+    loadURL: "data:text/html,(%C3%A9%20%25%50)",
+    expectedURL: "data:text/html,(%C3%A9 %25P)",
+    copyExpected: "data:text/html,(%C3%A9 %25P)",
   },
   {
-    copyVal: "<data:text/html,(>\xe9)",
+    copyVal: "<data:text/html,(>%C3%A9 %25P)",
     copyExpected: "data:text/html,("
   },
   {
-    copyVal: "data:<text/html,(\xe9>)",
-    copyExpected: "text/html,(\xe9"
+    copyVal: "<data:text/html,(%C3%A9 %25P>)",
+    copyExpected: "data:text/html,(%C3%A9 %25P",
   }
 ];
 
 function nextTest() {
   let test = tests.shift();
   if (tests.length == 0)
     runTest(test, finish);
   else
--- a/browser/components/customizableui/CustomizableWidgets.jsm
+++ b/browser/components/customizableui/CustomizableWidgets.jsm
@@ -470,21 +470,23 @@ const CustomizableWidgets = [
           let tabEnt = this._createTabElement(doc, tab);
           attachFragment.appendChild(tabEnt);
         }
       }
     },
     _createTabElement(doc, tabInfo) {
       let win = doc.defaultView;
       let item = doc.createElementNS(kNSXUL, "toolbarbutton");
+      let tooltipText = (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url;
       item.setAttribute("itemtype", "tab");
       item.setAttribute("class", "subviewbutton");
       item.setAttribute("targetURI", tabInfo.url);
       item.setAttribute("label", tabInfo.title != "" ? tabInfo.title : tabInfo.url);
       item.setAttribute("image", tabInfo.icon);
+      item.setAttribute("tooltiptext", tooltipText);
       // We need to use "click" instead of "command" here so openUILink
       // respects different buttons (eg, to open in a new tab).
       item.addEventListener("click", e => {
         doc.defaultView.openUILink(tabInfo.url, e);
         CustomizableUI.hidePanelForNode(item);
       });
       return item;
     },
--- a/browser/components/customizableui/CustomizeMode.jsm
+++ b/browser/components/customizableui/CustomizeMode.jsm
@@ -229,25 +229,26 @@ CustomizeMode.prototype = {
     // Always disable the reset button at the start of customize mode, it'll be re-enabled
     // if necessary when we finish entering:
     let resetButton = this.document.getElementById("customization-reset-button");
     resetButton.setAttribute("disabled", "true");
 
     Task.spawn(function*() {
       // We shouldn't start customize mode until after browser-delayed-startup has finished:
       if (!this.window.gBrowserInit.delayedStartupFinished) {
-        let delayedStartupDeferred = Promise.defer();
-        let delayedStartupObserver = function(aSubject) {
-          if (aSubject == this.window) {
-            Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
-            delayedStartupDeferred.resolve();
-          }
-        }.bind(this);
-        Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
-        yield delayedStartupDeferred.promise;
+        yield new Promise(resolve => {
+          let delayedStartupObserver = aSubject => {
+            if (aSubject == this.window) {
+              Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
+              resolve();
+            }
+          };
+
+          Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
+        });
       }
 
       let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn);
       let togglableToolbars = window.getTogglableToolbars();
       let bookmarksToolbar = document.getElementById("PersonalToolbar");
       if (togglableToolbars.length == 0) {
         toolbarVisibilityBtn.setAttribute("hidden", "true");
       } else {
@@ -557,59 +558,59 @@ CustomizeMode.prototype = {
    * order - customize-entered, customize-exiting, remove LWT swapping,
    * pre-customization mode.
    *
    * When in the customize-entering, customize-entered, or customize-exiting
    * phases, there is a "customizing" attribute set on the main-window to simplify
    * excluding certain styles while in any phase of customize mode.
    */
   _doTransition: function(aEntering) {
-    let deferred = Promise.defer();
     let deck = this.document.getElementById("content-deck");
-
-    let customizeTransitionEnd = (aEvent) => {
-      if (aEvent != "timedout" &&
-          (aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) {
-        return;
-      }
-      this.window.clearTimeout(catchAllTimeout);
-      // We request an animation frame to do the final stage of the transition
-      // to improve perceived performance. (bug 962677)
-      this.window.requestAnimationFrame(() => {
-        deck.removeEventListener("transitionend", customizeTransitionEnd);
+    let customizeTransitionEndPromise = new Promise(resolve => {
+      let customizeTransitionEnd = (aEvent) => {
+        if (aEvent != "timedout" &&
+            (aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) {
+          return;
+        }
+        this.window.clearTimeout(catchAllTimeout);
+        // We request an animation frame to do the final stage of the transition
+        // to improve perceived performance. (bug 962677)
+        this.window.requestAnimationFrame(() => {
+          deck.removeEventListener("transitionend", customizeTransitionEnd);
 
-        if (!aEntering) {
-          this.document.documentElement.removeAttribute("customize-exiting");
-          this.document.documentElement.removeAttribute("customizing");
-        } else {
-          this.document.documentElement.setAttribute("customize-entered", true);
-          this.document.documentElement.removeAttribute("customize-entering");
-        }
-        CustomizableUI.dispatchToolboxEvent("customization-transitionend", aEntering, this.window);
+          if (!aEntering) {
+            this.document.documentElement.removeAttribute("customize-exiting");
+            this.document.documentElement.removeAttribute("customizing");
+          } else {
+            this.document.documentElement.setAttribute("customize-entered", true);
+            this.document.documentElement.removeAttribute("customize-entering");
+          }
+          CustomizableUI.dispatchToolboxEvent("customization-transitionend", aEntering, this.window);
 
-        deferred.resolve();
-      });
-    };
-    deck.addEventListener("transitionend", customizeTransitionEnd);
+          resolve();
+        });
+      };
+      deck.addEventListener("transitionend", customizeTransitionEnd);
+      let catchAll = () => customizeTransitionEnd("timedout");
+      let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs);
+    });
 
     if (gDisableAnimation) {
       this.document.getElementById("tab-view-deck").setAttribute("fastcustomizeanimation", true);
     }
 
     if (aEntering) {
       this.document.documentElement.setAttribute("customizing", true);
       this.document.documentElement.setAttribute("customize-entering", true);
     } else {
       this.document.documentElement.setAttribute("customize-exiting", true);
       this.document.documentElement.removeAttribute("customize-entered");
     }
 
-    let catchAll = () => customizeTransitionEnd("timedout");
-    let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs);
-    return deferred.promise;
+    return customizeTransitionEndPromise;
   },
 
   updateLWTStyling: function(aData) {
     let docElement = this.document.documentElement;
     if (!aData) {
       let lwt = docElement._lightweightTheme;
       aData = lwt.getData();
     }
@@ -863,24 +864,22 @@ CustomizeMode.prototype = {
            aNode.localName == "toolbarspacer";
   },
 
   isWrappedToolbarItem: function(aNode) {
     return aNode.localName == "toolbarpaletteitem";
   },
 
   deferredWrapToolbarItem: function(aNode, aPlace) {
-    let deferred = Promise.defer();
-
-    dispatchFunction(function() {
-      let wrapper = this.wrapToolbarItem(aNode, aPlace);
-      deferred.resolve(wrapper);
-    }.bind(this));
-
-    return deferred.promise;
+    return new Promise(resolve => {
+      dispatchFunction(() => {
+        let wrapper = this.wrapToolbarItem(aNode, aPlace);
+        resolve(wrapper);
+      });
+    });
   },
 
   wrapToolbarItem: function(aNode, aPlace) {
     if (!this.isCustomizableItem(aNode)) {
       return aNode;
     }
     let wrapper = this.createOrUpdateWrapper(aNode, aPlace);
 
@@ -980,27 +979,27 @@ CustomizeMode.prototype = {
       wrapper.addEventListener("mousedown", this);
       wrapper.addEventListener("mouseup", this);
     }
 
     return wrapper;
   },
 
   deferredUnwrapToolbarItem: function(aWrapper) {
-    let deferred = Promise.defer();
-    dispatchFunction(function() {
-      let item = null;
-      try {
-        item = this.unwrapToolbarItem(aWrapper);
-      } catch (ex) {
-        Cu.reportError(ex);
-      }
-      deferred.resolve(item);
-    }.bind(this));
-    return deferred.promise;
+    return new Promise(resolve => {
+      dispatchFunction(() => {
+        let item = null;
+        try {
+          item = this.unwrapToolbarItem(aWrapper);
+        } catch (ex) {
+          Cu.reportError(ex);
+        }
+        resolve(item);
+      });
+    });
   },
 
   unwrapToolbarItem: function(aWrapper) {
     if (aWrapper.nodeName != "toolbarpaletteitem") {
       return aWrapper;
     }
     aWrapper.removeEventListener("mousedown", this);
     aWrapper.removeEventListener("mouseup", this);
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -2218,17 +2218,17 @@ BrowserGlue.prototype = {
 
   ensurePlacesDefaultQueriesInitialized: Task.async(function* () {
     // This is the current smart bookmarks version, it must be increased every
     // time they change.
     // When adding a new smart bookmark below, its newInVersion property must
     // be set to the version it has been added in.  We will compare its value
     // to users' smartBookmarksVersion and add new smart bookmarks without
     // recreating old deleted ones.
-    const SMART_BOOKMARKS_VERSION = 7;
+    const SMART_BOOKMARKS_VERSION = 8;
     const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
     const SMART_BOOKMARKS_PREF = "browser.places.smartBookmarksVersion";
 
     // TODO bug 399268: should this be a pref?
     const MAX_RESULTS = 10;
 
     // Get current smart bookmarks version.  If not set, create them.
     let smartBookmarksCurrentVersion = 0;
@@ -2251,28 +2251,16 @@ BrowserGlue.prototype = {
       let smartBookmarks = {
         MostVisited: {
           title: bundle.GetStringFromName("mostVisitedTitle"),
           url: "place:sort=" + queryOptions.SORT_BY_VISITCOUNT_DESCENDING +
                     "&maxResults=" + MAX_RESULTS,
           parentGuid: PlacesUtils.bookmarks.toolbarGuid,
           newInVersion: 1
         },
-        RecentlyBookmarked: {
-          title: bundle.GetStringFromName("recentlyBookmarkedTitle"),
-          url: "place:folder=BOOKMARKS_MENU" +
-                    "&folder=UNFILED_BOOKMARKS" +
-                    "&folder=TOOLBAR" +
-                    "&queryType=" + queryOptions.QUERY_TYPE_BOOKMARKS +
-                    "&sort=" + queryOptions.SORT_BY_DATEADDED_DESCENDING +
-                    "&maxResults=" + MAX_RESULTS +
-                    "&excludeQueries=1",
-          parentGuid: PlacesUtils.bookmarks.menuGuid,
-          newInVersion: 1
-        },
         RecentTags: {
           title: bundle.GetStringFromName("recentTagsTitle"),
           url: "place:type=" + queryOptions.RESULTS_AS_TAG_QUERY +
                     "&sort=" + queryOptions.SORT_BY_LASTMODIFIED_DESCENDING +
                     "&maxResults=" + MAX_RESULTS,
           parentGuid: PlacesUtils.bookmarks.menuGuid,
           newInVersion: 1
         },
--- a/browser/components/places/tests/unit/head_bookmarks.js
+++ b/browser/components/places/tests/unit/head_bookmarks.js
@@ -33,19 +33,19 @@ Cu.import("resource://testing-common/App
 updateAppInfo({
   name: "PlacesTest",
   ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
   version: "1",
   platformVersion: "",
 });
 
 // Smart bookmarks constants.
-const SMART_BOOKMARKS_VERSION = 7;
+const SMART_BOOKMARKS_VERSION = 8;
 const SMART_BOOKMARKS_ON_TOOLBAR = 1;
-const SMART_BOOKMARKS_ON_MENU =  3; // Takes into account the additional separator.
+const SMART_BOOKMARKS_ON_MENU =  2; // Takes into account the additional separator.
 
 // Default bookmarks constants.
 const DEFAULT_BOOKMARKS_ON_TOOLBAR = 1;
 const DEFAULT_BOOKMARKS_ON_MENU = 1;
 
 const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
 
 function checkItemHasAnnotation(guid, name) {
--- a/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js
+++ b/browser/components/places/tests/unit/test_browserGlue_smartBookmarks.js
@@ -137,23 +137,16 @@ add_task(function* test_version_change_p
 
   let bm = yield PlacesUtils.bookmarks.fetch({
     parentGuid: PlacesUtils.bookmarks.menuGuid,
     index: 0
   });
   yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
   let firstItemTitle = bm.title;
 
-  bm = yield PlacesUtils.bookmarks.fetch({
-    parentGuid: PlacesUtils.bookmarks.menuGuid,
-    index: 1
-  });
-  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
-  let secondItemTitle = bm.title;
-
   // Set preferences.
   Services.prefs.setIntPref(PREF_SMART_BOOKMARKS_VERSION, 1);
 
   yield rebuildSmartBookmarks();
 
   // Count items.
   Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
                SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
@@ -163,23 +156,16 @@ add_task(function* test_version_change_p
   // Check smart bookmarks are still in correct position.
   bm = yield PlacesUtils.bookmarks.fetch({
     parentGuid: PlacesUtils.bookmarks.menuGuid,
     index: 0
   });
   yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
   Assert.equal(bm.title, firstItemTitle);
 
-  bm = yield PlacesUtils.bookmarks.fetch({
-    parentGuid: PlacesUtils.bookmarks.menuGuid,
-    index: 1
-  });
-  yield checkItemHasAnnotation(bm.guid, SMART_BOOKMARKS_ANNO);
-  Assert.equal(bm.title, secondItemTitle);
-
   // Check version has been updated.
   Assert.equal(Services.prefs.getIntPref(PREF_SMART_BOOKMARKS_VERSION),
                SMART_BOOKMARKS_VERSION);
 });
 
 add_task(function* test_version_change_pos_moved() {
   do_print("moved bookmarks position is retained when version changes.");
 
@@ -191,23 +177,16 @@ add_task(function* test_version_change_p
 
   let bm1 = yield PlacesUtils.bookmarks.fetch({
     parentGuid: PlacesUtils.bookmarks.menuGuid,
     index: 0
   });
   yield checkItemHasAnnotation(bm1.guid, SMART_BOOKMARKS_ANNO);
   let firstItemTitle = bm1.title;
 
-  let bm2 = yield PlacesUtils.bookmarks.fetch({
-    parentGuid: PlacesUtils.bookmarks.menuGuid,
-    index: 1
-  });
-  yield checkItemHasAnnotation(bm2.guid, SMART_BOOKMARKS_ANNO);
-  let secondItemTitle = bm2.title;
-
   // Move the first smart bookmark to the end of the menu.
   yield PlacesUtils.bookmarks.update({
     parentGuid: PlacesUtils.bookmarks.menuGuid,
     guid: bm1.guid,
     index: PlacesUtils.bookmarks.DEFAULT_INDEX
   });
 
   let bm = yield PlacesUtils.bookmarks.fetch({
@@ -222,24 +201,16 @@ add_task(function* test_version_change_p
   yield rebuildSmartBookmarks();
 
   // Count items.
   Assert.equal(countFolderChildren(PlacesUtils.toolbarFolderId),
                SMART_BOOKMARKS_ON_TOOLBAR + DEFAULT_BOOKMARKS_ON_TOOLBAR);
   Assert.equal(countFolderChildren(PlacesUtils.bookmarksMenuFolderId),
                SMART_BOOKMARKS_ON_MENU + DEFAULT_BOOKMARKS_ON_MENU);
 
-  // Check smart bookmarks are still in correct position.
-  bm2 = yield PlacesUtils.bookmarks.fetch({
-    parentGuid: PlacesUtils.bookmarks.menuGuid,
-    index: 0
-  });
-  yield checkItemHasAnnotation(bm2.guid, SMART_BOOKMARKS_ANNO);
-  Assert.equal(bm2.title, secondItemTitle);
-
   bm1 = yield PlacesUtils.bookmarks.fetch({
     parentGuid: PlacesUtils.bookmarks.menuGuid,
     index: PlacesUtils.bookmarks.DEFAULT_INDEX
   });
   yield checkItemHasAnnotation(bm1.guid, SMART_BOOKMARKS_ANNO);
   Assert.equal(bm1.title, firstItemTitle);
 
   // Move back the smart bookmark to the original position.
--- a/browser/config/mozconfigs/linux32/debug
+++ b/browser/config/mozconfigs/linux32/debug
@@ -5,16 +5,19 @@ ac_add_options --with-google-oauth-api-k
 
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . $topsrcdir/build/unix/mozconfig.linux32
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 #Use ccache
 
 # Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
 ac_add_options --enable-warnings-as-errors
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
--- a/browser/config/mozconfigs/linux32/debug-asan
+++ b/browser/config/mozconfigs/linux32/debug-asan
@@ -8,15 +8,18 @@ ac_add_options --enable-optimize="-O1"
 # ASan specific options on Linux
 ac_add_options --enable-valgrind
 
 . $topsrcdir/build/unix/mozconfig.asan
 
 export PKG_CONFIG_LIBDIR=/usr/lib/pkgconfig:/usr/share/pkgconfig
 . $topsrcdir/build/unix/mozconfig.gtk
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
 # Need this to prevent name conflicts with the normal nightly build packages
 export MOZ_PKG_SPECIAL=asan
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/linux32/l10n-mozconfig
+++ b/browser/config/mozconfigs/linux32/l10n-mozconfig
@@ -4,11 +4,14 @@ ac_add_options --with-l10n-base=../../l1
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 ac_add_options --enable-update-packaging
 ac_add_options --with-branding=browser/branding/nightly
 
 . $topsrcdir/build/unix/mozconfig.linux32
 
 export MOZILLA_OFFICIAL=1
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 ac_add_options --disable-stdcxx-compat
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/linux32/nightly-asan
+++ b/browser/config/mozconfigs/linux32/nightly-asan
@@ -10,12 +10,15 @@ ac_add_options --enable-valgrind
 . $topsrcdir/build/unix/mozconfig.asan
 
 export PKG_CONFIG_LIBDIR=/usr/lib/pkgconfig:/usr/share/pkgconfig
 . $topsrcdir/build/unix/mozconfig.gtk
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 # Need this to prevent name conflicts with the normal nightly build packages
 export MOZ_PKG_SPECIAL=asan
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/linux64/debug
+++ b/browser/config/mozconfigs/linux64/debug
@@ -5,16 +5,19 @@ ac_add_options --with-google-oauth-api-k
 
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . $topsrcdir/build/unix/mozconfig.linux
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 # Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
 ac_add_options --enable-warnings-as-errors
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
 ac_add_options --with-branding=browser/branding/nightly
 
--- a/browser/config/mozconfigs/linux64/debug-asan
+++ b/browser/config/mozconfigs/linux64/debug-asan
@@ -8,15 +8,18 @@ ac_add_options --enable-optimize="-O1"
 # ASan specific options on Linux
 ac_add_options --enable-valgrind
 
 . $topsrcdir/build/unix/mozconfig.asan
 
 export PKG_CONFIG_LIBDIR=/usr/lib64/pkgconfig:/usr/share/pkgconfig
 . $topsrcdir/build/unix/mozconfig.gtk
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
 # Need this to prevent name conflicts with the normal nightly build packages
 export MOZ_PKG_SPECIAL=asan
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/linux64/l10n-mozconfig
+++ b/browser/config/mozconfigs/linux64/l10n-mozconfig
@@ -4,11 +4,14 @@ ac_add_options --with-l10n-base=../../l1
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 ac_add_options --enable-update-packaging
 ac_add_options --with-branding=browser/branding/nightly
 
 . $topsrcdir/build/unix/mozconfig.linux
 
 export MOZILLA_OFFICIAL=1
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 ac_add_options --disable-stdcxx-compat
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/macosx-universal/l10n-mozconfig
+++ b/browser/config/mozconfigs/macosx-universal/l10n-mozconfig
@@ -7,10 +7,13 @@ ac_add_options --enable-update-packaging
 ac_add_options --with-branding=browser/branding/nightly
 
 if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
 ac_add_options --with-macbundlename-prefix=Firefox
 fi
 
 export MOZILLA_OFFICIAL=1
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 . "$topsrcdir/build/mozconfig.common.override"
 . "$topsrcdir/build/mozconfig.cache"
--- a/browser/config/mozconfigs/macosx64/debug
+++ b/browser/config/mozconfigs/macosx64/debug
@@ -3,16 +3,19 @@
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
 ac_add_options --enable-verify-mar
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
 ac_add_options --with-macbundlename-prefix=Firefox
 fi
 
 # Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
 ac_add_options --enable-warnings-as-errors
 
 # Package js shell.
--- a/browser/config/mozconfigs/macosx64/debug-asan
+++ b/browser/config/mozconfigs/macosx64/debug-asan
@@ -1,15 +1,18 @@
 . $topsrcdir/build/unix/mozconfig.asan
 
 ac_add_options --enable-application=browser
 ac_add_options --enable-debug
 ac_add_options --enable-optimize="-O1"
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
 if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
 ac_add_options --with-macbundlename-prefix=Firefox
 fi
 
 # Need this to prevent name conflicts with the normal nightly build packages
--- a/browser/config/mozconfigs/macosx64/nightly
+++ b/browser/config/mozconfigs/macosx64/nightly
@@ -1,16 +1,19 @@
 . $topsrcdir/build/macosx/mozconfig.common
 
 ac_add_options --enable-verify-mar
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
 ac_add_options --with-macbundlename-prefix=Firefox
 fi
 
 # Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
 ac_add_options --enable-warnings-as-errors
 
 # Package js shell.
--- a/browser/config/mozconfigs/win32/debug
+++ b/browser/config/mozconfigs/win32/debug
@@ -13,16 +13,19 @@ if [ -f /c/builds/google-oauth-api.key ]
 else
   _google_oauth_api_keyfile=/e/builds/google-oauth-api.key
 fi
 ac_add_options --with-google-oauth-api-keyfile=${_google_oauth_api_keyfile}
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 . $topsrcdir/build/win32/mozconfig.vs2015-win64
 
 # Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
 ac_add_options --enable-warnings-as-errors
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
--- a/browser/config/mozconfigs/win32/l10n-mozconfig
+++ b/browser/config/mozconfigs/win32/l10n-mozconfig
@@ -3,15 +3,18 @@
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 ac_add_options --enable-update-packaging
 ac_add_options --with-l10n-base=../../l10n
 ac_add_options --with-windows-version=603
 ac_add_options --with-branding=browser/branding/nightly
 
 export MOZILLA_OFFICIAL=1
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 if test "$PROCESSOR_ARCHITECTURE" = "AMD64" -o "$PROCESSOR_ARCHITEW6432" = "AMD64"; then
   . $topsrcdir/build/win32/mozconfig.vs2015-win64
 else
   . $topsrcdir/build/win32/mozconfig.vs2010
 fi
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/win64/debug
+++ b/browser/config/mozconfigs/win64/debug
@@ -15,16 +15,19 @@ if [ -f /c/builds/google-oauth-api.key ]
 else
   _google_oauth_api_keyfile=/e/builds/google-oauth-api.key
 fi
 ac_add_options --with-google-oauth-api-keyfile=${_google_oauth_api_keyfile}
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 # Treat warnings as errors (modulo ALLOW_COMPILER_WARNINGS).
 ac_add_options --enable-warnings-as-errors
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
 ac_add_options --with-branding=browser/branding/nightly
 
--- a/browser/config/mozconfigs/win64/l10n-mozconfig
+++ b/browser/config/mozconfigs/win64/l10n-mozconfig
@@ -4,11 +4,14 @@
 ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
 ac_add_options --enable-update-packaging
 ac_add_options --with-l10n-base=../../l10n
 ac_add_options --with-windows-version=603
 ac_add_options --with-branding=browser/branding/nightly
 
 export MOZILLA_OFFICIAL=1
 
+# Enable Telemetry
+export MOZ_TELEMETRY_REPORTING=1
+
 . $topsrcdir/build/win64/mozconfig.vs2015
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/locales/en-US/chrome/browser/places/places.properties
+++ b/browser/locales/en-US/chrome/browser/places/places.properties
@@ -60,17 +60,16 @@ EnterExport=Export Bookmarks File
 detailsPane.noItems=No items
 # LOCALIZATION NOTE (detailsPane.itemsCountLabel): Semicolon-separated list of plural forms.
 # See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
 # #1 number of items
 # example: 111 items
 detailsPane.itemsCountLabel=One item;#1 items
 
 mostVisitedTitle=Most Visited
-recentlyBookmarkedTitle=Recently Bookmarked
 recentTagsTitle=Recent Tags
 
 OrganizerQueryHistory=History
 OrganizerQueryDownloads=Downloads
 OrganizerQueryAllBookmarks=All Bookmarks
 OrganizerQueryTags=Tags
 
 # LOCALIZATION NOTE (tagResultLabel, bookmarkResultLabel, switchtabResultLabel,
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -102,25 +102,21 @@
   /* Move up into the TabsToolbar for the inner highlight at the top of the nav-bar */
   margin-top: calc(-1 * var(--navbar-tab-toolbar-highlight-overlap));
   /* Position the toolbar above the bottom of background tabs */
   position: relative;
   z-index: 1;
 }
 
 #nav-bar {
-  box-shadow: 0 1px 0 @toolbarHighlight@ inset;
+  box-shadow: 0 1px 0 @navbarInsetHighlight@ inset;
   padding-top: 2px;
   padding-bottom: 2px;
 }
 
-#nav-bar:-moz-lwtheme {
-  box-shadow: 0 1px 0 @toolbarHighlightLWT@ inset;
-}
-
 #nav-bar-overflow-button {
   -moz-image-region: rect(-5px, 12px, 11px, -4px);
 }
 
 /* This only has an effect when this element is placed on the bookmarks toolbar.
  * It's 30px to make sure buttons with 18px icons fit along with the default 16px
  * icons, without changing the size of the toolbar.
  */
--- a/browser/themes/linux/linuxShared.inc
+++ b/browser/themes/linux/linuxShared.inc
@@ -1,11 +1,13 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 %filter substitution
 
 %define toolbarHighlight hsla(0,0%,100%,.05)
 %define toolbarHighlightLWT rgba(255,255,255,.4)
+/* navbarInsetHighlight is tightly coupled to the toolbarHighlight constant. */
+%define navbarInsetHighlight hsla(0,0%,100%,.4)
 %define fgTabTexture linear-gradient(transparent 2px, @toolbarHighlight@ 2px, @toolbarHighlight@)
 %define fgTabTextureLWT linear-gradient(transparent 2px, @toolbarHighlightLWT@ 2px, @toolbarHighlightLWT@)
 %define fgTabBackgroundColor -moz-dialog
--- a/browser/themes/osx/customizableui/panelUIOverlay.css
+++ b/browser/themes/osx/customizableui/panelUIOverlay.css
@@ -1,18 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 %include ../../shared/customizableui/panelUIOverlay.inc.css
 
-:root {
-  --panel-separator-color: hsla(210,4%,10%,.15);
-}
-
 .panel-subviews {
   background-color: hsla(0,0%,100%,.97);
 }
 
 .panelUI-grid .toolbarbutton-1 {
   margin-right: 0;
   margin-left: 0;
   margin-bottom: 0;
@@ -72,9 +68,9 @@ menu.subviewbutton > .menu-right > image
 
 .PanelUI-subView menuseparator,
 .cui-widget-panelview menuseparator {
   padding: 0 !important;
 }
 
 toolbarpaletteitem[place="palette"] > .toolbarbutton-1 > .toolbarbutton-menubutton-button {
   padding: 3px 1px;
-}
+}
\ No newline at end of file
--- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css
+++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css
@@ -16,17 +16,16 @@
 %define menuStateHover :not(:-moz-any([disabled],:active))[_moz-menuactive]
 %define buttonStateActive :not([disabled]):-moz-any([open],:hover:active)
 %define menuStateActive :not([disabled])[_moz-menuactive]:active
 %define menuStateMenuActive :not([disabled])[_moz-menuactive]
 
 %include ../browser.inc
 
 :root {
-  --panel-separator-color: ThreeDShadow;
   --panel-ui-exit-subview-gutter-width: 38px;
 }
 
 #PanelUI-popup #PanelUI-contents:empty {
   height: 128px;
 }
 
 #PanelUI-popup #PanelUI-contents:empty::before {
--- a/devtools/.eslintrc
+++ b/devtools/.eslintrc
@@ -28,17 +28,17 @@
   },
   "rules": {
     // These are the rules that have been configured so far to match the
     // devtools coding style.
 
     // Rules from the mozilla plugin
     "mozilla/mark-test-function-used": 1,
     "mozilla/no-aArgs": 1,
-    "mozilla/no-cpows-in-tests": 1,
+    "mozilla/no-cpows-in-tests": 2,
     // See bug 1224289.
     "mozilla/reject-importGlobalProperties": 1,
     "mozilla/var-only-at-top-level": 1,
 
     // Rules from the React plugin
     "react/display-name": 2,
     "react/no-danger": 2,
     "react/no-did-mount-set-state": 2,
--- a/devtools/bootstrap.js
+++ b/devtools/bootstrap.js
@@ -69,29 +69,64 @@ let getTopLevelWindow = function (window
                .QueryInterface(Ci.nsIInterfaceRequestor)
                .getInterface(Ci.nsIDOMWindow);
 };
 
 function reload(event) {
   // We automatically reload the toolbox if we are on a browser tab
   // with a toolbox already opened
   let top = getTopLevelWindow(event.view)
-  let isBrowser = top.location.href.includes("/browser.xul") && top.gDevToolsBrowser;
+  let isBrowser = top.location.href.includes("/browser.xul");
   let reloadToolbox = false;
-  if (isBrowser && top.gDevToolsBrowser.hasToolboxOpened) {
-    reloadToolbox = top.gDevToolsBrowser.hasToolboxOpened(top);
+  if (isBrowser && top.gBrowser) {
+    // We do not use any devtools code before the call to Loader.jsm reload as
+    // any attempt to use Loader.jsm to load a module will instanciate a new
+    // Loader.
+    let nbox = top.gBrowser.getNotificationBox();
+    reloadToolbox =
+      top.document.getAnonymousElementByAttribute(nbox, "class",
+        "devtools-toolbox-bottom-iframe") ||
+      top.document.getAnonymousElementByAttribute(nbox, "class",
+        "devtools-toolbox-side-iframe") ||
+      Services.wm.getMostRecentWindow("devtools:toolbox");
   }
+  let browserConsole = Services.wm.getMostRecentWindow("devtools:webconsole");
+  let reopenBrowserConsole = false;
+  if (browserConsole) {
+    browserConsole.close();
+    reopenBrowserConsole = true;
+  }
+
   dump("Reload DevTools.  (reload-toolbox:"+reloadToolbox+")\n");
 
   // Invalidate xul cache in order to see changes made to chrome:// files
   Services.obs.notifyObservers(null, "startupcache-invalidate", null);
 
-  // Ask the loader to update itself and reopen the toolbox if needed
+  // This frame script is going to be executed in all processes: parent and childs
+  Services.ppmm.loadProcessScript("data:,new " + function () {
+    /* Flush message manager cached frame scripts as well as chrome locales */
+    let obs = Components.classes["@mozilla.org/observer-service;1"]
+                        .getService(Components.interfaces.nsIObserverService);
+    obs.notifyObservers(null, "message-manager-flush-caches", null);
+
+    /* Also purge cached modules in child processes, we do it a few lines after
+       in the parent process */
+    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+      Services.obs.notifyObservers(null, "devtools-unload", "reload");
+    }
+  }, false);
+
+  // As we can't get a reference to existing Loader.jsm instances, we send them
+  // an observer service notification to unload them.
+  Services.obs.notifyObservers(null, "devtools-unload", "reload");
+
+  // Then spawn a brand new Loader.jsm instance and start the main module
+  Cu.unload("resource://devtools/shared/Loader.jsm");
   const {devtools} = Cu.import("resource://devtools/shared/Loader.jsm", {});
-  devtools.reload();
+  devtools.require("devtools/client/framework/devtools-browser");
 
   // Go over all top level windows to reload all devtools related things
   let windowsEnum = Services.wm.getEnumerator(null);
   while (windowsEnum.hasMoreElements()) {
     let window = windowsEnum.getNext();
     let windowtype = window.document.documentElement.getAttribute("windowtype");
     if (windowtype == "navigator:browser" && window.gBrowser) {
       // Enumerate tabs on firefox windows
@@ -109,25 +144,16 @@ function reload(event) {
           let isJSONView = content.document.baseURI.startsWith("resource://devtools/");
           if (isJSONView) {
             content.location.reload();
           }
         }, false);
       }
     } else if (windowtype === "devtools:webide") {
       window.location.reload();
-    } else if (windowtype === "devtools:webconsole") {
-      // Browser console document can't just be reloaded.
-      // HUDService is going to close it on unload.
-      // Instead we have to manually toggle it.
-      let HUDService = devtools.require("devtools/client/webconsole/hudservice");
-      HUDService.toggleBrowserConsole()
-        .then(() => {
-          HUDService.toggleBrowserConsole();
-        });
     }
   }
 
   if (reloadToolbox) {
     // Reopen the toolbox automatically if we are reloading from toolbox shortcut
     // and are on a browser window.
     // Wait for a second before opening the toolbox to avoid races
     // between the old and the new one.
@@ -135,16 +161,24 @@ function reload(event) {
     setTimeout(() => {
       let { TargetFactory } = devtools.require("devtools/client/framework/target");
       let { gDevTools } = devtools.require("devtools/client/framework/devtools");
       let target = TargetFactory.forTab(top.gBrowser.selectedTab);
       gDevTools.showToolbox(target);
     }, 1000);
   }
 
+  // Browser console document can't just be reloaded.
+  // HUDService is going to close it on unload.
+  // Instead we have to manually toggle it.
+  if (reopenBrowserConsole) {
+    let HUDService = devtools.require("devtools/client/webconsole/hudservice");
+    HUDService.toggleBrowserConsole();
+  }
+
   actionOccurred("reloadAddonReload");
 }
 
 let prefs = {
   // Enable dump as some errors are only printed on the stdout
   "browser.dom.window.dump.enabled": true,
   // Enable the browser toolbox and various chrome-only features
   "devtools.chrome.enabled": true,
--- a/devtools/client/aboutdebugging/components/addons-controls.js
+++ b/devtools/client/aboutdebugging/components/addons-controls.js
@@ -5,17 +5,17 @@
 /* eslint-env browser */
 /* globals AddonManager */
 
 "use strict";
 
 loader.lazyImporter(this, "AddonManager",
   "resource://gre/modules/AddonManager.jsm");
 
-const { Cc, Ci } = require("chrome");
+const { Cc, Ci, Cu } = require("chrome");
 const { createFactory, createClass, DOM: dom } =
   require("devtools/client/shared/vendor/react");
 const Services = require("Services");
 
 const AddonsInstallError = createFactory(require("./addons-install-error"));
 
 const Strings = Services.strings.createBundle(
   "chrome://devtools/locale/aboutdebugging.properties");
@@ -52,16 +52,17 @@ module.exports = createClass({
     // AddonManager.installTemporaryAddon accepts either
     // addon directory or final xpi file.
     if (!file.isDirectory() && !file.leafName.endsWith(".xpi")) {
       file = file.parent;
     }
 
     AddonManager.installTemporaryAddon(file)
       .catch(e => {
+        Cu.reportError(e);
         this.setState({ installError: e.message });
       });
   },
 
   render() {
     let { debugDisabled } = this.props;
 
     return dom.div({ className: "addons-top" },
--- a/devtools/client/debugger/test/mochitest/browser_dbg_chrome-debugging.js
+++ b/devtools/client/debugger/test/mochitest/browser_dbg_chrome-debugging.js
@@ -12,18 +12,17 @@ const TAB_URL = EXAMPLE_URL + "doc_inlin
 var gClient, gThreadClient;
 var gAttached = promise.defer();
 var gNewGlobal = promise.defer()
 var gNewChromeSource = promise.defer()
 
 var { DevToolsLoader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 var loader = new DevToolsLoader();
 loader.invisibleToDebugger = true;
-loader.main("devtools/server/main");
-var DebuggerServer = loader.DebuggerServer;
+var { DebuggerServer } = loader.require("devtools/server/main");
 
 function test() {
   if (!DebuggerServer.initialized) {
     DebuggerServer.init();
     DebuggerServer.addBrowserActors();
   }
   DebuggerServer.allowChromeProcess = true;
 
--- a/devtools/client/devtools-startup.js
+++ b/devtools/client/devtools-startup.js
@@ -65,17 +65,17 @@ DevToolsStartup.prototype = {
     Services.obs.addObserver(onStartup, "browser-delayed-startup-finished",
                              false);
   },
 
   initDevTools: function() {
     let { loader } = Cu.import("resource://devtools/shared/Loader.jsm", {});
     // Ensure loading main devtools module that hooks up into browser UI
     // and initialize all devtools machinery.
-    loader.main("devtools/client/main");
+    loader.require("devtools/client/framework/devtools-browser");
   },
 
   handleConsoleFlag: function(cmdLine) {
     let window = Services.wm.getMostRecentWindow("devtools:webconsole");
     if (!window) {
       this.initDevTools();
 
       let { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
@@ -149,18 +149,18 @@ DevToolsStartup.prototype = {
       // Create a separate loader instance, so that we can be sure to receive
       // a separate instance of the DebuggingServer from the rest of the
       // devtools.  This allows us to safely use the tools against even the
       // actors and DebuggingServer itself, especially since we can mark
       // serverLoader as invisible to the debugger (unlike the usual loader
       // settings).
       let serverLoader = new DevToolsLoader();
       serverLoader.invisibleToDebugger = true;
-      serverLoader.main("devtools/server/main");
-      let debuggerServer = serverLoader.DebuggerServer;
+      let { DebuggerServer: debuggerServer } =
+        serverLoader.require("devtools/server/main");
       debuggerServer.init();
       debuggerServer.addBrowserActors();
       debuggerServer.allowChromeProcess = true;
 
       let listener = debuggerServer.createListener();
       listener.portOrPath = portOrPath;
       listener.open();
       dump("Started debugger server on " + portOrPath + "\n");
--- a/devtools/client/framework/ToolboxProcess.jsm
+++ b/devtools/client/framework/ToolboxProcess.jsm
@@ -122,18 +122,18 @@ BrowserToolboxProcess.prototype = {
 
     // Create a separate loader instance, so that we can be sure to receive a
     // separate instance of the DebuggingServer from the rest of the devtools.
     // This allows us to safely use the tools against even the actors and
     // DebuggingServer itself, especially since we can mark this loader as
     // invisible to the debugger (unlike the usual loader settings).
     this.loader = new DevToolsLoader();
     this.loader.invisibleToDebugger = true;
-    this.loader.main("devtools/server/main");
-    this.debuggerServer = this.loader.DebuggerServer;
+    let { DebuggerServer } = this.loader.require("devtools/server/main");
+    this.debuggerServer = DebuggerServer;
     dumpn("Created a separate loader instance for the DebuggerServer.");
 
     // Forward interesting events.
     this.debuggerServer.on("connectionchange", this.emit.bind(this));
 
     this.debuggerServer.init();
     this.debuggerServer.addBrowserActors();
     this.debuggerServer.allowChromeProcess = true;
--- a/devtools/client/framework/toolbox-process-window.js
+++ b/devtools/client/framework/toolbox-process-window.js
@@ -5,17 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 var { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 var { loader, require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
 // Require this module to setup core modules
-loader.main("devtools/client/main");
+loader.require("devtools/client/framework/devtools-browser");
 
 var { gDevTools } = require("devtools/client/framework/devtools");
 var { TargetFactory } = require("devtools/client/framework/target");
 var { Toolbox } = require("devtools/client/framework/toolbox");
 var Services = require("Services");
 var { DebuggerClient } = require("devtools/shared/client/main");
 var { PrefsHelper } = require("devtools/client/shared/prefs");
 var { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -1,13 +1,14 @@
 [DEFAULT]
 tags = devtools
 subsuite = devtools
 support-files =
   doc_author-sheet.html
+  doc_blob_stylesheet.html
   doc_content_stylesheet.html
   doc_content_stylesheet_imported.css
   doc_content_stylesheet_imported2.css
   doc_content_stylesheet_linked.css
   doc_content_stylesheet_script.css
   doc_copystyles.css
   doc_copystyles.html
   doc_cssom.html
@@ -50,16 +51,17 @@ support-files =
 [browser_rules_add-rule_02.js]
 [browser_rules_add-rule_03.js]
 [browser_rules_add-rule_04.js]
 [browser_rules_add-rule_05.js]
 [browser_rules_add-rule_pseudo_class.js]
 [browser_rules_authored.js]
 [browser_rules_authored_color.js]
 [browser_rules_authored_override.js]
+[browser_rules_blob_stylesheet.js]
 [browser_rules_colorpicker-and-image-tooltip_01.js]
 [browser_rules_colorpicker-and-image-tooltip_02.js]
 [browser_rules_colorpicker-appears-on-swatch-click.js]
 [browser_rules_colorpicker-commit-on-ENTER.js]
 [browser_rules_colorpicker-edit-gradient.js]
 [browser_rules_colorpicker-hides-on-tooltip.js]
 [browser_rules_colorpicker-multiple-changes.js]
 [browser_rules_colorpicker-release-outside-frame.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_blob_stylesheet.js
@@ -0,0 +1,20 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the rule-view content is correct for stylesheet generated
+// with createObjectURL(cssBlob)
+const TEST_URL = URL_ROOT + "doc_blob_stylesheet.html";
+
+add_task(function* () {
+  yield addTab(TEST_URL);
+  let {inspector, view} = yield openRuleView();
+
+  yield selectNode("h1", inspector);
+  is(view.element.querySelectorAll("#noResults").length, 0,
+    "The no-results element is not displayed");
+
+  is(view.element.querySelectorAll(".ruleview-rule").length, 2,
+    "There are 2 displayed rules");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_blob_stylesheet.html
@@ -0,0 +1,39 @@
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!doctype html>
+</html>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Blob stylesheet sourcemap</title>
+</head>
+<body>
+<h1>Test</h1>
+<script>
+"use strict";
+
+var cssContent = `body {
+  background-color: black;
+}
+body > h1 {
+  color: white;
+}
+` +
+"/*# sourceMappingURL=data:application/json;base64,ewoidmVyc2lvbiI6IDMsCiJtYX" +
+"BwaW5ncyI6ICJBQUFBLElBQUs7RUFDSCxnQkFBZ0IsRUFBRSxLQUFLOztBQUN2QixTQUFPO0VBQ0" +
+"wsS0FBSyxFQUFFLEtBQUsiLAoic291cmNlcyI6IFsidGVzdC5zY3NzIl0sCiJzb3VyY2VzQ29udG" +
+"VudCI6IFsiYm9keSB7XG4gIGJhY2tncm91bmQtY29sb3I6IGJsYWNrO1xuICAmID4gaDEge1xuIC" +
+"AgIGNvbG9yOiB3aGl0ZTsgIFxuICB9XG59XG4iXSwKIm5hbWVzIjogW10sCiJmaWxlIjogInRlc3" +
+"QuY3NzIgp9Cg== */";
+var cssBlob = new Blob([cssContent], {type: "text/css"});
+var url = URL.createObjectURL(cssBlob);
+
+var head = document.querySelector("head");
+var link = document.createElement("link");
+link.rel = "stylesheet";
+link.type = "text/css";
+link.href = url;
+head.appendChild(link);
+</script>
+</body>
+</html>
--- a/devtools/client/locales/en-US/memory.properties
+++ b/devtools/client/locales/en-US/memory.properties
@@ -60,23 +60,26 @@ checkbox.recordAllocationStacks.tooltip=
 # LOCALIZATION NOTE (toolbar.displayBy): The label describing the select menu
 # options of the display options.
 toolbar.displayBy=Group by:
 
 # LOCALIZATION NOTE (toolbar.displayBy.tooltip): The tooltip for the label
 # describing the select menu options of the display options.
 toolbar.displayBy.tooltip=Change how objects are grouped
 
-# TODO FITZGEN
+# LOCALIZATION NOTE (toolbar.pop-view): The text in the button to go back to the
+# previous view.
 toolbar.pop-view=←
 
-# TODO FITZGEN
+# LOCALIZATION NOTE (toolbar.pop-view.label): The text for the label for the
+# button to go back to the previous view.
 toolbar.pop-view.label=Go back to aggregates
 
-# TODO FITZGEN
+# LOCALIZATION NOTE (toolbar.viewing-individuals): The text letting the user
+# know that they are viewing individual nodes from a census group.
 toolbar.viewing-individuals=⁂ Viewing individuals in group
 
 # LOCALIZATION NOTE (censusDisplays.coarseType.tooltip): The tooltip for the
 # "coarse type" display option.
 censusDisplays.coarseType.tooltip=Group items by their type
 
 # LOCALIZATION NOTE (censusDisplays.allocationStack.tooltip): The tooltip for
 # the "allocation stack" display option.
@@ -173,17 +176,18 @@ diff-snapshots.tooltip=Compare snapshots
 # LOCALIZATION NOTE (filter.placeholder): The placeholder text used for the
 # memory tool's filter search box.
 filter.placeholder=Filter
 
 # LOCALIZATION NOTE (filter.tooltip): The tooltip text used for the memory
 # tool's filter search box.
 filter.tooltip=Filter the contents of the heap snapshot
 
-# TODO FITZGEN
+# LOCALIZATION NOTE (tree-item.view-individuals.tooltip): The tooltip for the
+# button to view individuals in this group.
 tree-item.view-individuals.tooltip=View individual nodes in this group and their retaining paths
 
 # LOCALIZATION NOTE (tree-item.load-more): The label for the links to fetch the
 # lazily loaded sub trees in the dominator tree view.
 tree-item.load-more=Load more…
 
 # LOCALIZATION NOTE (tree-item.rootlist): The label for the root of the
 # dominator tree.
@@ -302,32 +306,38 @@ snapshot.state.saving-census.full=Saving census…
 # LOCALIZATION NOTE (snapshot.state.saving-tree-map.full): The label describing
 # the snapshot state SAVING, used in the main heap view.
 snapshot.state.saving-tree-map.full=Saving tree map…
 
 # LOCALIZATION NOTE (snapshot.state.error.full): The label describing the
 # snapshot state ERROR, used in the main heap view.
 snapshot.state.error.full=There was an error processing this snapshot.
 
-# TODO FITZGEN
+# LOCALIZATION NOTE (individuals.state.error): The short message displayed when
+# there is an error fetching individuals from a group.
 individuals.state.error=Error
 
-# TODO FITZGEN
+# LOCALIZATION NOTE (individuals.state.error.full): The longer message displayed
+# when there is an error fetching individuals from a group.
 individuals.state.error.full=There was an error while fetching individuals in the group
 
-# TODO FITZGEN
+# LOCALIZATION NOTE (individuals.state.fetching): The short message displayed
+# while fetching individuals.
 individuals.state.fetching=Fetching…
 
-# TODO FITZGEN
+# LOCALIZATION NOTE (individuals.state.fetching.full): The longer message
+# displayed while fetching individuals.
 individuals.state.fetching.full=Fetching individuals in group…
 
-# TODO FITZGEN
+# LOCALIZATION NOTE (individuals.field.node): The header label for an individual
+# node.
 individuals.field.node=Node
 
-# TODO FITZGEN
+# LOCALIZATION NOTE (individuals.field.node.tooltip): The tooltip for the header
+# label for an individual node.
 individuals.field.node.tooltip=The individual node in the snapshot
 
 # LOCALIZATION NOTE (snapshot.state.saving): The label describing the snapshot
 # state SAVING, used in the snapshot list view
 snapshot.state.saving=Saving snapshot…
 
 # LOCALIZATION NOTE (snapshot.state.importing): The label describing the
 # snapshot state IMPORTING, used in the snapshot list view
deleted file mode 100644
--- a/devtools/client/main.js
+++ /dev/null
@@ -1,22 +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/. */
-
-"use strict";
-
-/**
- * This module could have been devtools-browser.js.
- * But we need this wrapper in order to define precisely what we are exporting
- * out of client module loader (Loader.jsm): only Toolbox and TargetFactory.
- */
-
-// For compatiblity reasons, exposes these symbols on "devtools":
-Object.defineProperty(exports, "Toolbox", {
-  get: () => require("devtools/client/framework/toolbox").Toolbox
-});
-Object.defineProperty(exports, "TargetFactory", {
-  get: () => require("devtools/client/framework/target").TargetFactory
-});
-
-// Load the main browser module
-require("devtools/client/framework/devtools-browser");
--- a/devtools/client/memory/actions/census-display.js
+++ b/devtools/client/memory/actions/census-display.js
@@ -4,17 +4,16 @@
 "use strict";
 
 const { assert } = require("devtools/shared/DevToolsUtils");
 const { actions } = require("../constants");
 const { refresh } = require("./refresh");
 
 exports.setCensusDisplayAndRefresh = function(heapWorker, display) {
   return function*(dispatch, getState) {
-    console.log("FITZGEN: setCensusDisplayAndRefresh", display);
     dispatch(setCensusDisplay(display));
     yield dispatch(refresh(heapWorker));
   };
 };
 
 /**
  * Clears out all cached census data in the snapshots and sets new display data
  * for censuses.
--- a/devtools/client/memory/actions/refresh.js
+++ b/devtools/client/memory/actions/refresh.js
@@ -9,17 +9,16 @@ const { refreshDiffing } = require("./di
 const snapshot = require("./snapshot");
 
 /**
  * Refresh the main thread's data from the heap analyses worker, if needed.
  *
  * @param {HeapAnalysesWorker} heapWorker
  */
 exports.refresh = function (heapWorker) {
-  console.log("FITZGEN: refresh");
   return function* (dispatch, getState) {
     switch (getState().view.state) {
       case viewState.DIFFING:
         assert(getState().diffing, "Should have diffing state if in diffing view");
         yield dispatch(refreshDiffing(heapWorker));
         return;
 
       case viewState.CENSUS:
--- a/devtools/client/memory/actions/snapshot.js
+++ b/devtools/client/memory/actions/snapshot.js
@@ -135,37 +135,33 @@ const takeSnapshot = exports.takeSnapsho
  */
 const readSnapshot = exports.readSnapshot =
 TaskCache.declareCacheableTask({
   getCacheKey(_, id) {
     return id;
   },
 
   task: function*(heapWorker, id, removeFromCache, dispatch, getState) {
-    console.log("FITZGEN: readSnapshot");
-
     const snapshot = getSnapshot(getState(), id);
     assert([states.SAVED, states.IMPORTING].includes(snapshot.state),
            `Should only read a snapshot once. Found snapshot in state ${snapshot.state}`);
 
     let creationTime;
 
     dispatch({ type: actions.READ_SNAPSHOT_START, id });
     try {
       yield heapWorker.readHeapSnapshot(snapshot.path);
       creationTime = yield heapWorker.getCreationTime(snapshot.path);
     } catch (error) {
-      console.log("FITZGEN: readSnapshot: error", error);
       removeFromCache();
       reportException("readSnapshot", error);
       dispatch({ type: actions.SNAPSHOT_ERROR, id, error });
       return;
     }
 
-    console.log("FITZGEN: readSnapshot: done reading");
     removeFromCache();
     dispatch({ type: actions.READ_SNAPSHOT_END, id, creationTime });
   }
 });
 
 let takeCensusTaskCounter = 0;
 
 /**
@@ -191,47 +187,43 @@ function makeTakeCensusTask({ getDisplay
    */
   let thisTakeCensusTaskId = ++takeCensusTaskCounter;
   return TaskCache.declareCacheableTask({
     getCacheKey(_, id) {
       return `take-census-task-${thisTakeCensusTaskId}-${id}`;
     },
 
     task: function*(heapWorker, id, removeFromCache, dispatch, getState) {
-      console.log("FITZGEN: takeCensus");
       const snapshot = getSnapshot(getState(), id);
       if (!snapshot) {
-        console.log("FITZGEN:     no snapshot");
         removeFromCache();
         return;
       }
 
       // Assert that snapshot is in a valid state
       assert(canTakeCensus(snapshot),
              `Attempting to take a census when the snapshot is not in a ready state. snapshot.state = ${snapshot.state}, census.state = ${(getCensus(snapshot) || { state: null }).state}`);
 
       let report, parentMap;
       let display = getDisplay(getState());
       let filter = getFilter(getState());
 
       // If display, filter and inversion haven't changed, don't do anything.
       if (censusIsUpToDate(filter, display, getCensus(snapshot))) {
-        console.log("FITZGEN:     census is up to date");
         removeFromCache();
         return;
       }
 
       // Keep taking a census if the display changes while our request is in
       // flight. Recheck that the display used for the census is the same as the
       // state's display.
       do {
         display = getDisplay(getState());
         filter = getState().filter;
 
-        console.log("FITZGEN:     taking census with display =", display.displayName);
         dispatch({
           type: beginAction,
           id,
           filter,
           display
         });
 
         let opts = display.inverted
@@ -241,28 +233,25 @@ function makeTakeCensusTask({ getDisplay
         opts.filter = filter || null;
 
         try {
           ({ report, parentMap } = yield heapWorker.takeCensus(
             snapshot.path,
             { breakdown: display.breakdown },
             opts));
         } catch (error) {
-          console.log("FITZGEN:     error taking census: " + error + "\n" + error.stack);
           removeFromCache();
           reportException("takeCensus", error);
           dispatch({ type: errorAction, id, error });
           return;
         }
       }
       while (filter !== getState().filter ||
              display !== getDisplay(getState()));
 
-      console.log("FITZGEN:     done taking census");
-
       removeFromCache();
       dispatch({
         type: endAction,
         id,
         display,
         filter,
         report,
         parentMap
@@ -460,33 +449,30 @@ const refreshIndividuals = exports.refre
 
 /**
  * Refresh the selected snapshot's census data, if need be (for example,
  * display configuration changed).
  *
  * @param {HeapAnalysesClient} heapWorker
  */
 const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWorker) {
-  console.log("FITZGEN: refreshSelectedCensus");
   return function*(dispatch, getState) {
     let snapshot = getState().snapshots.find(s => s.selected);
     if (!snapshot || snapshot.state !== states.READ) {
-      console.log("FITZGEN:     nothing to do");
       return;
     }
 
     // Intermediate snapshot states will get handled by the task action that is
     // orchestrating them. For example, if the snapshot census's state is
     // SAVING, then the takeCensus action will keep taking a census until
     // the inverted property matches the inverted state. If the snapshot is
     // still in the process of being saved or read, the takeSnapshotAndCensus
     // task action will follow through and ensure that a census is taken.
     if ((snapshot.census && snapshot.census.state === censusState.SAVED) ||
         !snapshot.census) {
-      console.log("FITZGEN:     taking census");
       yield dispatch(takeCensus(heapWorker, snapshot.id));
     }
   };
 };
 
 /**
  * Refresh the selected snapshot's tree map data, if need be (for example,
  * display configuration changed).
--- a/devtools/client/memory/actions/task-cache.js
+++ b/devtools/client/memory/actions/task-cache.js
@@ -40,17 +40,16 @@ const TaskCache = module.exports = class
   }
 
   /**
    * Remove the cache entry with the given key.
    *
    * @param {Any} key
    */
   remove(key) {
-    console.log("FITZGEN: removing task from cache with keky =", key);
     assert(this._cache.has(key),
            `Should have an extant entry for key = ${key}`);
 
     this._cache.delete(key);
   }
 };
 
 /**
@@ -66,22 +65,19 @@ TaskCache.declareCacheableTask = functio
   const cache = new TaskCache();
 
   return function(...args) {
     return function*(dispatch, getState) {
       const key = getCacheKey(...args);
 
       const extantResult = cache.get(key);
       if (extantResult) {
-        console.log("FITZGEN: re-using task with cache key =", key);
         return extantResult;
       }
 
-      console.log("FITZGEN: creating new task with cache key =", key);
-
       // Ensure that we have our new entry in the cache *before* dispatching the
       // task!
       let resolve;
       cache.put(key, new Promise(r => {
         resolve = r;
       }));
 
       resolve(dispatch(function*() {
--- a/devtools/client/moz.build
+++ b/devtools/client/moz.build
@@ -46,11 +46,10 @@ EXTRA_COMPONENTS += [
     'devtools-startup.js',
     'devtools-startup.manifest',
 ]
 
 JAR_MANIFESTS += ['jar.mn']
 
 DevToolsModules(
     'definitions.js',
-    'main.js',
     'menus.js',
 )
--- a/devtools/client/responsive.html/test/browser/browser_viewport_basics.js
+++ b/devtools/client/responsive.html/test/browser/browser_viewport_basics.js
@@ -19,12 +19,12 @@ addRDMTask(TEST_URL, function* ({ ui }) 
   is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("width"),
      "320px", "Viewport has default width");
   is(ui.toolWindow.getComputedStyle(viewport).getPropertyValue("height"),
      "480px", "Viewport has default height");
 
   // Browser's location should match original tab
   yield waitForFrameLoad(ui, TEST_URL);
   let location = yield spawnViewportTask(ui, {}, function* () {
-    return content.location.href;
+    return content.location.href; // eslint-disable-line
   });
   is(location, TEST_URL, "Viewport location matches");
 });
--- a/devtools/client/webconsole/test/browser_webconsole_view_source.js
+++ b/devtools/client/webconsole/test/browser_webconsole_view_source.js
@@ -11,25 +11,28 @@
 const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
                  "test/test-error.html";
 
 add_task(function*() {
   yield loadTab(TEST_URI);
   let hud = yield openConsole(null);
   info("console opened");
 
-  let button = content.document.querySelector("button");
-  ok(button, "we have the button on the page");
 
   // On e10s, the exception is triggered in child process
   // and is ignored by test harness
   if (!Services.appinfo.browserTabsRemoteAutostart) {
     expectUncaughtException();
   }
-  EventUtils.sendMouseEvent({ type: "click" }, button, content);
+
+  ContentTask.spawn(gBrowser.selectedBrowser, {}, function*() {
+    let button = content.document.querySelector("button");
+    ok(button, "we have the button on the page");
+    button.click();
+  });
 
   let [result] = yield waitForMessages({
     webconsole: hud,
     messages: [{
       text: "fooBazBaz is not defined",
       category: CATEGORY_JS,
       severity: SEVERITY_ERROR,
     }],
--- a/devtools/server/actors/script.js
+++ b/devtools/server/actors/script.js
@@ -1952,59 +1952,76 @@ const ThreadActor = ActorClass({
 
     // The scripts must be added to the ScriptStore before restoring
     // breakpoints. If we try to add them to the ScriptStore any later, we can
     // accidentally set a breakpoint in a top level script as a "closest match"
     // because we wouldn't have added the child scripts to the ScriptStore yet.
     this.scripts.addScripts(this.dbg.findScripts({ source: aSource }));
 
     let sourceActor = this.sources.createNonSourceMappedActor(aSource);
+    let bpActors = [...this.breakpointActorMap.findActors()];
 
-    // Set any stored breakpoints.
-    let bpActors = [...this.breakpointActorMap.findActors()];
-    let promises = [];
+    if (this._options.useSourceMaps) {
+      let promises = [];
 
-    // Go ahead and establish the source actors for this script, which
-    // fetches sourcemaps if available and sends onNewSource
-    // notifications.
-    let sourceActorsCreated = this.sources.createSourceActors(aSource);
+      // Go ahead and establish the source actors for this script, which
+      // fetches sourcemaps if available and sends onNewSource
+      // notifications.
+      let sourceActorsCreated = this.sources._createSourceMappedActors(aSource);
 
-    if (bpActors.length) {
-      // We need to use unsafeSynchronize here because if the page is being reloaded,
-      // this call will replace the previous set of source actors for this source
-      // with a new one. If the source actors have not been replaced by the time
-      // we try to reset the breakpoints below, their location objects will still
-      // point to the old set of source actors, which point to different
-      // scripts.
-      this.unsafeSynchronize(sourceActorsCreated);
-    }
+      if (bpActors.length) {
+        // We need to use unsafeSynchronize here because if the page is being reloaded,
+        // this call will replace the previous set of source actors for this source
+        // with a new one. If the source actors have not been replaced by the time
+        // we try to reset the breakpoints below, their location objects will still
+        // point to the old set of source actors, which point to different
+        // scripts.
+        this.unsafeSynchronize(sourceActorsCreated);
+      }
+
+      for (let _actor of bpActors) {
+        // XXX bug 1142115: We do async work in here, so we need to create a fresh
+        // binding because for/of does not yet do that in SpiderMonkey.
+        let actor = _actor;
 
-    for (let _actor of bpActors) {
-      // XXX bug 1142115: We do async work in here, so we need to create a fresh
-      // binding because for/of does not yet do that in SpiderMonkey.
-      let actor = _actor;
+        if (actor.isPending) {
+          promises.push(actor.originalLocation.originalSourceActor._setBreakpoint(actor));
+        } else {
+          promises.push(this.sources.getAllGeneratedLocations(actor.originalLocation)
+                                    .then((generatedLocations) => {
+            if (generatedLocations.length > 0 &&
+                generatedLocations[0].generatedSourceActor.actorID === sourceActor.actorID) {
+              sourceActor._setBreakpointAtAllGeneratedLocations(
+                actor,
+                generatedLocations
+              );
+            }
+          }));
+        }
+      }
 
-      if (actor.isPending) {
-        promises.push(actor.originalLocation.originalSourceActor._setBreakpoint(actor));
-      } else {
-        promises.push(this.sources.getAllGeneratedLocations(actor.originalLocation)
-                                  .then((generatedLocations) => {
-          if (generatedLocations.length > 0 &&
-              generatedLocations[0].generatedSourceActor.actorID === sourceActor.actorID) {
-            sourceActor._setBreakpointAtAllGeneratedLocations(
-              actor,
-              generatedLocations
-            );
-          }
-        }));
+      if (promises.length > 0) {
+        this.unsafeSynchronize(promise.all(promises));
       }
-    }
-
-    if (promises.length > 0) {
-      this.unsafeSynchronize(promise.all(promises));
+    } else {
+      // Bug 1225160: If addSource is called in response to a new script
+      // notification, and this notification was triggered by loading a JSM from
+      // chrome code, calling unsafeSynchronize could cause a debuggee timer to
+      // fire. If this causes the JSM to be loaded a second time, the browser
+      // will crash, because loading JSMS is not reentrant, and the first load
+      // has not completed yet.
+      //
+      // The root of the problem is that unsafeSynchronize can cause debuggee
+      // code to run. Unfortunately, fixing that is prohibitively difficult. The
+      // best we can do at the moment is disable source maps for the browser
+      // debugger, and carefully avoid the use of unsafeSynchronize in this
+      // function when source maps are disabled.
+      for (let actor of bpActors) {
+        actor.originalLocation.originalSourceActor._setBreakpoint(actor);
+      }
     }
 
     this._debuggerSourcesSeen.add(aSource);
     return true;
   },
 
 
   /**
--- a/devtools/server/actors/stylesheets.js
+++ b/devtools/server/actors/stylesheets.js
@@ -798,16 +798,19 @@ var StyleSheetActor = protocol.ActorClas
     }
     this._originalSources = null;
   },
 
   /**
    * Sets the source map's sourceRoot to be relative to the source map url.
    */
   _setSourceMapRoot: function(aSourceMap, aAbsSourceMapURL, aScriptURL) {
+    if (aScriptURL.startsWith("blob:")) {
+      aScriptURL = aScriptURL.replace("blob:", "");
+    }
     const base = dirname(
       aAbsSourceMapURL.startsWith("data:")
         ? aScriptURL
         : aAbsSourceMapURL);
     aSourceMap.sourceRoot = aSourceMap.sourceRoot
       ? normalize(aSourceMap.sourceRoot, base)
       : base;
   },
--- a/devtools/server/actors/utils/TabSources.js
+++ b/devtools/server/actors/utils/TabSources.js
@@ -324,17 +324,23 @@ TabSources.prototype = {
               // it's an xml page.
               spec.isInlineSource = true;
             }
             else if (extension === "js") {
               spec.contentType = "text/javascript";
             }
           } catch (e) {
             // This only needs to be here because URL is not yet exposed to
-            // workers.
+            // workers. (BUG 1258892)
+            const filename = url;
+            const index = filename.lastIndexOf(".");
+            const extension = index >= 0 ? filename.slice(index + 1) : "";
+            if (extension === "js") {
+              spec.contentType = "text/javascript";
+            }
           }
         }
       }
       else {
         // Assume the content is javascript if there's no URL
         spec.contentType = "text/javascript";
       }
     }
--- a/devtools/server/content-server.jsm
+++ b/devtools/server/content-server.jsm
@@ -14,18 +14,17 @@ const { DevToolsLoader } = Cu.import("re
 this.EXPORTED_SYMBOLS = ["init"];
 
 function init(msg) {
   // Init a custom, invisible DebuggerServer, in order to not pollute
   // the debugger with all devtools modules, nor break the debugger itself with using it
   // in the same process.
   let devtools = new DevToolsLoader();
   devtools.invisibleToDebugger = true;
-  devtools.main("devtools/server/main");
-  let { DebuggerServer, ActorPool } = devtools;
+  let { DebuggerServer, ActorPool } = devtools.require("devtools/server/main");
 
   if (!DebuggerServer.initialized) {
     DebuggerServer.init();
     DebuggerServer.isInChildProcess = true;
   }
 
   // In case of apps being loaded in parent process, DebuggerServer is already
   // initialized, but child specific actors are not registered.
--- a/devtools/shared/Loader.jsm
+++ b/devtools/shared/Loader.jsm
@@ -1,28 +1,23 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-/* globals NetUtil, FileUtils, OS */
 
 "use strict";
 
 /**
  * Manages the addon-sdk loader instance used to load the developer tools.
  */
 
 var { Constructor: CC, classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
-XPCOMUtils.defineLazyModuleGetter(this, "NetUtil", "resource://gre/modules/NetUtil.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
-
 var { Loader } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
 var promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 
 this.EXPORTED_SYMBOLS = ["DevToolsLoader", "devtools", "BuiltinProvider",
                          "require", "loader"];
 
 /**
  * Providers are different strategies for loading the devtools.
@@ -130,29 +125,28 @@ BuiltinProvider.prototype = {
     Loader.unload(this.loader, reason);
     delete this.loader;
   },
 };
 
 var gNextLoaderID = 0;
 
 /**
- * The main devtools API.
- * In addition to a few loader-related details, this object will also include all
- * exports from the main module.  The standard instance of this loader is
- * exported as |devtools| below, but if a fresh copy of the loader is needed,
- * then a new one can also be created.
+ * The main devtools API. The standard instance of this loader is exported as
+ * |devtools| below, but if a fresh copy of the loader is needed, then a new
+ * one can also be created.
  */
 this.DevToolsLoader = function DevToolsLoader() {
   this.require = this.require.bind(this);
   this.lazyGetter = XPCOMUtils.defineLazyGetter.bind(XPCOMUtils);
   this.lazyImporter = XPCOMUtils.defineLazyModuleGetter.bind(XPCOMUtils);
   this.lazyServiceGetter = XPCOMUtils.defineLazyServiceGetter.bind(XPCOMUtils);
   this.lazyRequireGetter = this.lazyRequireGetter.bind(this);
-  this.main = this.main.bind(this);
+
+  Services.obs.addObserver(this, "devtools-unload", false);
 };
 
 DevToolsLoader.prototype = {
   get provider() {
     if (!this._provider) {
       this._loadProvider();
     }
     return this._provider;
@@ -214,56 +208,16 @@ DevToolsLoader.prototype = {
         return value;
       },
       configurable: true,
       enumerable: true
     });
   },
 
   /**
-   * Add a URI to the loader.
-   * @param string id
-   *    The module id that can be used within the loader to refer to this module.
-   * @param string uri
-   *    The URI to load as a module.
-   * @returns The module's exports.
-   */
-  loadURI: function(id, uri) {
-    let module = Loader.Module(id, uri);
-    return Loader.load(this.provider.loader, module).exports;
-  },
-
-  /**
-   * Let the loader know the ID of the main module to load.
-   *
-   * The loader doesn't need a main module, but it's nice to have.  This
-   * will be called by the browser devtools to load the devtools/main module.
-   *
-   * When only using the server, there's no main module, and this method
-   * can be ignored.
-   */
-  main: function(id) {
-    // Ensure the main module isn't loaded twice, because it may have observable
-    // side-effects.
-    if (this._mainid) {
-      return;
-    }
-    this._mainid = id;
-    this._main = Loader.main(this.provider.loader, id);
-
-    // Mirror the main module's exports on this object.
-    Object.getOwnPropertyNames(this._main).forEach(key => {
-      XPCOMUtils.defineLazyGetter(this, key, () => this._main[key]);
-    });
-
-    var events = this.require("sdk/system/events");
-    events.emit("devtools-loaded", {});
-  },
-
-  /**
    * Override the provider used to load the tools.
    */
   setProvider: function(provider) {
     if (provider === this._provider) {
       return;
     }
 
     if (this._provider) {
@@ -281,18 +235,17 @@ DevToolsLoader.prototype = {
       atob: atob,
       btoa: btoa,
       _Iterator: Iterator,
       loader: {
         lazyGetter: this.lazyGetter,
         lazyImporter: this.lazyImporter,
         lazyServiceGetter: this.lazyServiceGetter,
         lazyRequireGetter: this.lazyRequireGetter,
-        id: this.id,
-        main: this.main
+        id: this.id
       },
       // Make sure `define` function exists.  This allows defining some modules
       // in AMD format while retaining CommonJS compatibility through this hook.
       // JSON Viewer needs modules in AMD format, as it currently uses RequireJS
       // from a content document and can't access our usual loaders.  So, any
       // modules shared with the JSON Viewer should include a define wrapper:
       //
       //   // Make this available to both AMD and CJS environments
@@ -318,28 +271,31 @@ DevToolsLoader.prototype = {
   /**
    * Choose a default tools provider based on the preferences.
    */
   _loadProvider: function() {
     this.setProvider(new BuiltinProvider());
   },
 
   /**
-   * Reload the current provider.
+   * Handles "devtools-unload" event
+   *
+   * @param String data
+   *    reason passed to modules when unloaded
    */
-  reload: function() {
-    var events = this.require("sdk/system/events");
-    events.emit("startupcache-invalidate", {});
+  observe: function(subject, topic, data) {
+    if (topic != "devtools-unload") {
+      return;
+    }
+    Services.obs.removeObserver(this, "devtools-unload");
 
-    this._provider.unload("reload");
-    delete this._provider;
-    let mainid = this._mainid;
-    delete this._mainid;
-    this._loadProvider();
-    this.main(mainid);
+    if (this._provider) {
+      this._provider.unload(data);
+      delete this._provider;
+    }
   },
 
   /**
    * Sets whether the compartments loaded by this instance should be invisible
    * to the debugger.  Invisibility is needed for loaders that support debugging
    * of chrome code.  This is true of remote target environments, like Fennec or
    * B2G.  It is not the default case for desktop Firefox because we offer the
    * Browser Toolbox for chrome debugging there, which uses its own, separate
@@ -348,8 +304,16 @@ DevToolsLoader.prototype = {
    */
   invisibleToDebugger: Services.appinfo.name !== "Firefox"
 };
 
 // Export the standard instance of DevToolsLoader used by the tools.
 this.devtools = this.loader = new DevToolsLoader();
 
 this.require = this.devtools.require.bind(this.devtools);
+
+// For compatibility reasons, expose these symbols on "devtools":
+Object.defineProperty(this.devtools, "Toolbox", {
+  get: () => this.require("devtools/client/framework/toolbox").Toolbox
+});
+Object.defineProperty(this.devtools, "TargetFactory", {
+  get: () => this.require("devtools/client/framework/target").TargetFactory
+});
--- a/devtools/shared/gcli/commands/listen.js
+++ b/devtools/shared/gcli/commands/listen.js
@@ -20,18 +20,17 @@ XPCOMUtils.defineLazyGetter(this, "debug
   // Create a separate loader instance, so that we can be sure to receive
   // a separate instance of the DebuggingServer from the rest of the
   // devtools.  This allows us to safely use the tools against even the
   // actors and DebuggingServer itself, especially since we can mark
   // serverLoader as invisible to the debugger (unlike the usual loader
   // settings).
   let serverLoader = new DevToolsLoader();
   serverLoader.invisibleToDebugger = true;
-  serverLoader.main("devtools/server/main");
-  let debuggerServer = serverLoader.DebuggerServer;
+  let { DebuggerServer: debuggerServer } = serverLoader.require("devtools/server/main");
   debuggerServer.init();
   debuggerServer.addBrowserActors();
   debuggerServer.allowChromeProcess = !l10n.hiddenByChromePref();
   return debuggerServer;
 });
 
 exports.items = [
   {
--- a/dom/push/PushService.jsm
+++ b/dom/push/PushService.jsm
@@ -1068,17 +1068,17 @@ this.PushService = {
    * Exceptions thrown in _onRegisterError are caught by the promise obtained
    * from _service.request, causing the promise to be rejected instead.
    */
   _onRegisterError: function(reply) {
     console.debug("_onRegisterError()");
     Services.telemetry.getHistogramById("PUSH_API_SUBSCRIBE_FAILED").add()
     if (!reply.error) {
       console.warn("onRegisterError: Called without valid error message!",
-        reply, String(reply));
+        reply);
       throw new Error("Registration error");
     }
     throw reply.error;
   },
 
   notificationsCleared() {
     this._visibleNotifications.clear();
   },
--- a/dom/security/nsCSPContext.cpp
+++ b/dom/security/nsCSPContext.cpp
@@ -940,17 +940,17 @@ nsCSPContext::SendReports(nsISupports* a
 
     nsCOMPtr<nsIUploadChannel> uploadChannel(do_QueryInterface(reportChannel));
     if (!uploadChannel) {
       // It's possible the URI provided can't be uploaded to, in which case
       // we skip this one. We'll already have warned about a non-HTTP URI earlier.
       continue;
     }
 
-    rv = uploadChannel->SetUploadStream(sis, NS_LITERAL_CSTRING("application/json"), -1);
+    rv = uploadChannel->SetUploadStream(sis, NS_LITERAL_CSTRING("application/csp-report"), -1);
     NS_ENSURE_SUCCESS(rv, rv);
 
     // if this is an HTTP channel, set the request method to post
     nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(reportChannel));
     if (httpChannel) {
       httpChannel->SetRequestMethod(NS_LITERAL_CSTRING("POST"));
     }
 
--- a/dom/security/test/unit/test_csp_reports.js
+++ b/dom/security/test/unit/test_csp_reports.js
@@ -28,16 +28,24 @@ const REPORT_SERVER_PATH = "/report";
 function makeReportHandler(testpath, message, expectedJSON) {
   return function(request, response) {
     // we only like "POST" submissions for reports!
     if (request.method !== "POST") {
       do_throw("violation report should be a POST request");
       return;
     }
 
+    // check content-type of report is "application/csp-report"
+    var contentType = request.hasHeader("Content-Type")
+                    ? request.getHeader("Content-Type") : undefined;
+    if (contentType !== "application/csp-report") {
+      do_throw("violation report should have the 'application/csp-report' " +
+               "content-type, when in fact it is " + contentType.toString())
+    }
+
     // obtain violation report
     var reportObj = JSON.parse(
           NetUtil.readInputStreamToString(
             request.bodyInputStream,
             request.bodyInputStream.available()));
 
     // dump("GOT REPORT:\n" + JSON.stringify(reportObj) + "\n");
     // dump("TESTPATH:    " + testpath + "\n");
--- a/layout/base/AccessibleCaretManager.cpp
+++ b/layout/base/AccessibleCaretManager.cpp
@@ -76,16 +76,18 @@ AccessibleCaretManager::sCaretsExtendedV
 /*static*/ bool
 AccessibleCaretManager::sCaretsAlwaysTilt = false;
 /*static*/ bool
 AccessibleCaretManager::sCaretsScriptUpdates = false;
 /*static*/ bool
 AccessibleCaretManager::sCaretsAllowDraggingAcrossOtherCaret = true;
 /*static*/ bool
 AccessibleCaretManager::sHapticFeedback = false;
+/*static*/ bool
+AccessibleCaretManager::sExtendSelectionForPhoneNumber = false;
 
 AccessibleCaretManager::AccessibleCaretManager(nsIPresShell* aPresShell)
   : mPresShell(aPresShell)
 {
   if (!mPresShell) {
     return;
   }
 
@@ -105,16 +107,18 @@ AccessibleCaretManager::AccessibleCaretM
     Preferences::AddBoolVarCache(&sCaretsAlwaysTilt,
                                  "layout.accessiblecaret.always_tilt");
     Preferences::AddBoolVarCache(&sCaretsScriptUpdates,
       "layout.accessiblecaret.allow_script_change_updates");
     Preferences::AddBoolVarCache(&sCaretsAllowDraggingAcrossOtherCaret,
       "layout.accessiblecaret.allow_dragging_across_other_caret", true);
     Preferences::AddBoolVarCache(&sHapticFeedback,
                                  "layout.accessiblecaret.hapticfeedback");
+    Preferences::AddBoolVarCache(&sExtendSelectionForPhoneNumber,
+      "layout.accessiblecaret.extend_selection_for_phone_number");
     addedPrefs = true;
   }
 }
 
 AccessibleCaretManager::~AccessibleCaretManager()
 {
 }
 
@@ -815,16 +819,21 @@ AccessibleCaretManager::SelectWord(nsIFr
   SetSelectionDragState(true);
   nsFrame* frame = static_cast<nsFrame*>(aFrame);
   nsresult rs = frame->SelectByTypeAtPoint(mPresShell->GetPresContext(), aPoint,
                                            eSelectWord, eSelectWord, 0);
 
   SetSelectionDragState(false);
   ClearMaintainedSelection();
 
+  // Smart-select phone numbers if possible.
+  if (sExtendSelectionForPhoneNumber) {
+    SelectMoreIfPhoneNumber();
+  }
+
   return rs;
 }
 
 void
 AccessibleCaretManager::SetSelectionDragState(bool aState) const
 {
   RefPtr<nsFrameSelection> fs = GetFrameSelection();
   if (fs) {
@@ -837,16 +846,63 @@ AccessibleCaretManager::SetSelectionDrag
     nsIDocument* doc = mPresShell->GetDocument();
     MOZ_ASSERT(doc);
     nsIWidget* widget = nsContentUtils::WidgetForDocument(doc);
     static_cast<nsWindow*>(widget)->SetSelectionDragState(aState);
   #endif
 }
 
 void
+AccessibleCaretManager::SelectMoreIfPhoneNumber() const
+{
+  SetSelectionDirection(eDirNext);
+  ExtendPhoneNumberSelection(NS_LITERAL_STRING("forward"));
+
+  SetSelectionDirection(eDirPrevious);
+  ExtendPhoneNumberSelection(NS_LITERAL_STRING("backward"));
+}
+
+void
+AccessibleCaretManager::ExtendPhoneNumberSelection(const nsAString& aDirection) const
+{
+  nsIDocument* doc = mPresShell->GetDocument();
+
+  // Extend the phone number selection until we find a boundary.
+  Selection* selection = GetSelection();
+
+  while (selection) {
+    // Save current Focus position, and extend the selection one char.
+    nsINode* focusNode = selection->GetFocusNode();
+    uint32_t focusOffset = selection->FocusOffset();
+    selection->Modify(NS_LITERAL_STRING("extend"),
+                      aDirection,
+                      NS_LITERAL_STRING("character"));
+
+    // If the selection didn't change, (can't extend further), we're done.
+    if (selection->GetFocusNode() == focusNode &&
+        selection->FocusOffset() == focusOffset) {
+      return;
+    }
+
+    // If the changed selection isn't a valid phone number, we're done.
+    nsAutoString selectedText;
+    selection->Stringify(selectedText);
+    nsAutoString phoneRegex(NS_LITERAL_STRING("(^\\+)?[0-9\\s,\\-.()*#pw]{1,30}$"));
+
+    if (!nsContentUtils::IsPatternMatching(selectedText, phoneRegex, doc)) {
+      // Backout the undesired selection extend, (collapse to original
+      // Anchor, extend to original Focus), before exit.
+      selection->Collapse(selection->GetAnchorNode(), selection->AnchorOffset());
+      selection->Extend(focusNode, focusOffset);
+      return;
+    }
+  }
+}
+
+void
 AccessibleCaretManager::SetSelectionDirection(nsDirection aDir) const
 {
   Selection* selection = GetSelection();
   if (selection) {
     selection->AdjustAnchorFocusForMultiRange(aDir);
   }
 }
 
--- a/layout/base/AccessibleCaretManager.h
+++ b/layout/base/AccessibleCaretManager.h
@@ -150,16 +150,22 @@ protected:
   nsIFrame* GetFocusableFrame(nsIFrame* aFrame) const;
 
   // Change focus to aFrame if it isn't nullptr. Otherwise, clear the old focus
   // then re-focus the window.
   void ChangeFocusToOrClearOldFocus(nsIFrame* aFrame) const;
 
   nsresult SelectWord(nsIFrame* aFrame, const nsPoint& aPoint) const;
   void SetSelectionDragState(bool aState) const;
+
+  // Called to extend a selection if possible that it's a phone number.
+  void SelectMoreIfPhoneNumber() const;
+  // Extend the current phone number selection in the requested direction.
+  void ExtendPhoneNumberSelection(const nsAString& aDirection) const;
+
   void SetSelectionDirection(nsDirection aDir) const;
 
   // If aDirection is eDirNext, get the frame for the range start in the first
   // range from the current selection, and return the offset into that frame as
   // well as the range start node and the node offset. Otherwise, get the frame
   // and offset for the range end in the last range instead.
   nsIFrame* GetFrameForFirstRangeStartOrLastRangeEnd(
     nsDirection aDirection, int32_t* aOutOffset, nsINode** aOutNode = nullptr,
@@ -277,16 +283,20 @@ protected:
   // boundary by 61 app units, which is 1 pixel + 1 app unit as defined in
   // AppUnit.h.
   static const int32_t kBoundaryAppUnits = 61;
 
   // Preference to show selection bars at the two ends in selection mode. The
   // selection bar is always disabled in cursor mode.
   static bool sSelectionBarEnabled;
 
+  // Preference to allow smarter selection of phone numbers,
+  // when user long presses text to start.
+  static bool sExtendSelectionForPhoneNumber;
+
   // Preference to show caret in cursor mode when long tapping on an empty
   // content. This also changes the default update behavior in cursor mode,
   // which is based on the emptiness of the content, into something more
   // heuristic. See UpdateCaretsForCursorMode() for the details.
   static bool sCaretShownWhenLongTappingOnEmptyContent;
 
   // Android specific visibility extensions correct compatibility issues
   // with ActionBar visibility during page scroll.
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -931,16 +931,20 @@ pref("layout.accessiblecaret.always_tilt
 
 // Selection change notifications generated by Javascript changes
 // update active AccessibleCarets / UI interactions.
 pref("layout.accessiblecaret.allow_script_change_updates", true);
 
 // Optionally provide haptic feedback on longPress selection events.
 pref("layout.accessiblecaret.hapticfeedback", true);
 
+// Initial text selection on long-press is enhanced to provide
+// a smarter phone-number selection for direct-dial ActionBar action.
+pref("layout.accessiblecaret.extend_selection_for_phone_number", true);
+
 // Disable sending console to logcat on release builds.
 #ifdef RELEASE_BUILD
 pref("consoleservice.logcat", false);
 #else
 pref("consoleservice.logcat", true);
 #endif
 
 // Enable Cardboard VR on mobile, assuming VR at all is enabled
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -352,16 +352,22 @@
             android:name="org.mozilla.gecko.dlc.DownloadContentService">
         </service>
 
         <service
             android:exported="false"
             android:name="org.mozilla.gecko.feeds.FeedService">
         </service>
 
+        <!-- DON'T EXPORT THIS, please! An attacker could delete arbitrary files. -->
+        <service
+            android:exported="false"
+            android:name="org.mozilla.gecko.cleanup.FileCleanupService">
+        </service>
+
         <receiver
             android:name="org.mozilla.gecko.feeds.FeedAlarmReceiver"
             android:exported="false" />
 
         <receiver
             android:name="org.mozilla.gecko.BootReceiver"
             android:exported="false">
             <intent-filter>
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -15,16 +15,17 @@ import org.json.JSONArray;
 import org.mozilla.gecko.adjust.AdjustHelperInterface;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.Tabs.TabEvents;
 import org.mozilla.gecko.animation.PropertyAnimator;
 import org.mozilla.gecko.animation.ViewHelper;
+import org.mozilla.gecko.cleanup.FileCleanupController;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.SuggestedSites;
 import org.mozilla.gecko.distribution.Distribution;
 import org.mozilla.gecko.distribution.Distribution.DistributionDescriptor;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.dlc.DownloadContentService;
 import org.mozilla.gecko.dlc.catalog.DownloadContent;
@@ -1057,16 +1058,21 @@ public class BrowserApp extends GeckoApp
                 if (profile.inGuestMode()) {
                     GuestSession.showNotification(BrowserApp.this);
                 } else {
                     // If we're restarting, we won't destroy the activity.
                     // Make sure we remove any guest notifications that might
                     // have been shown.
                     GuestSession.hideNotification(BrowserApp.this);
                 }
+
+                // It'd be better to launch this once, in onCreate, but there's ambiguity for when the
+                // profile is created so we run here instead. Don't worry, call start short-circuits pretty fast.
+                final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(BrowserApp.this, profile.getName());
+                FileCleanupController.startIfReady(BrowserApp.this, sharedPrefs, profile.getDir().getAbsolutePath());
             }
         });
 
         // We don't upload in onCreate because that's only called when the Activity needs to be instantiated
         // and it's possible the system will never free the Activity from memory.
         //
         // We don't upload in onResume/onPause because that will be called each time the Activity is obscured,
         // including by our own Activities/dialogs, and there is no reason to upload each time we're unobscured.
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
@@ -552,23 +552,31 @@ public final class GeckoProfile {
     public String getName() {
         return mName;
     }
 
     public boolean isCustomProfile() {
         return CUSTOM_PROFILE.equals(mName);
     }
 
+    /**
+     * Retrieves the directory backing the profile. This method acts
+     * as a lazy initializer for the GeckoProfile instance.
+     */
     @RobocopTarget
     public synchronized File getDir() {
         forceCreate();
         return mProfileDir;
     }
 
-    public synchronized GeckoProfile forceCreate() {
+    /**
+     * Forces profile creation. Consider using {@link #getDir()} to initialize the profile instead - it is the
+     * lazy initializer and, for our code reasoning abilities, we should initialize the profile in one place.
+     */
+    private synchronized GeckoProfile forceCreate() {
         if (mProfileDir != null) {
             return this;
         }
 
         try {
             // Check if a profile with this name already exists.
             try {
                 mProfileDir = findProfileDir();
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoThread.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoThread.java
@@ -372,17 +372,17 @@ public class GeckoThread extends Thread 
 
             if (args == null || !args.contains(BrowserApp.GUEST_BROWSING_ARG)) {
                 profileArg += " " + BrowserApp.GUEST_BROWSING_ARG;
             }
 
         } else {
             // Make sure a profile exists.
             final GeckoProfile profile = getProfile();
-            profile.forceCreate();
+            profile.getDir(); // call the lazy initializer
 
             // If args don't include the profile, make sure it's included.
             if (args == null || !args.matches(".*\\B-(P|profile)\\s+\\S+.*")) {
                 if (profile.isCustomProfile()) {
                     profileArg = " -profile " + profile.getDir().getAbsolutePath();
                 } else {
                     profileArg = " -P " + profile.getName();
                 }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupController.java
@@ -0,0 +1,81 @@
+/*
+ * 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.cleanup;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.support.annotation.VisibleForTesting;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Encapsulates the code to run the {@link FileCleanupService}. Call
+ * {@link #startIfReady(Context, SharedPreferences, String)} to start the clean-up.
+ *
+ * Note: for simplicity, the current implementation does not cache which
+ * files have been cleaned up and will attempt to delete the same files
+ * each time it is run. If the file deletion list grows large, consider
+ * keeping a cache.
+ */
+public class FileCleanupController {
+
+    private static final long MILLIS_BETWEEN_CLEANUPS = TimeUnit.DAYS.toMillis(7);
+    @VisibleForTesting static final String PREF_LAST_CLEANUP_MILLIS = "cleanup.lastFileCleanupMillis";
+
+    // These will be prepended with the path of the profile we're cleaning up.
+    private static final String[] PROFILE_FILES_TO_CLEANUP = new String[] {
+            "health.db",
+            "health.db-journal",
+            "health.db-shm",
+            "health.db-wal",
+    };
+
+    /**
+     * Starts the clean-up if it's time to clean-up, otherwise returns. For simplicity,
+     * it does not schedule the cleanup for some point in the future - this method will
+     * have to be called again (i.e. polled) in order to run the clean-up service.
+     *
+     * @param context Context of the calling {@link android.app.Activity}
+     * @param sharedPrefs The {@link SharedPreferences} instance to store the controller state to
+     * @param profilePath The path to the profile the service should clean-up files from
+     */
+    public static void startIfReady(final Context context, final SharedPreferences sharedPrefs, final String profilePath) {
+        if (!isCleanupReady(sharedPrefs)) {
+            return;
+        }
+
+        recordCleanupScheduled(sharedPrefs);
+
+        final Intent fileCleanupIntent = new Intent(context, FileCleanupService.class);
+        fileCleanupIntent.setAction(FileCleanupService.ACTION_DELETE_FILES);
+        fileCleanupIntent.putExtra(FileCleanupService.EXTRA_FILE_PATHS_TO_DELETE, getFilesToCleanup(profilePath + "/"));
+        context.startService(fileCleanupIntent);
+    }
+
+    private static boolean isCleanupReady(final SharedPreferences sharedPrefs) {
+        final long lastCleanupMillis = sharedPrefs.getLong(PREF_LAST_CLEANUP_MILLIS, -1);
+        return lastCleanupMillis + MILLIS_BETWEEN_CLEANUPS < System.currentTimeMillis();
+    }
+
+    private static void recordCleanupScheduled(final SharedPreferences sharedPrefs) {
+        final SharedPreferences.Editor editor = sharedPrefs.edit();
+        editor.putLong(PREF_LAST_CLEANUP_MILLIS, System.currentTimeMillis()).apply();
+    }
+
+    @VisibleForTesting
+    static ArrayList<String> getFilesToCleanup(final String profilePath) {
+        final ArrayList<String> out = new ArrayList<>(PROFILE_FILES_TO_CLEANUP.length);
+        for (final String path : PROFILE_FILES_TO_CLEANUP) {
+            // Append a file separator, just in-case the caller didn't include one.
+            out.add(profilePath + File.separator + path);
+        }
+        return out;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/cleanup/FileCleanupService.java
@@ -0,0 +1,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/.
+ */
+
+package org.mozilla.gecko.cleanup;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.util.Log;
+
+import java.io.File;
+import java.util.ArrayList;
+
+/**
+ * An IntentService to delete files.
+ *
+ * It takes an {@link ArrayList} of String file paths to delete via the extra
+ * {@link #EXTRA_FILE_PATHS_TO_DELETE}. If these file paths are directories, they will
+ * not be traversed recursively and will only be deleted if empty. This is to avoid accidentally
+ * trashing a users' profile if a folder is accidentally listed.
+ *
+ * An IntentService was chosen because:
+ *   * It generally won't be killed when the Activity is
+ *   * (unlike HandlerThread) The system handles scheduling, prioritizing,
+ * and shutting down the underlying background thread
+ *   * (unlike an existing background thread) We don't block our background operations
+ * for this, which doesn't directly affect the user.
+ *
+ * The major trade-off is that this Service is very dangerous if it's exported... so don't do that!
+ */
+public class FileCleanupService extends IntentService {
+    private static final String LOGTAG = "Gecko" + FileCleanupService.class.getSimpleName();
+    private static final String WORKER_THREAD_NAME = LOGTAG + "Worker";
+
+    public static final String ACTION_DELETE_FILES = "org.mozilla.gecko.intent.action.DELETE_FILES";
+    public static final String EXTRA_FILE_PATHS_TO_DELETE = "org.mozilla.gecko.file_paths_to_delete";
+
+    public FileCleanupService() {
+        super(WORKER_THREAD_NAME);
+
+        // We're likely to get scheduled again - let's wait until then in order to avoid:
+        //   * The coding complexity of re-running this
+        //   * Consuming system resources: we were probably killed for resource conservation purposes
+        setIntentRedelivery(false);
+    }
+
+    @Override
+    protected void onHandleIntent(final Intent intent) {
+        if (!isIntentValid(intent)) {
+            return;
+        }
+
+        final ArrayList<String> filesToDelete = intent.getStringArrayListExtra(EXTRA_FILE_PATHS_TO_DELETE);
+        for (final String path : filesToDelete) {
+            final File file = new File(path);
+            file.delete();
+        }
+    }
+
+    private static boolean isIntentValid(final Intent intent) {
+        if (intent == null) {
+            Log.w(LOGTAG, "Received null intent");
+            return false;
+        }
+
+        if (!intent.getAction().equals(ACTION_DELETE_FILES)) {
+            Log.w(LOGTAG, "Received unknown intent action: " + intent.getAction());
+            return false;
+        }
+
+        if (!intent.hasExtra(EXTRA_FILE_PATHS_TO_DELETE)) {
+            Log.w(LOGTAG, "Received intent with no files extra");
+            return false;
+        }
+
+        return true;
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
@@ -19,30 +19,9 @@ public class TelemetryConstants {
     public static final String EXTRA_DEFAULT_SEARCH_ENGINE = "defaultSearchEngine";
     public static final String EXTRA_DOC_ID = "docId";
     public static final String EXTRA_PROFILE_NAME = "geckoProfileName";
     public static final String EXTRA_PROFILE_PATH = "geckoProfilePath";
     public static final String EXTRA_SEQ = "seq";
 
     public static final String PREF_SERVER_URL = "telemetry-serverUrl";
     public static final String PREF_SEQ_COUNT = "telemetry-seqCount";
-
-    public static class CorePing {
-        private CorePing() { /* To prevent instantiation */ }
-
-        public static final String NAME = "core";
-        public static final int VERSION_VALUE = 4; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
-        public static final String OS_VALUE = "Android";
-
-        public static final String ARCHITECTURE = "arch";
-        public static final String CLIENT_ID = "clientId";
-        public static final String DEFAULT_SEARCH_ENGINE = "defaultSearch";
-        public static final String DEVICE = "device";
-        public static final String DISTRIBUTION_ID = "distributionId";
-        public static final String EXPERIMENTS = "experiments";
-        public static final String LOCALE = "locale";
-        public static final String OS_ATTR = "os";
-        public static final String OS_VERSION = "osversion";
-        public static final String PROFILE_CREATION_DATE = "profileDate";
-        public static final String SEQ = "seq";
-        public static final String VERSION_ATTR = "v";
-    }
 }
deleted file mode 100644
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java
+++ /dev/null
@@ -1,107 +0,0 @@
-/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko.telemetry;
-
-import android.content.Context;
-import android.os.Build;
-import android.support.annotation.Nullable;
-import android.text.TextUtils;
-
-import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.Locales;
-import org.mozilla.gecko.sync.ExtendedJSONObject;
-import org.mozilla.gecko.telemetry.TelemetryConstants.CorePing;
-import org.mozilla.gecko.util.Experiments;
-import org.mozilla.gecko.util.StringUtils;
-
-import java.io.IOException;
-import java.util.Locale;
-
-/**
- * A class with static methods to generate the various Java-created Telemetry pings to upload to the telemetry server.
- */
-public class TelemetryPingGenerator {
-
-    // In the server url, the initial path directly after the "scheme://host:port/"
-    private static final String SERVER_INITIAL_PATH = "submit/telemetry";
-
-    /**
-     * Returns a url of the format:
-     *   http://hostname/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
-     *
-     * @param docId A unique document ID for the ping associated with the upload to this server
-     * @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
-     * @param docType The name of the ping (e.g. "main")
-     * @return a url at which to POST the telemetry data to
-     */
-    private static String getTelemetryServerURL(final String docId, final String serverURLSchemeHostPort,
-            final String docType) {
-        final String appName = AppConstants.MOZ_APP_BASENAME;
-        final String appVersion = AppConstants.MOZ_APP_VERSION;
-        final String appUpdateChannel = AppConstants.MOZ_UPDATE_CHANNEL;
-        final String appBuildId = AppConstants.MOZ_APP_BUILDID;
-
-        // The compiler will optimize a single String concatenation into a StringBuilder statement.
-        // If you change this `return`, be sure to keep it as a single statement to keep it optimized!
-        return serverURLSchemeHostPort + '/' +
-                SERVER_INITIAL_PATH + '/' +
-                docId + '/' +
-                docType + '/' +
-                appName + '/' +
-                appVersion + '/' +
-                appUpdateChannel + '/' +
-                appBuildId;
-    }
-
-    /**
-     * @param docId A unique document ID for the ping associated with the upload to this server
-     * @param clientId The client ID of this profile (from Gecko)
-     * @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
-     * @param profileCreationDateDays The profile creation date in days to the UNIX epoch, NOT MILLIS.
-     * @throws IOException when client ID could not be created
-     */
-    public static TelemetryPing createCorePing(final Context context, final String docId, final String clientId,
-            final String serverURLSchemeHostPort, final int seq, final long profileCreationDateDays,
-            @Nullable final String distributionId, @Nullable final String defaultSearchEngine) {
-        final String serverURL = getTelemetryServerURL(docId, serverURLSchemeHostPort, CorePing.NAME);
-        final ExtendedJSONObject payload =
-                createCorePingPayload(context, clientId, seq, profileCreationDateDays, distributionId, defaultSearchEngine);
-        return new TelemetryPing(serverURL, payload);
-    }
-
-    private static ExtendedJSONObject createCorePingPayload(final Context context, final String clientId,
-            final int seq, final long profileCreationDate, @Nullable final String distributionId,
-            @Nullable final String defaultSearchEngine) {
-        final ExtendedJSONObject ping = new ExtendedJSONObject();
-        ping.put(CorePing.VERSION_ATTR, CorePing.VERSION_VALUE);
-        ping.put(CorePing.OS_ATTR, CorePing.OS_VALUE);
-
-        // We limit the device descriptor to 32 characters because it can get long. We give fewer characters to the
-        // manufacturer because we're less likely to have manufacturers with similar names than we are for a
-        // manufacturer to have two devices with the similar names (e.g. Galaxy S6 vs. Galaxy Note 6).
-        final String deviceDescriptor =
-                StringUtils.safeSubstring(Build.MANUFACTURER, 0, 12) + '-' + StringUtils.safeSubstring(Build.MODEL, 0, 19);
-
-        ping.put(CorePing.ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH);
-        ping.put(CorePing.CLIENT_ID, clientId);
-        ping.put(CorePing.DEFAULT_SEARCH_ENGINE, TextUtils.isEmpty(defaultSearchEngine) ? null : defaultSearchEngine);
-        ping.put(CorePing.DEVICE, deviceDescriptor);
-        ping.put(CorePing.LOCALE, Locales.getLanguageTag(Locale.getDefault()));
-        ping.put(CorePing.OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
-        ping.put(CorePing.SEQ, seq);
-        ping.putArray(CorePing.EXPERIMENTS, Experiments.getActiveExperiments(context));
-
-        // Optional.
-        if (distributionId != null) {
-            ping.put(CorePing.DISTRIBUTION_ID, distributionId);
-        }
-
-        // `null` indicates failure more clearly than < 0.
-        final Long finalProfileCreationDate = (profileCreationDate < 0) ? null : profileCreationDate;
-        ping.put(CorePing.PROFILE_CREATION_DATE, finalProfileCreationDate);
-        return ping;
-    }
-}
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
@@ -5,27 +5,30 @@
 package org.mozilla.gecko.telemetry;
 
 import android.content.Context;
 import android.content.Intent;
 import android.content.SharedPreferences;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 import android.support.annotation.WorkerThread;
+import android.text.TextUtils;
 import android.util.Log;
 import ch.boye.httpclientandroidlib.HttpResponse;
 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.background.BackgroundService;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.sync.net.BaseResource;
 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
 import org.mozilla.gecko.sync.net.Resource;
+import org.mozilla.gecko.telemetry.pings.TelemetryCorePingBuilder;
+import org.mozilla.gecko.telemetry.pings.TelemetryPing;
 import org.mozilla.gecko.util.StringUtils;
 
 import java.io.IOException;
 import java.net.URISyntaxException;
 import java.security.GeneralSecurityException;
 
 /**
  * The service that handles uploading telemetry payloads to the server.
@@ -165,33 +168,43 @@ public class TelemetryUploadService exte
 
         return true;
     }
 
     @WorkerThread
     private void uploadCorePing(@NonNull final String docId, final int seq, @NonNull final String profileName,
                 @NonNull final String profilePath, @Nullable final String defaultSearchEngine) {
         final GeckoProfile profile = GeckoProfile.get(this, profileName, profilePath);
-        final long profileCreationDate = getProfileCreationDate(profile);
         final String clientId;
         try {
             clientId = profile.getClientId();
         } catch (final IOException e) {
             Log.w(LOGTAG, "Unable to get client ID to generate core ping: returning.", e);
             return;
         }
 
         // Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile.
         final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(this, profileName);
         // TODO (bug 1241685): Sync this preference with the gecko preference.
         final String serverURLSchemeHostPort =
                 sharedPrefs.getString(TelemetryConstants.PREF_SERVER_URL, TelemetryConstants.DEFAULT_SERVER_URL);
+
+        final long profileCreationDate = getProfileCreationDate(profile);
+        final TelemetryCorePingBuilder builder = new TelemetryCorePingBuilder(this, serverURLSchemeHostPort)
+                .setClientID(clientId)
+                .setDefaultSearchEngine(TextUtils.isEmpty(defaultSearchEngine) ? null : defaultSearchEngine)
+                .setProfileCreationDate(profileCreationDate < 0 ? null : profileCreationDate)
+                .setSequenceNumber(seq);
+
         final String distributionId = sharedPrefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null);
-        final TelemetryPing corePing = TelemetryPingGenerator.createCorePing(this, docId, clientId,
-                serverURLSchemeHostPort, seq, profileCreationDate, distributionId, defaultSearchEngine);
+        if (distributionId != null) {
+            builder.setOptDistributionID(distributionId);
+        }
+
+        final TelemetryPing corePing = builder.build();
         final CorePingResultDelegate resultDelegate = new CorePingResultDelegate();
         uploadPing(corePing, resultDelegate);
     }
 
     private void uploadPing(final TelemetryPing ping, final ResultDelegate delegate) {
         final BaseResource resource;
         try {
             resource = new BaseResource(ping.getURL());
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pings/TelemetryCorePingBuilder.java
@@ -0,0 +1,138 @@
+/*
+ * 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.telemetry.pings;
+
+import android.content.Context;
+import android.os.Build;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.util.Experiments;
+import org.mozilla.gecko.util.StringUtils;
+
+import java.util.Locale;
+
+/**
+ * Builds a {@link TelemetryPing} representing a core ping.
+ *
+ * See https://gecko.readthedocs.org/en/latest/toolkit/components/telemetry/telemetry/core-ping.html
+ * for details on the core ping.
+ */
+public class TelemetryCorePingBuilder extends TelemetryPingBuilder {
+
+    private static final String NAME = "core";
+    private static final int VERSION_VALUE = 4; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
+    private static final String OS_VALUE = "Android";
+
+    private static final String ARCHITECTURE = "arch";
+    private static final String CLIENT_ID = "clientId";
+    private static final String DEFAULT_SEARCH_ENGINE = "defaultSearch";
+    private static final String DEVICE = "device";
+    private static final String DISTRIBUTION_ID = "distributionId";
+    private static final String EXPERIMENTS = "experiments";
+    private static final String LOCALE = "locale";
+    private static final String OS_ATTR = "os";
+    private static final String OS_VERSION = "osversion";
+    private static final String PROFILE_CREATION_DATE = "profileDate";
+    private static final String SEQ = "seq";
+    private static final String VERSION_ATTR = "v";
+
+    public TelemetryCorePingBuilder(final Context context, final String serverURLSchemeHostPort) {
+        super(serverURLSchemeHostPort);
+        initPayloadConstants(context);
+    }
+
+    private void initPayloadConstants(final Context context) {
+        payload.put(VERSION_ATTR, VERSION_VALUE);
+        payload.put(OS_ATTR, OS_VALUE);
+
+        // We limit the device descriptor to 32 characters because it can get long. We give fewer characters to the
+        // manufacturer because we're less likely to have manufacturers with similar names than we are for a
+        // manufacturer to have two devices with the similar names (e.g. Galaxy S6 vs. Galaxy Note 6).
+        final String deviceDescriptor =
+                StringUtils.safeSubstring(Build.MANUFACTURER, 0, 12) + '-' + StringUtils.safeSubstring(Build.MODEL, 0, 19);
+
+        payload.put(ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH);
+        payload.put(DEVICE, deviceDescriptor);
+        payload.put(LOCALE, Locales.getLanguageTag(Locale.getDefault()));
+        payload.put(OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
+        payload.putArray(EXPERIMENTS, Experiments.getActiveExperiments(context));
+    }
+
+    @Override
+    String getDocType() {
+        return NAME;
+    }
+
+    @Override
+    String[] getMandatoryFields() {
+        return new String[] {
+                ARCHITECTURE,
+                CLIENT_ID,
+                DEFAULT_SEARCH_ENGINE,
+                DEVICE,
+                LOCALE,
+                OS_ATTR,
+                OS_VERSION,
+                PROFILE_CREATION_DATE,
+                SEQ,
+                VERSION_ATTR,
+        };
+    }
+
+    public TelemetryCorePingBuilder setClientID(@NonNull final String clientID) {
+        if (clientID == null) {
+            throw new IllegalArgumentException("Expected non-null clientID");
+        }
+        payload.put(CLIENT_ID, clientID);
+        return this;
+    }
+
+    /**
+     * @param engine the default search engine identifier, or null if there is an error.
+     */
+    public TelemetryCorePingBuilder setDefaultSearchEngine(@Nullable final String engine) {
+        if (engine != null && engine.isEmpty()) {
+            throw new IllegalArgumentException("Received empty string. Expected identifier or null.");
+        }
+        payload.put(DEFAULT_SEARCH_ENGINE, engine);
+        return this;
+    }
+
+    public TelemetryCorePingBuilder setOptDistributionID(@NonNull final String distributionID) {
+        if (distributionID == null) {
+            throw new IllegalArgumentException("Expected non-null distribution ID");
+        }
+        payload.put(DISTRIBUTION_ID, distributionID);
+        return this;
+    }
+
+    /**
+     * @param date a positive date value, or null if there is an error.
+     */
+    public TelemetryCorePingBuilder setProfileCreationDate(@Nullable final Long date) {
+        if (date != null && date < 0) {
+            throw new IllegalArgumentException("Expect positive date value. Received: " + date);
+        }
+        payload.put(PROFILE_CREATION_DATE, date);
+        return this;
+    }
+
+    // TODO (mcomella): We can potentially build two pings with the same seq no if we leave seq as an argument.
+    /**
+     * @param seq a positive sequence number.
+     */
+    public TelemetryCorePingBuilder setSequenceNumber(final int seq) {
+        if (seq < 0) {
+            throw new IllegalArgumentException("Expected positive sequence number. Recived: " + seq);
+        }
+        payload.put(SEQ, seq);
+        return this;
+    }
+}
rename from mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
rename to mobile/android/base/java/org/mozilla/gecko/telemetry/pings/TelemetryPing.java
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pings/TelemetryPing.java
@@ -1,19 +1,22 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.gecko.telemetry;
+package org.mozilla.gecko.telemetry.pings;
 
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 
 /**
  * Container for telemetry data and the data necessary to upload it.
+ *
+ * If you want to create one of these, consider extending
+ * {@link TelemetryPingBuilder} or one of its descendants.
  */
 public class TelemetryPing {
     private final String url;
     private final ExtendedJSONObject payload;
 
     public TelemetryPing(final String url, final ExtendedJSONObject payload) {
         this.url = url;
         this.payload = payload;
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pings/TelemetryPingBuilder.java
@@ -0,0 +1,88 @@
+/*
+ * 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.telemetry.pings;
+
+import org.mozilla.gecko.AppConstants;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+
+import java.util.Set;
+import java.util.UUID;
+
+/**
+ * A generic Builder for {@link TelemetryPing} instances. Each overriding class is
+ * expected to create a specific type of ping (e.g. "core").
+ *
+ * This base class handles the common ping operations under the hood:
+ *   * Validating mandatory fields
+ *   * Forming the server url
+ */
+abstract class TelemetryPingBuilder {
+    // In the server url, the initial path directly after the "scheme://host:port/"
+    private static final String SERVER_INITIAL_PATH = "submit/telemetry";
+
+    private final String serverUrl;
+    protected final ExtendedJSONObject payload;
+
+    public TelemetryPingBuilder(final String serverURLSchemeHostPort) {
+        serverUrl = getTelemetryServerURL(getDocType(), serverURLSchemeHostPort);
+        payload = new ExtendedJSONObject();
+    }
+
+    /**
+     * @return the name of the ping (e.g. "core")
+     */
+    abstract String getDocType();
+
+    /**
+     * @return the fields that are mandatory for the resultant ping to be uploaded to
+     *         the server. These will be validated before the ping is built.
+     */
+    abstract String[] getMandatoryFields();
+
+    public TelemetryPing build() {
+        validatePayload();
+        return new TelemetryPing(serverUrl, payload);
+    }
+
+    private void validatePayload() {
+        final Set<String> keySet = payload.keySet();
+        for (final String mandatoryField : getMandatoryFields()) {
+            if (!keySet.contains(mandatoryField)) {
+                throw new IllegalArgumentException("Builder does not contain mandatory field: " +
+                        mandatoryField);
+            }
+        }
+    }
+
+    /**
+     * Returns a url of the format:
+     *   http://hostname/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
+     *
+     * @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
+     * @param docType The name of the ping (e.g. "main")
+     * @return a url at which to POST the telemetry data to
+     */
+    private static String getTelemetryServerURL(final String docType,
+            final String serverURLSchemeHostPort) {
+        final String docId = UUID.randomUUID().toString();
+        final String appName = AppConstants.MOZ_APP_BASENAME;
+        final String appVersion = AppConstants.MOZ_APP_VERSION;
+        final String appUpdateChannel = AppConstants.MOZ_UPDATE_CHANNEL;
+        final String appBuildId = AppConstants.MOZ_APP_BUILDID;
+
+        // The compiler will optimize a single String concatenation into a StringBuilder statement.
+        // If you change this `return`, be sure to keep it as a single statement to keep it optimized!
+        return serverURLSchemeHostPort + '/' +
+                SERVER_INITIAL_PATH + '/' +
+                docId + '/' +
+                docType + '/' +
+                appName + '/' +
+                appVersion + '/' +
+                appUpdateChannel + '/' +
+                appBuildId;
+    }
+}
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -102,17 +102,17 @@
      &formatS; will be replaced with the title of the tab, as received from the
      web page. When audio is not playing in a tab, &formatS; will be used as
      the content description. -->
 <!ENTITY tab_title_prefix_is_playing_audio "Playing audio – &formatS;">
 
 <!ENTITY settings "Settings">
 <!ENTITY settings_title "Settings">
 <!ENTITY pref_category_general "General">
-<!ENTITY pref_category_general_summary2 "Home, language, URL bar">
+<!ENTITY pref_category_general_summary3 "Home, language, tab queue">
 
 <!-- Localization note (pref_category_language) : This is the preferences
      section in which the user picks the locale in which to display Firefox
      UI. The locale includes both language and region concepts. -->
 <!ENTITY pref_category_language "Language">
 <!ENTITY pref_category_language_summary "Change the language of your browser">
 <!ENTITY pref_browser_locale "Browser language">
 
@@ -141,21 +141,21 @@
 <!ENTITY overlay_share_no_url "No link found in this share">
 <!ENTITY overlay_share_select_device "Select device">
 <!-- Localization note (overlay_no_synced_devices) : Used when the menu option
      to send a tab to a synced device is pressed and no other synced devices
      are found. -->
 <!ENTITY overlay_no_synced_devices "No Firefox Account connected devices found">
 
 <!ENTITY pref_category_search3 "Search">
-<!ENTITY pref_category_search_summary "Customize your search providers">
+<!ENTITY pref_category_search_summary2 "Add, set default, show suggestions">
 <!ENTITY pref_category_accessibility "Accessibility">
 <!ENTITY pref_category_accessibility_summary2 "Text size, zoom, voice input">
 <!ENTITY pref_category_privacy_short "Privacy">
-<!ENTITY pref_category_privacy_summary3 "Tracking, cookies, data choices">
+<!ENTITY pref_category_privacy_summary4 "Tracking, logins, data choices">
 <!ENTITY pref_category_vendor2 "&vendorShortName; &brandShortName;">
 <!ENTITY pref_category_vendor_summary2 "About &brandShortName;, FAQs, feedback">
 <!ENTITY pref_category_datareporting "Data choices">
 <!ENTITY pref_category_logins "Logins">
 <!ENTITY pref_learn_more "Learn more">
 <!ENTITY pref_category_installed_search_engines "Installed search engines">
 <!ENTITY pref_category_add_search_providers "Add more search providers">
 <!ENTITY pref_category_search_restore_defaults "Restore search engines">
@@ -163,17 +163,17 @@
 <!ENTITY pref_search_restore_defaults_summary "Restore defaults">
 <!-- Localization note (pref_search_hint) : "TIP" as in "hint", "clue" etc. Displayed as an
      advisory message on the customise search providers settings page explaining how to add new
      search providers.
      The &formatI; in the string will be replaced by a small image of the icon described, and can be moved to wherever
      it is applicable. -->
 <!ENTITY pref_search_hint2 "TIP: Add any website to your list of search providers by long-pressing on its search field and then touching the &formatI; icon.">
 <!ENTITY pref_category_advanced "Advanced">
-<!ENTITY pref_category_advanced_summary2 "Restore tabs, plugins, developer tools">
+<!ENTITY pref_category_advanced_summary3 "Restore tabs, data saver, developer tools">
 <!ENTITY pref_category_notifications "Notifications">
 <!ENTITY pref_category_notifications_summary "New features, website updates">
 <!ENTITY pref_content_notifications "Website updates">
 <!ENTITY pref_content_notifications_summary2 "Discover new content from supported sites">
 <!ENTITY pref_developer_remotedebugging_usb "Remote debugging via USB">
 <!ENTITY pref_developer_remotedebugging_wifi "Remote debugging via Wi-Fi">
 <!ENTITY pref_developer_remotedebugging_wifi_disabled_summary "Wi-Fi debugging requires your device to have a QR code reader app installed.">
 <!ENTITY pref_remember_signons2 "Remember logins">
@@ -344,17 +344,17 @@ size. -->
 <!ENTITY pref_private_data_cookies2 "Cookies &amp; active logins">
 <!ENTITY pref_private_data_passwords2 "Saved logins">
 <!ENTITY pref_private_data_cache "Cache">
 <!ENTITY pref_private_data_offlineApps "Offline website data">
 <!ENTITY pref_private_data_siteSettings2 "Site settings">
 <!ENTITY pref_private_data_downloadFiles2 "Downloads">
 <!ENTITY pref_private_data_syncedTabs "Synced tabs">
 
-
+<!ENTITY pref_default_browser "Make default browser">
 <!ENTITY pref_about_firefox "About &brandShortName;">
 <!ENTITY pref_vendor_faqs "FAQs">
 <!ENTITY pref_vendor_feedback "Give feedback">
 
 <!ENTITY pref_dialog_set_default "Set as default">
 <!ENTITY pref_dialog_default "Default">
 <!ENTITY pref_dialog_remove "Remove">
 
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -201,16 +201,18 @@ gbjar.sources += ['java/org/mozilla/geck
     'animation/Rotate3DAnimation.java',
     'animation/ViewHelper.java',
     'ANRReporter.java',
     'AppNotificationClient.java',
     'BaseGeckoInterface.java',
     'BootReceiver.java',
     'BrowserApp.java',
     'BrowserLocaleManager.java',
+    'cleanup/FileCleanupController.java',
+    'cleanup/FileCleanupService.java',
     'ContactService.java',
     'ContextGetter.java',
     'CrashHandler.java',
     'CustomEditText.java',
     'DataReportingNotification.java',
     'db/AbstractPerProfileDatabaseProvider.java',
     'db/AbstractTransactionalProvider.java',
     'db/BaseTable.java',
@@ -567,19 +569,20 @@ gbjar.sources += ['java/org/mozilla/geck
     'tabs/TabPanelBackButton.java',
     'tabs/TabsGridLayout.java',
     'tabs/TabsLayoutAdapter.java',
     'tabs/TabsLayoutItemView.java',
     'tabs/TabsListLayout.java',
     'tabs/TabsPanel.java',
     'tabs/TabsPanelThumbnailView.java',
     'Telemetry.java',
+    'telemetry/pings/TelemetryCorePingBuilder.java',
+    'telemetry/pings/TelemetryPing.java',
+    'telemetry/pings/TelemetryPingBuilder.java',
     'telemetry/TelemetryConstants.java',
-    'telemetry/TelemetryPing.java',
-    'telemetry/TelemetryPingGenerator.java',
     'telemetry/TelemetryUploadService.java',
     'TelemetryContract.java',
     'text/FloatingActionModeCallback.java',
     'text/FloatingToolbarTextSelection.java',
     'text/TextAction.java',
     'text/TextSelection.java',
     'TextSelectionHandle.java',
     'ThumbnailHelper.java',
--- a/mobile/android/base/resources/xml/preferences.xml
+++ b/mobile/android/base/resources/xml/preferences.xml
@@ -65,16 +65,21 @@
             android:title="@string/pref_clear_private_data_now"
             android:persistent="true"
             android:positiveButtonText="@string/button_clear_data"
             gecko:entries="@array/pref_private_data_entries"
             gecko:entryValues="@array/pref_private_data_values"
             gecko:entryKeys="@array/pref_private_data_keys"
             gecko:initialValues="@array/pref_private_data_defaults" />
 
+    <org.mozilla.gecko.preferences.LinkPreference android:key="android.not_a_preference.default_browser.link"
+                                                  android:title="@string/pref_default_browser"
+                                                  android:persistent="false"
+                                                  url="https://support.mozilla.org/kb/make-firefox-default-browser-android?utm_source=inproduct&amp;utm_medium=settings&amp;utm_campaign=mobileandroid"/>
+
     <PreferenceScreen android:title="@string/pref_category_vendor"
                       android:summary="@string/pref_category_vendor_summary"
                       android:fragment="org.mozilla.gecko.preferences.GeckoPreferenceFragment" >
         <extra android:name="resource"
                android:value="preferences_vendor"/>
     </PreferenceScreen>
 
 </PreferenceScreen>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -71,23 +71,16 @@
   <string name="crash_email">&crash_email;</string>
   <string name="crash_closing_alert">&crash_closing_alert;</string>
   <string name="sending_crash_report">&sending_crash_report;</string>
   <string name="crash_close_label">&crash_close_label;</string>
   <string name="crash_restart_label">&crash_restart_label;</string>
 
   <string name="url_bar_default_text">&url_bar_default_text2;</string>
 
-  <!-- https://input.mozilla.org/feedback/android/%VERSION%/%CHANNEL%/?utm_source=feedback-settings
-       This should be kept in sync with the "app.feedbackURL" pref defined in mobile.js -->
-  <string name="feedback_link">https://input.mozilla.org/feedback/android/&formatS1;/&formatS2;/?utm_source=feedback-settings</string>
-
-  <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/faq -->
-  <string name="faq_link">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/faq</string>
-
   <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/ -->
   <string name="help_link">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/</string>
   <string name="help_menu">&help_menu;</string>
 
   <string name="quit">&quit;</string>
   <string name="bookmark">&bookmark;</string>
   <string name="bookmark_remove">&bookmark_remove;</string>
   <string name="bookmark_added">&bookmark_added;</string>
@@ -131,24 +124,24 @@
   <string name="overlay_share_send_tab_btn_label">&overlay_share_send_tab_btn_label;</string>
   <string name="overlay_share_no_url">&overlay_share_no_url;</string>
   <string name="overlay_share_select_device">&overlay_share_select_device;</string>
   <string name="overlay_no_synced_devices">&overlay_no_synced_devices;</string>
 
   <string name="settings">&settings;</string>
   <string name="settings_title">&settings_title;</string>
   <string name="pref_category_general">&pref_category_general;</string>
-  <string name="pref_category_general_summary">&pref_category_general_summary2;</string>
+  <string name="pref_category_general_summary">&pref_category_general_summary3;</string>
 
   <string name="pref_category_search">&pref_category_search3;</string>
-  <string name="pref_category_search_summary">&pref_category_search_summary;</string>
+  <string name="pref_category_search_summary">&pref_category_search_summary2;</string>
   <string name="pref_category_accessibility">&pref_category_accessibility;</string>
   <string name="pref_category_accessibility_summary">&pref_category_accessibility_summary2;</string>
   <string name="pref_category_privacy_short">&pref_category_privacy_short;</string>
-  <string name="pref_category_privacy_summary">&pref_category_privacy_summary3;</string>
+  <string name="pref_category_privacy_summary">&pref_category_privacy_summary4;</string>
   <string name="pref_category_vendor">&pref_category_vendor2;</string>
   <string name="pref_category_vendor_summary">&pref_category_vendor_summary2;</string>
   <string name="pref_category_datareporting">&pref_category_datareporting;</string>
   <string name="pref_category_logins">&pref_category_logins;</string>
   <string name="pref_category_installed_search_engines">&pref_category_installed_search_engines;</string>
   <string name="pref_category_add_search_providers">&pref_category_add_search_providers;</string>
   <string name="pref_category_search_restore_defaults">&pref_category_search_restore_defaults;</string>
   <string name="pref_search_restore_defaults">&pref_search_restore_defaults;</string>
@@ -156,17 +149,17 @@
   <string name="pref_search_hint">&pref_search_hint2;</string>
 
   <string name="pref_category_language">&pref_category_language;</string>
   <string name="pref_category_language_summary">&pref_category_language_summary;</string>
   <string name="pref_browser_locale">&pref_browser_locale;</string>
   <string name="locale_system_default">&locale_system_default;</string>
 
   <string name="pref_category_advanced">&pref_category_advanced;</string>
-  <string name="pref_category_advanced_summary">&pref_category_advanced_summary2;</string>
+  <string name="pref_category_advanced_summary">&pref_category_advanced_summary3;</string>
   <string name="pref_developer_remotedebugging_usb">&pref_developer_remotedebugging_usb;</string>
   <string name="pref_developer_remotedebugging_wifi">&pref_developer_remotedebugging_wifi;</string>
   <string name="pref_developer_remotedebugging_wifi_disabled_summary">&pref_developer_remotedebugging_wifi_disabled_summary;</string>
 
   <string name="pref_category_notifications">&pref_category_notifications;</string>
   <string name="pref_category_notifications_summary">&pref_category_notifications_summary;</string>
   <string name="pref_content_notifications">&pref_content_notifications;</string>
   <string name="pref_content_notifications_summary">&pref_content_notifications_summary2;</string>
@@ -301,19 +294,28 @@
   <string name="tab_queue_notification_title">&tab_queue_notification_title;</string>
   <string name="tab_queue_notification_settings">&tab_queue_notification_settings;</string>
 
   <string name="content_notification_summary">&content_notification_summary;</string>
   <string name="content_notification_title_plural">&content_notification_title_plural;</string>
   <string name="content_notification_action_settings">&content_notification_action_settings;</string>
   <string name="content_notification_updated_on">&content_notification_updated_on;</string>
 
+  <string name="pref_default_browser">&pref_default_browser;</string>
+
   <string name="pref_about_firefox">&pref_about_firefox;</string>
+
   <string name="pref_vendor_faqs">&pref_vendor_faqs;</string>
+  <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/faq -->
+  <string name="faq_link">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/faq</string>
+
   <string name="pref_vendor_feedback">&pref_vendor_feedback;</string>
+  <!-- https://input.mozilla.org/feedback/android/%VERSION%/%CHANNEL%/?utm_source=feedback-settings
+       This should be kept in sync with the "app.feedbackURL" pref defined in mobile.js -->
+  <string name="feedback_link">https://input.mozilla.org/feedback/android/&formatS1;/&formatS2;/?utm_source=feedback-settings</string>
 
   <string name="pref_dialog_set_default">&pref_dialog_set_default;</string>
   <string name="pref_default">&pref_dialog_default;</string>
   <string name="pref_dialog_remove">&pref_dialog_remove;</string>
 
   <string name="pref_search_last_toast">&pref_search_last_toast;</string>
 
   <string name="pref_panels_show">&pref_panels_show;</string>
--- a/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java
@@ -55,16 +55,19 @@ public class AutopushClient {
     protected static final String[] REGISTER_USER_AGENT_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UAID, JSON_KEY_SECRET, JSON_KEY_CHANNEL_ID, JSON_KEY_ENDPOINT };
     protected static final String[] REGISTER_CHANNEL_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_CHANNEL_ID, JSON_KEY_ENDPOINT };
 
     public static final String JSON_KEY_CODE = "code";
     public static final String JSON_KEY_ERRNO = "errno";
     public static final String JSON_KEY_ERROR = "error";
     public static final String JSON_KEY_MESSAGE = "message";
 
+    protected static final String[] requiredErrorStringFields = { JSON_KEY_ERROR, JSON_KEY_MESSAGE };
+    protected static final String[] requiredErrorLongFields = { JSON_KEY_CODE, JSON_KEY_ERRNO };
+
     /**
      * The server's URI.
      * <p>
      * We assume throughout that this ends with a trailing slash (and guarantee as
      * much in the constructor).
      */
     public final String serverURI;
 
@@ -131,29 +134,30 @@ public class AutopushClient {
      * @return response's HTTP status code.
      * @throws AutopushClientException
      */
     public static int validateResponse(HttpResponse response) throws AutopushClientException {
         final int status = response.getStatusLine().getStatusCode();
         if (200 <= status && status <= 299) {
             return status;
         }
-        int code;
-        int errno;
+        long code;
+        long errno;
         String error;
         String message;
         String info;
         ExtendedJSONObject body;
         try {
             body = new SyncStorageResponse(response).jsonObjectBody();
             // TODO: The service doesn't do the right thing yet :(
             // body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
-            // body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
-            code = body.getLong(JSON_KEY_CODE).intValue();
-            errno = body.getLong(JSON_KEY_ERRNO).intValue();
+            body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
+            // Would throw above if missing; the -1 defaults quiet NPE warnings.
+            code = body.getLong(JSON_KEY_CODE, -1);
+            errno = body.getLong(JSON_KEY_ERRNO, -1);
             error = body.getString(JSON_KEY_ERROR);
             message = body.getString(JSON_KEY_MESSAGE);
         } catch (Exception e) {
             throw new AutopushClientException.AutopushClientMalformedResponseException(response);
         }
         throw new AutopushClientException.AutopushClientRemoteException(response, code, errno, error, message, body);
     }
 
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupController.java
@@ -0,0 +1,92 @@
+/*
+ * 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.cleanup;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.ArrayList;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.atMost;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+/**
+ * Tests functionality of the {@link FileCleanupController}.
+ */
+@RunWith(TestRunner.class)
+public class TestFileCleanupController {
+
+    @Test
+    public void testStartIfReadyEmptySharedPrefsRunsCleanup() {
+        final Context context = mock(Context.class);
+        FileCleanupController.startIfReady(context, getSharedPreferences(), "");
+        verify(context).startService(any(Intent.class));
+    }
+
+    @Test
+    public void testStartIfReadyLastRunNowDoesNotRun() {
+        final SharedPreferences sharedPrefs = getSharedPreferences();
+        sharedPrefs.edit()
+                .putLong(FileCleanupController.PREF_LAST_CLEANUP_MILLIS, System.currentTimeMillis())
+                .commit(); // synchronous to finish before test runs.
+
+        final Context context = mock(Context.class);
+        FileCleanupController.startIfReady(context, sharedPrefs, "");
+
+        verify(context, never()).startService((any(Intent.class)));
+    }
+
+    /**
+     * Depends on {@link #testStartIfReadyEmptySharedPrefsRunsCleanup()} success –
+     * i.e. we expect the cleanup to run with empty prefs.
+     */
+    @Test
+    public void testStartIfReadyDoesNotRunTwiceInSuccession() {
+        final Context context = mock(Context.class);
+        final SharedPreferences sharedPrefs = getSharedPreferences();
+
+        FileCleanupController.startIfReady(context, sharedPrefs, "");
+        verify(context).startService(any(Intent.class));
+
+        // Note: the Controller relies on SharedPrefs.apply, but
+        // robolectric made this a synchronous call. Yay!
+        FileCleanupController.startIfReady(context, sharedPrefs, "");
+        verify(context, atMost(1)).startService(any(Intent.class));
+    }
+
+    @Test
+    public void testGetFilesToCleanupContainsProfilePath() {
+        final String profilePath = "/a/profile/path";
+        final ArrayList<String> fileList = FileCleanupController.getFilesToCleanup(profilePath);
+        assertNotNull("Returned file list is non-null", fileList);
+
+        boolean atLeastOneStartsWithProfilePath = false;
+        final String pathToCheck = profilePath + "/"; // Ensure the calling code adds a slash to divide the path.
+        for (final String path : fileList) {
+            if (path.startsWith(pathToCheck)) {
+                // It'd be great if we could assert these individually so
+                // we could display the Strings in console output.
+                atLeastOneStartsWithProfilePath = true;
+            }
+        }
+        assertTrue("At least one returned String starts with a profile path", atLeastOneStartsWithProfilePath);
+    }
+
+    private SharedPreferences getSharedPreferences() {
+        return RuntimeEnvironment.application.getSharedPreferences("TestFileCleanupController", 0);
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/cleanup/TestFileCleanupService.java
@@ -0,0 +1,106 @@
+/*
+ * 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.cleanup;
+
+import android.content.Intent;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * Tests the methods of {@link FileCleanupService}.
+ */
+@RunWith(TestRunner.class)
+public class TestFileCleanupService {
+    @Rule
+    public final TemporaryFolder tempFolder = new TemporaryFolder();
+
+    private void assertAllFilesExist(final List<File> fileList) {
+        for (final File file : fileList) {
+            assertTrue("File exists", file.exists());
+        }
+    }
+
+    private void assertAllFilesDoNotExist(final List<File> fileList) {
+        for (final File file : fileList) {
+            assertFalse("File does not exist", file.exists());
+        }
+    }
+
+    private void onHandleIntent(final ArrayList<String> filePaths) {
+        final FileCleanupService service = new FileCleanupService();
+        final Intent intent = new Intent(FileCleanupService.ACTION_DELETE_FILES);
+        intent.putExtra(FileCleanupService.EXTRA_FILE_PATHS_TO_DELETE, filePaths);
+        service.onHandleIntent(intent);
+    }
+
+    @Test
+    public void testOnHandleIntentDeleteSpecifiedFiles() throws Exception {
+        final int fileListCount = 3;
+        final ArrayList<File> filesToDelete = generateFileList(fileListCount);
+
+        final ArrayList<String> pathsToDelete = new ArrayList<>(fileListCount);
+        for (final File file : filesToDelete) {
+            pathsToDelete.add(file.getAbsolutePath());
+        }
+
+        assertAllFilesExist(filesToDelete);
+        onHandleIntent(pathsToDelete);
+        assertAllFilesDoNotExist(filesToDelete);
+    }
+
+    @Test
+    public void testOnHandleIntentDoesNotDeleteUnrelatedFiles() throws Exception {
+        final ArrayList<File> filesShouldNotBeDeleted = generateFileList(3);
+        assertAllFilesExist(filesShouldNotBeDeleted);
+        onHandleIntent(new ArrayList<String>());
+        assertAllFilesExist(filesShouldNotBeDeleted);
+    }
+
+    @Test
+    public void testOnHandleIntentDeletesEmptyDirectory() throws Exception {
+        final File dir = tempFolder.newFolder();
+        final ArrayList<String> filesToDelete = new ArrayList<>(1);
+        filesToDelete.add(dir.getAbsolutePath());
+
+        assertTrue("Empty directory exists", dir.exists());
+        onHandleIntent(filesToDelete);
+        assertFalse("Empty directory deleted by service", dir.exists());
+    }
+
+    @Test
+    public void testOnHandleIntentDoesNotDeleteNonEmptyDirectory() throws Exception {
+        final File dir = tempFolder.newFolder();
+        final ArrayList<String> filesCannotDelete = new ArrayList<>(1);
+        filesCannotDelete.add(dir.getAbsolutePath());
+        assertTrue("Directory exists", dir.exists());
+
+        final File fileInDir = new File(dir, "file_in_dir");
+        assertTrue("File in dir created", fileInDir.createNewFile());
+
+        onHandleIntent(filesCannotDelete);
+        assertTrue("Non-empty directory not deleted", dir.exists());
+        assertTrue("File in directory not deleted", fileInDir.exists());
+    }
+
+    private ArrayList<File> generateFileList(final int size) throws IOException {
+        final ArrayList<File> fileList = new ArrayList<>(size);
+        for (int i = 0; i < size; ++i) {
+            fileList.add(tempFolder.newFile());
+        }
+        return fileList;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/pings/TestTelemetryPingBuilder.java
@@ -0,0 +1,92 @@
+/*
+ * 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.telemetry.pings;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+
+import static org.junit.Assert.*;
+
+/**
+ * Unit test methods of the {@link TelemetryPingBuilder} class.
+ */
+@RunWith(TestRunner.class)
+public class TestTelemetryPingBuilder {
+    @Test
+    public void testMandatoryFieldsNone() {
+        final NoMandatoryFieldsBuilder builder = new NoMandatoryFieldsBuilder();
+        builder.setNonMandatoryField();
+        assertNotNull("Builder does not throw and returns a non-null value", builder.build());
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testMandatoryFieldsMissing() {
+        final MandatoryFieldsBuilder builder = new MandatoryFieldsBuilder();
+        builder.setNonMandatoryField()
+                .build(); // should throw
+    }
+
+    @Test
+    public void testMandatoryFieldsIncluded() {
+        final MandatoryFieldsBuilder builder = new MandatoryFieldsBuilder();
+        builder.setNonMandatoryField()
+                .setMandatoryField();
+        assertNotNull("Builder does not throw and returns non-null value", builder.build());
+    }
+
+    private static class NoMandatoryFieldsBuilder extends TelemetryPingBuilder {
+        public NoMandatoryFieldsBuilder() {
+            super("");
+        }
+
+        @Override
+        String getDocType() {
+            return "";
+        }
+
+        @Override
+        String[] getMandatoryFields() {
+            return new String[0];
+        }
+
+        public NoMandatoryFieldsBuilder setNonMandatoryField() {
+            payload.put("non-mandatory", true);
+            return this;
+        }
+    }
+
+    private static class MandatoryFieldsBuilder extends TelemetryPingBuilder {
+        private static final String MANDATORY_FIELD = "mandatory-field";
+
+        public MandatoryFieldsBuilder() {
+            super("");
+        }
+
+        @Override
+        String getDocType() {
+            return "";
+        }
+
+        @Override
+        String[] getMandatoryFields() {
+            return new String[] {
+                    MANDATORY_FIELD,
+            };
+        }
+
+        public MandatoryFieldsBuilder setNonMandatoryField() {
+            payload.put("non-mandatory", true);
+            return this;
+        }
+
+        public MandatoryFieldsBuilder setMandatoryField() {
+            payload.put(MANDATORY_FIELD, true);
+            return this;
+        }
+    }
+}
--- a/mobile/android/tests/browser/robocop/testAccessibleCarets.html
+++ b/mobile/android/tests/browser/robocop/testAccessibleCarets.html
@@ -26,10 +26,18 @@
     <input id="LTRinput" style="direction: ltr;" value="Type something">
     <input id="RTLinput" style="direction: rtl;" value="לרוץ במעלה הגבעה">
     <br>
 
     <textarea id="LTRtextarea" style="direction: ltr;"
       rows="3" cols="8">Words in a box</textarea>
     <textarea id="RTLtextarea" style="direction: rtl;"
       rows="3" cols="8">הספר הוא טוב</textarea>
+
+    <br>
+    <input id="LTRphone" style="direction: ltr;" size="40"
+      value="09876543210 .-.)(wp#*1034103410341034X">
+    <br>
+    <input id="RTLphone" style="direction: rtl;" size="40"
+      value="התקשר +972 3 7347514 במשך זמן טוב">
+
   </body>
 </html>
--- a/mobile/android/tests/browser/robocop/testAccessibleCarets.js
+++ b/mobile/android/tests/browser/robocop/testAccessibleCarets.js
@@ -55,43 +55,43 @@ function isInputOrTextarea(element) {
  */
 function elementSelection(element) {
   return (isInputOrTextarea(element)) ?
     element.editor.selection :
     element.ownerDocument.defaultView.getSelection();
 }
 
 /**
- * Select the first character of a target element, w/o affecting focus.
+ * Select the requested character of a target element, w/o affecting focus.
  */
-function selectElementFirstChar(doc, element) {
+function selectElementChar(doc, element, char) {
   if (isInputOrTextarea(element)) {
-    element.setSelectionRange(0, 1);
+    element.setSelectionRange(char, char + 1);
     return;
   }
 
   // Simple test cases designed firstChild == #text node.
   let range = doc.createRange();
-  range.setStart(element.firstChild, 0);
-  range.setEnd(element.firstChild, 1);
+  range.setStart(element.firstChild, char);
+  range.setEnd(element.firstChild, char + 1);
 
   let selection = elementSelection(element);
   selection.removeAllRanges();
   selection.addRange(range);
 }
 
 /**
- * Get longpress point. Determine the midpoint in the first character of
+ * Get longpress point. Determine the midpoint in the requested character of
  * the content in the element. X will be midpoint from left to right.
  * Y will be 1/3 of the height up from the bottom to account for both
  * LTR and smaller RTL characters. ie: |X| vs. |א|
  */
-function getFirstCharPressPoint(doc, element, expected) {
+function getCharPressPoint(doc, element, char, expected) {
   // Select the first char in the element.
-  selectElementFirstChar(doc, element);
+  selectElementChar(doc, element, char);
 
   // Reality check selected char to expected.
   let selection = elementSelection(element);
   is(selection.toString(), expected, "Selected char should match expected char.");
 
   // Return a point where long press should select entire word.
   let rect = selection.getRangeAt(0).getBoundingClientRect();
   let r = new Point(rect.left + (rect.width / 2), rect.bottom - (rect.height / 3));
@@ -157,27 +157,32 @@ add_task(function* testAccessibleCarets(
   let i_LTR_elem = doc.getElementById("LTRinput");
   let ta_LTR_elem = doc.getElementById("LTRtextarea");
 
   let ce_RTL_elem = doc.getElementById("RTLcontenteditable");
   let tc_RTL_elem = doc.getElementById("RTLtextContent");
   let i_RTL_elem = doc.getElementById("RTLinput");
   let ta_RTL_elem = doc.getElementById("RTLtextarea");
 
-  // Locate longpress midpoints for test elements, ensure expactations.
-  let ce_LTR_midPoint = getFirstCharPressPoint(doc, ce_LTR_elem, "F");
-  let tc_LTR_midPoint = getFirstCharPressPoint(doc, tc_LTR_elem, "O");
-  let i_LTR_midPoint = getFirstCharPressPoint(doc, i_LTR_elem, "T");
-  let ta_LTR_midPoint = getFirstCharPressPoint(doc, ta_LTR_elem, "W");
+  let ip_LTR_elem = doc.getElementById("LTRphone");
+  let ip_RTL_elem = doc.getElementById("RTLphone");
 
-  let ce_RTL_midPoint = getFirstCharPressPoint(doc, ce_RTL_elem, "א");
-  let tc_RTL_midPoint = getFirstCharPressPoint(doc, tc_RTL_elem, "ת");
-  let i_RTL_midPoint = getFirstCharPressPoint(doc, i_RTL_elem, "ל");
-  let ta_RTL_midPoint = getFirstCharPressPoint(doc, ta_RTL_elem, "ה");
+  // Locate longpress midpoints for test elements, ensure expactations.
+  let ce_LTR_midPoint = getCharPressPoint(doc, ce_LTR_elem, 0, "F");
+  let tc_LTR_midPoint = getCharPressPoint(doc, tc_LTR_elem, 0, "O");
+  let i_LTR_midPoint = getCharPressPoint(doc, i_LTR_elem, 0, "T");
+  let ta_LTR_midPoint = getCharPressPoint(doc, ta_LTR_elem, 0, "W");
 
+  let ce_RTL_midPoint = getCharPressPoint(doc, ce_RTL_elem, 0, "א");
+  let tc_RTL_midPoint = getCharPressPoint(doc, tc_RTL_elem, 0, "ת");
+  let i_RTL_midPoint = getCharPressPoint(doc, i_RTL_elem, 0, "ל");
+  let ta_RTL_midPoint = getCharPressPoint(doc, ta_RTL_elem, 0, "ה");
+
+  let ip_LTR_midPoint = getCharPressPoint(doc, ip_LTR_elem, 8, "2");
+  let ip_RTL_midPoint = getCharPressPoint(doc, ip_RTL_elem, 9, "2");
 
   // Longpress various LTR content elements. Test focused element against
   // expected, and selected text against expected.
   let result = getLongPressResult(browser, ce_LTR_midPoint);
   is(result.focusedElement, ce_LTR_elem, "Focused element should match expected.");
   is(result.text, "Find", "Selected text should match expected text.");
 
   result = getLongPressResult(browser, tc_LTR_midPoint);
@@ -187,16 +192,23 @@ add_task(function* testAccessibleCarets(
   result = getLongPressResult(browser, i_LTR_midPoint);
   is(result.focusedElement, i_LTR_elem, "Focused element should match expected.");
   is(result.text, "Type", "Selected text should match expected text.");
 
   result = getLongPressResult(browser, ta_LTR_midPoint);
   is(result.focusedElement, ta_LTR_elem, "Focused element should match expected.");
   is(result.text, "Words", "Selected text should match expected text.");
 
+  result = getLongPressResult(browser, ip_LTR_midPoint);
+  is(result.focusedElement, ip_LTR_elem, "Focused element should match expected.");
+  is(result.text, "09876543210 .-.)(wp#*103410341",
+    "Selected phone number should match expected text.");
+  is(result.text.length, 30,
+    "Selected phone number length should match expected maximum.");
+
   // Longpress various RTL content elements. Test focused element against
   // expected, and selected text against expected.
   result = getLongPressResult(browser, ce_RTL_midPoint);
   is(result.focusedElement, ce_RTL_elem, "Focused element should match expected.");
   is(result.text, "איפה", "Selected text should match expected text.");
 
   result = getLongPressResult(browser, tc_RTL_midPoint);
   is(result.focusedElement, null, "No focused element is expected.");
@@ -205,12 +217,17 @@ add_task(function* testAccessibleCarets(
   result = getLongPressResult(browser, i_RTL_midPoint);
   is(result.focusedElement, i_RTL_elem, "Focused element should match expected.");
   is(result.text, "לרוץ", "Selected text should match expected text.");
 
   result = getLongPressResult(browser, ta_RTL_midPoint);
   is(result.focusedElement, ta_RTL_elem, "Focused element should match expected.");
   is(result.text, "הספר", "Selected text should match expected text.");
 
+  result = getLongPressResult(browser, ip_RTL_midPoint);
+  is(result.focusedElement, ip_RTL_elem, "Focused element should match expected.");
+  is(result.text, "+972 3 7347514 ",
+    "Selected phone number should match expected text.");
+
   ok(true, "Finished all tests.");
 });
 
 run_next_test();
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -416,17 +416,18 @@ FxAccountsInternal.prototype = {
   /**
    * Ask the server whether the user's email has been verified
    */
   checkEmailStatus: function checkEmailStatus(sessionToken, options = {}) {
     if (!sessionToken) {
       return Promise.reject(new Error(
         "checkEmailStatus called without a session token"));
     }
-    return this.fxAccountsClient.recoveryEmailStatus(sessionToken, options);
+    return this.fxAccountsClient.recoveryEmailStatus(sessionToken,
+      options).catch(error => this._handleTokenError(error));
   },
 
   /**
    * Once the user's email is verified, we can request the keys
    */
   fetchKeys: function fetchKeys(keyFetchToken) {
     log.debug("fetchKeys: " + !!keyFetchToken);
     if (logPII) {
@@ -668,20 +669,21 @@ FxAccountsInternal.prototype = {
     log.trace('checkVerificationStatus');
     let currentState = this.currentAccountState;
     return currentState.getUserAccountData().then(data => {
       if (!data) {
         log.trace("checkVerificationStatus - no user data");
         return null;
       }
 
-      if (!this.isUserEmailVerified(data)) {
-        log.trace("checkVerificationStatus - forcing verification status check");
-        this.pollEmailStatus(currentState, data.sessionToken, "push");
-      }
+      // Always check the verification status, even if the local state indicates
+      // we're already verified. If the user changed their password, the check
+      // will fail, and we'll enter the reauth state.
+      log.trace("checkVerificationStatus - forcing verification status check");
+      return this.pollEmailStatus(currentState, data.sessionToken, "push");
     });
   },
 
   _destroyOAuthToken: function(tokenData) {
     let client = new FxAccountsOAuthGrantClient({
       serverURL: tokenData.server,
       client_id: FX_OAUTH_CLIENT_ID
     });
@@ -1094,17 +1096,19 @@ FxAccountsInternal.prototype = {
         // handle a rejection" messages, so add an error handler directly
         // on the promise to log the error.
         currentState.whenVerifiedDeferred.promise.then(null, err => {
           log.info("the wait for user verification was stopped: " + err);
         });
       }
     }
 
-    this.checkEmailStatus(sessionToken, { reason: why })
+    // We return a promise for testing only. Other callers can ignore this,
+    // since verification polling continues in the background.
+    return this.checkEmailStatus(sessionToken, { reason: why })
       .then((response) => {
         log.debug("checkEmailStatus -> " + JSON.stringify(response));
         if (response && response.verified) {
           currentState.updateUserAccountData({ verified: true })
             .then(() => {
               return currentState.getUserAccountData();
             })
             .then(data => {
--- a/services/fxaccounts/FxAccountsWebChannel.jsm
+++ b/services/fxaccounts/FxAccountsWebChannel.jsm
@@ -34,16 +34,36 @@ const COMMAND_LOGOUT               = "fx
 const COMMAND_DELETE               = "fxaccounts:delete";
 const COMMAND_SYNC_PREFERENCES     = "fxaccounts:sync_preferences";
 const COMMAND_CHANGE_PASSWORD      = "fxaccounts:change_password";
 
 const PREF_LAST_FXA_USER           = "identity.fxaccounts.lastSignedInUserHash";
 const PREF_SYNC_SHOW_CUSTOMIZATION = "services.sync-setup.ui.showCustomizationDialog";
 
 /**
+ * A helper function that extracts the message and stack from an error object.
+ * Returns a `{ message, stack }` tuple. `stack` will be null if the error
+ * doesn't have a stack trace.
+ */
+function getErrorDetails(error) {
+  let details = { message: String(error), stack: null };
+
+  // Adapted from Console.jsm.
+  if (error.stack) {
+    let frames = [];
+    for (let frame = error.stack; frame; frame = frame.caller) {
+      frames.push(String(frame).padStart(4));
+    }
+    details.stack = frames.join("\n");
+  }
+
+  return details;
+}
+
+/**
  * Create a new FxAccountsWebChannel to listen for account updates
  *
  * @param {Object} options Options
  *   @param {Object} options
  *     @param {String} options.content_uri
  *     The FxA Content server uri
  *     @param {String} options.channel_id
  *     The ID of the WebChannel
@@ -111,16 +131,69 @@ this.FxAccountsWebChannel.prototype = {
       this._webChannelOrigin = Services.io.newURI(this._contentUri, null, null);
       this._registerChannel();
     } catch (e) {
       log.error(e);
       throw e;
     }
   },
 
+  _receiveMessage(message, sendingContext) {
+    let command = message.command;
+    let data = message.data;
+
+    switch (command) {
+      case COMMAND_PROFILE_CHANGE:
+        Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, data.uid);
+        break;
+      case COMMAND_LOGIN:
+        this._helpers.login(data).catch(error =>
+          this._sendError(error, message, sendingContext));
+        break;
+      case COMMAND_LOGOUT:
+      case COMMAND_DELETE:
+        this._helpers.logout(data.uid).catch(error =>
+          this._sendError(error, message, sendingContext));
+        break;
+      case COMMAND_CAN_LINK_ACCOUNT:
+        let canLinkAccount = this._helpers.shouldAllowRelink(data.email);
+
+        let response = {
+          command: command,
+          messageId: message.messageId,
+          data: { ok: canLinkAccount }
+        };
+
+        log.debug("FxAccountsWebChannel response", response);
+        this._channel.send(response, sendingContext);
+        break;
+      case COMMAND_SYNC_PREFERENCES:
+        this._helpers.openSyncPreferences(sendingContext.browser, data.entryPoint);
+        break;
+      case COMMAND_CHANGE_PASSWORD:
+        this._helpers.changePassword(data).catch(error =>
+          this._sendError(error, message, sendingContext));
+        break;
+      default:
+        log.warn("Unrecognized FxAccountsWebChannel command", command);
+        break;
+    }
+  },
+
+  _sendError(error, incomingMessage, sendingContext) {
+    log.error("Failed to handle FxAccountsWebChannel message", error);
+    this._channel.send({
+      command: incomingMessage.command,
+      messageId: incomingMessage.messageId,
+      data: {
+        error: getErrorDetails(error),
+      },
+    }, sendingContext);
+  },
+
   /**
    * Create a new channel with the WebChannelBroker, setup a callback listener
    * @private
    */
   _registerChannel() {
     /**
      * Processes messages that are called back from the FxAccountsChannel
      *
@@ -141,51 +214,20 @@ this.FxAccountsWebChannel.prototype = {
      *
      */
     let listener = (webChannelId, message, sendingContext) => {
       if (message) {
         log.debug("FxAccountsWebChannel message received", message.command);
         if (logPII) {
           log.debug("FxAccountsWebChannel message details", message);
         }
-        let command = message.command;
-        let data = message.data;
-
-        switch (command) {
-          case COMMAND_PROFILE_CHANGE:
-            Services.obs.notifyObservers(null, ON_PROFILE_CHANGE_NOTIFICATION, data.uid);
-            break;
-          case COMMAND_LOGIN:
-            this._helpers.login(data);
-            break;
-          case COMMAND_LOGOUT:
-          case COMMAND_DELETE:
-            this._helpers.logout(data.uid);
-            break;
-          case COMMAND_CAN_LINK_ACCOUNT:
-            let canLinkAccount = this._helpers.shouldAllowRelink(data.email);
-
-            let response = {
-              command: command,
-              messageId: message.messageId,
-              data: { ok: canLinkAccount }
-            };
-
-            log.debug("FxAccountsWebChannel response", response);
-            this._channel.send(response, sendingContext);
-            break;
-          case COMMAND_SYNC_PREFERENCES:
-            this._helpers.openSyncPreferences(sendingContext.browser, data.entryPoint);
-            break;
-          case COMMAND_CHANGE_PASSWORD:
-            this._helpers.changePassword(data);
-            break;
-          default:
-            log.warn("Unrecognized FxAccountsWebChannel command", command);
-            break;
+        try {
+          this._receiveMessage(message, sendingContext);
+        } catch (error) {
+          this._sendError(error, message, sendingContext);
         }
       }
     };
 
     this._channelCallback = listener;
     this._channel = new WebChannel(this._webChannelId, this._webChannelOrigin);
     this._channel.listen(listener);
     log.debug("FxAccountsWebChannel registered: " + this._webChannelId + " with origin " + this._webChannelOrigin.prePath);
@@ -294,19 +336,17 @@ this.FxAccountsWebChannelHelpers.prototy
     let newCredentials = {};
     for (let name of Object.keys(credentials)) {
       if (name == "email" || name == "uid" || FxAccountsStorageManagerCanStoreField(name)) {
         newCredentials[name] = credentials[name];
       } else {
         log.info("changePassword ignoring unsupported field", name);
       }
     }
-    this._fxAccounts.updateUserAccountData(newCredentials).catch(err => {
-      log.error("Failed to update account data on password change", err);
-    });
+    return this._fxAccounts.updateUserAccountData(newCredentials);
   },
 
   /**
    * Get the hash of account name of the previously signed in account
    */
   getPreviousAccountNameHashPref() {
     try {
       return Services.prefs.getComplexValue(PREF_LAST_FXA_USER, Ci.nsISupportsString).data;
--- a/services/fxaccounts/tests/xpcshell/test_accounts.js
+++ b/services/fxaccounts/tests/xpcshell/test_accounts.js
@@ -1433,16 +1433,43 @@ add_test(function test_getSignedInUserPr
   fxa.getSignedInUserProfile()
     .catch(error => {
        do_check_eq(error.message, "NO_ACCOUNT");
        fxa.signOut().then(run_next_test);
     });
 
 });
 
+add_task(function* test_checkVerificationStatusFailed() {
+  let fxa = new MockFxAccounts();
+  let alice = getTestUser("alice");
+  alice.verified = true;
+
+  let client = fxa.internal.fxAccountsClient;
+  client.recoveryEmailStatus = () => {
+    return Promise.reject({
+      code: 401,
+      errno: ERRNO_INVALID_AUTH_TOKEN,
+    });
+  };
+  client.accountStatus = () => Promise.resolve(true);
+
+  yield fxa.setSignedInUser(alice);
+  let user = yield fxa.internal.getUserAccountData();
+  do_check_neq(alice.sessionToken, null);
+  do_check_eq(user.email, alice.email);
+  do_check_eq(user.verified, true);
+
+  yield fxa.checkVerificationStatus();
+
+  user = yield fxa.internal.getUserAccountData();
+  do_check_eq(user.email, alice.email);
+  do_check_eq(user.sessionToken, null);
+});
+
 /*
  * End of tests.
  * Utility functions follow.
  */
 
 function expandHex(two_hex) {
   // Return a 64-character hex string, encoding 32 identical bytes.
   let eight_hex = two_hex + two_hex + two_hex + two_hex;
--- a/services/fxaccounts/tests/xpcshell/test_web_channel.js
+++ b/services/fxaccounts/tests/xpcshell/test_web_channel.js
@@ -33,16 +33,90 @@ add_test(function () {
   validationHelper({
     content_uri: URL_STRING
   },
   'Error: Missing \'channel_id\' option');
 
   run_next_test();
 });
 
+add_task(function* test_rejection_reporting() {
+  let mockMessage = {
+    command: 'fxaccounts:login',
+    messageId: '1234',
+    data: { email: 'testuser@testuser.com' },
+  };
+
+  let channel = new FxAccountsWebChannel({
+    channel_id: WEBCHANNEL_ID,
+    content_uri: URL_STRING,
+    helpers: {
+      login(accountData) {
+        equal(accountData.email, 'testuser@testuser.com',
+          'Should forward incoming message data to the helper');
+        return Promise.reject(new Error('oops'));
+      },
+    },
+  });
+
+  let promiseSend = new Promise(resolve => {
+    channel._channel.send = (message, context) => {
+      resolve({ message, context });
+    };
+  });
+
+  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
+
+  let { message, context } = yield promiseSend;
+
+  equal(context, mockSendingContext, 'Should forward the original context');
+  equal(message.command, 'fxaccounts:login',
+    'Should include the incoming command');
+  equal(message.messageId, '1234', 'Should include the message ID');
+  equal(message.data.error.message, 'Error: oops',
+    'Should convert the error message to a string');
+  notStrictEqual(message.data.error.stack, null,
+    'Should include the stack for JS error rejections');
+});
+
+add_test(function test_exception_reporting() {
+  let mockMessage = {
+    command: 'fxaccounts:sync_preferences',
+    messageId: '5678',
+    data: { entryPoint: 'fxa:verification_complete' }
+  };
+
+  let channel = new FxAccountsWebChannel({
+    channel_id: WEBCHANNEL_ID,
+    content_uri: URL_STRING,
+    helpers: {
+      openSyncPreferences(browser, entryPoint) {
+        equal(entryPoint, 'fxa:verification_complete',
+          'Should forward incoming message data to the helper');
+        throw new TypeError('splines not reticulated');
+      },
+    },
+  });
+
+  channel._channel.send = (message, context) => {
+    equal(context, mockSendingContext, 'Should forward the original context');
+    equal(message.command, 'fxaccounts:sync_preferences',
+      'Should include the incoming command');
+    equal(message.messageId, '5678', 'Should include the message ID');
+    equal(message.data.error.message, 'TypeError: splines not reticulated',
+      'Should convert the exception to a string');
+    notStrictEqual(message.data.error.stack, null,
+      'Should include the stack for JS exceptions');
+
+    run_next_test();
+  };
+
+  channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
+});
+
 add_test(function test_profile_image_change_message() {
   var mockMessage = {
     command: "profile:change",
     data: { uid: "foo" }
   };
 
   makeObserver(ON_PROFILE_CHANGE_NOTIFICATION, function (subject, topic, data) {
     do_check_eq(data, "foo");
@@ -65,16 +139,17 @@ add_test(function test_login_message() {
 
   let channel = new FxAccountsWebChannel({
     channel_id: WEBCHANNEL_ID,
     content_uri: URL_STRING,
     helpers: {
       login: function (accountData) {
         do_check_eq(accountData.email, 'testuser@testuser.com');
         run_next_test();
+        return Promise.resolve();
       }
     }
   });
 
   channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
 });
 
 add_test(function test_logout_message() {
@@ -85,16 +160,17 @@ add_test(function test_logout_message() 
 
   let channel = new FxAccountsWebChannel({
     channel_id: WEBCHANNEL_ID,
     content_uri: URL_STRING,
     helpers: {
       logout: function (uid) {
         do_check_eq(uid, 'foo');
         run_next_test();
+        return Promise.resolve();
       }
     }
   });
 
   channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
 });
 
 add_test(function test_delete_message() {
@@ -105,16 +181,17 @@ add_test(function test_delete_message() 
 
   let channel = new FxAccountsWebChannel({
     channel_id: WEBCHANNEL_ID,
     content_uri: URL_STRING,
     helpers: {
       logout: function (uid) {
         do_check_eq(uid, 'foo');
         run_next_test();
+        return Promise.resolve();
       }
     }
   });
 
   channel._channelCallback(WEBCHANNEL_ID, mockMessage, mockSendingContext);
 });
 
 add_test(function test_can_link_account_message() {
@@ -194,113 +271,119 @@ add_test(function test_helpers_should_al
   };
 
   do_check_true(helpers.shouldAllowRelink('allowed_to_relink@testuser.com'));
   do_check_false(helpers.shouldAllowRelink('not_allowed_to_relink@testuser.com'));
 
   run_next_test();
 });
 
-add_test(function test_helpers_login_without_customize_sync() {
+add_task(function* test_helpers_login_without_customize_sync() {
   let helpers = new FxAccountsWebChannelHelpers({
     fxAccounts: {
       setSignedInUser: function(accountData) {
-        // ensure fxAccounts is informed of the new user being signed in.
-        do_check_eq(accountData.email, 'testuser@testuser.com');
+        return new Promise(resolve => {
+          // ensure fxAccounts is informed of the new user being signed in.
+          do_check_eq(accountData.email, 'testuser@testuser.com');
 
-        // verifiedCanLinkAccount should be stripped in the data.
-        do_check_false('verifiedCanLinkAccount' in accountData);
+          // verifiedCanLinkAccount should be stripped in the data.
+          do_check_false('verifiedCanLinkAccount' in accountData);
 
-        // the customizeSync pref should not update
-        do_check_false(helpers.getShowCustomizeSyncPref());
+          // the customizeSync pref should not update
+          do_check_false(helpers.getShowCustomizeSyncPref());
 
-        // previously signed in user preference is updated.
-        do_check_eq(helpers.getPreviousAccountNameHashPref(), helpers.sha256('testuser@testuser.com'));
+          // previously signed in user preference is updated.
+          do_check_eq(helpers.getPreviousAccountNameHashPref(), helpers.sha256('testuser@testuser.com'));
 
-        run_next_test();
+          resolve();
+        });
       }
     }
   });
 
   // the show customize sync pref should stay the same
   helpers.setShowCustomizeSyncPref(false);
 
   // ensure the previous account pref is overwritten.
   helpers.setPreviousAccountNameHashPref('lastuser@testuser.com');
 
-  helpers.login({
+  yield helpers.login({
     email: 'testuser@testuser.com',
     verifiedCanLinkAccount: true,
     customizeSync: false
   });
 });
 
-add_test(function test_helpers_login_with_customize_sync() {
+add_task(function* test_helpers_login_with_customize_sync() {
   let helpers = new FxAccountsWebChannelHelpers({
     fxAccounts: {
       setSignedInUser: function(accountData) {
-        // ensure fxAccounts is informed of the new user being signed in.
-        do_check_eq(accountData.email, 'testuser@testuser.com');
+        return new Promise(resolve => {
+          // ensure fxAccounts is informed of the new user being signed in.
+          do_check_eq(accountData.email, 'testuser@testuser.com');
 
-        // customizeSync should be stripped in the data.
-        do_check_false('customizeSync' in accountData);
+          // customizeSync should be stripped in the data.
+          do_check_false('customizeSync' in accountData);
 
-        // the customizeSync pref should not update
-        do_check_true(helpers.getShowCustomizeSyncPref());
+          // the customizeSync pref should not update
+          do_check_true(helpers.getShowCustomizeSyncPref());
 
-        run_next_test();
+          resolve();
+        });
       }
     }
   });
 
   // the customize sync pref should be overwritten
   helpers.setShowCustomizeSyncPref(false);
 
-  helpers.login({
+  yield helpers.login({
     email: 'testuser@testuser.com',
     verifiedCanLinkAccount: true,
     customizeSync: true
   });
 });
 
-add_test(function test_helpers_login_with_customize_sync_and_declined_engines() {
+add_task(function* test_helpers_login_with_customize_sync_and_declined_engines() {
   let helpers = new FxAccountsWebChannelHelpers({
     fxAccounts: {
       setSignedInUser: function(accountData) {
-        // ensure fxAccounts is informed of the new user being signed in.
-        do_check_eq(accountData.email, 'testuser@testuser.com');
+        return new Promise(resolve => {
+          // ensure fxAccounts is informed of the new user being signed in.
+          do_check_eq(accountData.email, 'testuser@testuser.com');
 
-        // customizeSync should be stripped in the data.
-        do_check_false('customizeSync' in accountData);
-        do_check_false('declinedSyncEngines' in accountData);
-        do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), false);
-        do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true);
-        do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true);
-        do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true);
-        do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), false);
-        do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true);
+          // customizeSync should be stripped in the data.
+          do_check_false('customizeSync' in accountData);
+          do_check_false('declinedSyncEngines' in accountData);
+          do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), false);
+          do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true);
+          do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true);
+          do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true);
+          do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), false);
+          do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true);
 
-        // the customizeSync pref should be disabled
-        do_check_false(helpers.getShowCustomizeSyncPref());
+          // the customizeSync pref should be disabled
+          do_check_false(helpers.getShowCustomizeSyncPref());
 
-        run_next_test();
+          resolve();
+        });
       }
     }
   });
 
   // the customize sync pref should be overwritten
   helpers.setShowCustomizeSyncPref(true);
 
   do_check_eq(Services.prefs.getBoolPref("services.sync.engine.addons"), true);
   do_check_eq(Services.prefs.getBoolPref("services.sync.engine.bookmarks"), true);
   do_check_eq(Services.prefs.getBoolPref("services.sync.engine.history"), true);
   do_check_eq(Services.prefs.getBoolPref("services.sync.engine.passwords"), true);
   do_check_eq(Services.prefs.getBoolPref("services.sync.engine.prefs"), true);
   do_check_eq(Services.prefs.getBoolPref("services.sync.engine.tabs"), true);
-  helpers.login({
+  yield helpers.login({
     email: 'testuser@testuser.com',
     verifiedCanLinkAccount: true,
     customizeSync: true,
     declinedSyncEngines: ['addons', 'prefs']
   });
 });
 
 add_test(function test_helpers_open_sync_preferences() {
@@ -314,34 +397,36 @@ add_test(function test_helpers_open_sync
       do_check_eq(uri, "about:preferences?entrypoint=fxa%3Averification_complete#sync");
       run_next_test();
     }
   };
 
   helpers.openSyncPreferences(mockBrowser, "fxa:verification_complete");
 });
 
-add_test(function test_helpers_change_password() {
+add_task(function* test_helpers_change_password() {
   let updateCalled = false;
   let helpers = new FxAccountsWebChannelHelpers({
     fxAccounts: {
       updateUserAccountData(credentials) {
-        do_check_true(credentials.hasOwnProperty("email"));
-        do_check_true(credentials.hasOwnProperty("uid"));
-        do_check_true(credentials.hasOwnProperty("kA"));
-        // "foo" isn't a field known by storage, so should be dropped.
-        do_check_false(credentials.hasOwnProperty("foo"));
-        updateCalled = true;
-        return Promise.resolve();
+        return new Promise(resolve => {
+          do_check_true(credentials.hasOwnProperty("email"));
+          do_check_true(credentials.hasOwnProperty("uid"));
+          do_check_true(credentials.hasOwnProperty("kA"));
+          // "foo" isn't a field known by storage, so should be dropped.
+          do_check_false(credentials.hasOwnProperty("foo"));
+          updateCalled = true;
+
+          resolve();
+        });
       }
     }
   });
-  helpers.changePassword({ email: "email", uid: "uid", kA: "kA", foo: "foo" });
+  yield helpers.changePassword({ email: "email", uid: "uid", kA: "kA", foo: "foo" });
   do_check_true(updateCalled);
-  run_next_test();
 });
 
 function run_test() {
   run_next_test();
 }
 
 function makeObserver(aObserveTopic, aObserveFunc) {
   let callback = function (aSubject, aTopic, aData) {
--- a/testing/eslint-plugin-mozilla/lib/rules/no-cpows-in-tests.js
+++ b/testing/eslint-plugin-mozilla/lib/rules/no-cpows-in-tests.js
@@ -17,34 +17,61 @@ var helpers = require("../helpers");
 var cpows = [
   /^gBrowser\.contentWindow/,
   /^gBrowser\.contentDocument/,
   /^gBrowser\.selectedBrowser.contentWindow/,
   /^browser\.contentDocument/,
   /^window\.content/
 ];
 
+var isInContentTask = false;
+
 module.exports = function(context) {
   // ---------------------------------------------------------------------------
   // Helpers
   // ---------------------------------------------------------------------------
 
   function showError(node, identifier) {
+    if (isInContentTask) {
+      return;
+    }
+
     context.report({
       node: node,
       message: identifier +
                " is a possible Cross Process Object Wrapper (CPOW)."
     });
   }
 
+  function isContentTask(node) {
+    return node &&
+           node.type === "MemberExpression" &&
+           node.property.type === "Identifier" &&
+           node.property.name === "spawn" &&
+           node.object.type === "Identifier" &&
+           node.object.name === "ContentTask";
+  }
+
   // ---------------------------------------------------------------------------
   // Public
   // ---------------------------------------------------------------------------
 
   return {
+    CallExpression: function(node) {
+      if (isContentTask(node.callee)) {
+        isInContentTask = true;
+      }
+    },
+
+    "CallExpression:exit": function(node) {
+      if (isContentTask(node.callee)) {
+        isInContentTask = false;
+      }
+    },
+
     MemberExpression: function(node) {
       if (!helpers.getIsBrowserMochitest(this)) {
         return;
       }
 
       var expression = context.getSource(node);
 
       // Only report a single CPOW error per node -- so if checking
@@ -72,15 +99,14 @@ module.exports = function(context) {
       var expression = context.getSource(node);
       if (expression == "content" || /^content\./.test(expression)) {
         if (node.parent.type === "MemberExpression" &&
             node.parent.object &&
             node.parent.object.type === "Identifier" &&
             node.parent.object.name != "content") {
           return;
         }
-
         showError(node, expression);
         return;
       }
     }
   };
 };
--- a/toolkit/components/extensions/ext-extension.js
+++ b/toolkit/components/extensions/ext-extension.js
@@ -33,12 +33,16 @@ extensions.registerSchemaAPI("extension"
 
       get inIncognitoContext() {
         return context.incognito;
       },
 
       isAllowedIncognitoAccess() {
         return Promise.resolve(true);
       },
+
+      isAllowedFileSchemeAccess() {
+        return Promise.resolve(true);
+      },
     },
   };
 });
 
--- a/toolkit/components/extensions/schemas/extension.json
+++ b/toolkit/components/extensions/schemas/extension.json
@@ -112,17 +112,16 @@
                 "description": "True if the extension has access to Incognito mode, false otherwise."
               }
             ]
           }
         ]
       },
       {
         "name": "isAllowedFileSchemeAccess",
-        "unsupported": true,
         "type": "function",
         "description": "Retrieves the state of the extension's access to the 'file://' scheme (as determined by the user-controlled 'Allow access to File URLs' checkbox.",
         "async": "callback",
         "parameters": [
           {
             "type": "function",
             "name": "callback",
             "parameters": [
--- a/toolkit/components/extensions/test/mochitest/test_ext_bookmarks.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_bookmarks.html
@@ -265,17 +265,17 @@ function backgroundScript() {
   }).then(results => {
     browser.test.assertEq(1, results.length, "Expected number of results returned for title search");
     checkBookmark({title: "EFF", url: "http://eff.org/", index: 0, parentId: bookmarkGuids.unfiledGuid}, results[0]);
 
     // finds menu items
     return browser.bookmarks.search("Menu Item");
   }).then(results => {
     browser.test.assertEq(1, results.length, "Expected number of results returned for menu item search");
-    checkBookmark({title: "Menu Item", url: "http://menu.org/", index: 4, parentId: bookmarkGuids.menuGuid}, results[0]);
+    checkBookmark({title: "Menu Item", url: "http://menu.org/", index: 3, parentId: bookmarkGuids.menuGuid}, results[0]);
 
     // finds toolbar items
     return browser.bookmarks.search("Toolbar Item");
   }).then(results => {
     browser.test.assertEq(1, results.length, "Expected number of results returned for toolbar item search");
     checkBookmark({title: "Toolbar Item", url: "http://toolbar.org/", index: 2, parentId: bookmarkGuids.toolbarGuid}, results[0]);
 
     // finds folders
--- a/toolkit/components/extensions/test/mochitest/test_ext_extension.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_extension.html
@@ -28,12 +28,32 @@ add_task(function* test_is_allowed_incog
 
   yield extension.startup();
   info("extension loaded");
   yield extension.awaitFinish("isAllowedIncognitoAccess");
   yield extension.unload();
   info("extension unloaded");
 });
 
+add_task(function* test_is_allowed_file_scheme_access() {
+  function backgroundScript() {
+    browser.extension.isAllowedFileSchemeAccess().then(isAllowedFileSchemeAccess => {
+      browser.test.assertEq(true, isAllowedFileSchemeAccess, "isAllowedFileSchemeAccess is true");
+      browser.test.notifyPass("isAllowedFileSchemeAccess");
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {},
+  });
+
+  yield extension.startup();
+  info("extension loaded");
+  yield extension.awaitFinish("isAllowedFileSchemeAccess");
+  yield extension.unload();
+  info("extension unloaded");
+});
+
 </script>
 
 </body>
 </html>
--- a/toolkit/content/tests/chrome/bug360437_window.xul
+++ b/toolkit/content/tests/chrome/bug360437_window.xul
@@ -1,86 +1,119 @@
 <?xml version="1.0"?>
 
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+  href="chrome://mochikit/content/tests/SimpleTest/test.css"
+  type="text/css"?>
 
 <window id="360437Test"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         width="600"
         height="600"
-        onload="onLoad();"
+        onload="startTest();"
         title="360437 test">
 
   <script type="application/javascript"><![CDATA[
-    const Ci = Components.interfaces;
-    const Cc = Components.classes;
-    const Cr = Components.results;
+    const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components;
+    Cu.import("resource://gre/modules/Task.jsm");
+    Cu.import("resource://testing-common/ContentTask.jsm");
+    ContentTask.setTestScope(window.opener.wrappedJSObject);
 
     var gFindBar = null;
     var gBrowser;
 
-    function ok(condition, message) {
-      window.opener.wrappedJSObject.SimpleTest.ok(condition, message);
+    var imports = ["SimpleTest", "ok", "is", "info"];
+    for (var name of imports) {
+      window[name] = window.opener.wrappedJSObject[name];
     }
-    function finish() {
-      window.close();
-      window.opener.wrappedJSObject.SimpleTest.finish();
+
+    function startTest() {
+      Task.spawn(function* () {
+        gFindBar = document.getElementById("FindToolbar");
+        for (let browserId of ["content", "content-remote"]) {
+          yield startTestWithBrowser(browserId);
+        }
+      }).then(() => {
+        window.close();
+        SimpleTest.finish();
+      });
     }
 
-    function onLoad() {
-      var _delayedOnLoad = function() {
-        gFindBar = document.getElementById("FindToolbar");
-        gBrowser = document.getElementById("content");
-        gBrowser.addEventListener("pageshow", onPageShow, false);
-        gBrowser.loadURI("data:text/html,<form><input id='input' type='text' value='text inside an input element'></form>");
-      }
-      setTimeout(_delayedOnLoad, 1000);
+    function* startTestWithBrowser(browserId) {
+      info("Starting test with browser '" + browserId + "'");
+      gBrowser = document.getElementById(browserId);
+      gFindBar.browser = gBrowser;
+      let promise = ContentTask.spawn(gBrowser, null, function* () {
+        return new Promise(resolve => {
+          addEventListener("DOMContentLoaded", function listener() {
+            removeEventListener("DOMContentLoaded", listener);
+            resolve();
+          });
+        });
+      });
+      gBrowser.loadURI("data:text/html,<form><input id='input' type='text' value='text inside an input element'></form>");
+      yield promise;
+      yield onDocumentLoaded();
     }
 
-    function onPageShow() {
-      testNormalFind();
-    }
-
-    function enterStringIntoFindField(aString) {
-      for (var i=0; i < aString.length; i++) {
-        var event = document.createEvent("KeyEvents");
-        event.initKeyEvent("keypress", true, true, null, false, false,
-                           false, false, 0, aString.charCodeAt(i));
-        gFindBar._findField.inputField.dispatchEvent(event);
-      }
-    }
-
-    function testNormalFind() {
+    function* onDocumentLoaded() {
       gFindBar.onFindCommand();
 
       // Make sure the findfield is correctly focused on open
       var searchStr = "text inside an input element";
-      enterStringIntoFindField(searchStr);
-      ok(document.commandDispatcher.focusedElement ==
+      yield* enterStringIntoFindField(searchStr);
+      is(document.commandDispatcher.focusedElement,
          gFindBar._findField.inputField, "Find field isn't focused");
 
       // Make sure "find again" correctly transfers focus to the content element
       // when the find bar is closed.
       gFindBar.close();
       gFindBar.onFindAgainCommand(false);
-      ok(document.commandDispatcher.focusedElement ==
-         gBrowser.contentDocument.getElementById("input"),
-             "Input Element isn't focused");
+      // For remote browsers, the content document DOM tree is not accessible, thus
+      // the focused element should fall back to the browser element.
+      if (gBrowser.hasAttribute("remote")) {
+        is(document.commandDispatcher.focusedElement, gBrowser,
+          "Browser element isn't focused");
+      }
+      yield ContentTask.spawn(gBrowser, null, function* () {
+        Assert.equal(content.document.activeElement,
+         content.document.getElementById("input"), "Input Element isn't focused");
+      });
 
       // Make sure "find again" doesn't focus the content element if focus
       // isn't in the content document.
       var textbox = document.getElementById("textbox");
       textbox.focus();
       gFindBar.close();
       gFindBar.onFindAgainCommand(false);
       ok(textbox.hasAttribute("focused"),
          "Focus was stolen from a chrome element");
-      finish();
+    }
+
+    function* enterStringIntoFindField(aString) {
+      for (let i = 0; i < aString.length; i++) {
+        let event = document.createEvent("KeyEvents");
+        let promise = new Promise(resolve => {
+          let listener = {
+            onFindResult: function() {
+              gFindBar.browser.finder.removeResultListener(listener);
+              resolve();
+            }
+          };
+          gFindBar.browser.finder.addResultListener(listener);
+        });
+        event.initKeyEvent("keypress", true, true, null, false, false,
+                           false, false, 0, aString.charCodeAt(i));
+        gFindBar._findField.inputField.dispatchEvent(event);
+        yield promise;
+      }
     }
   ]]></script>
   <textbox id="textbox"/>
   <browser type="content-primary" flex="1" id="content" src="about:blank"/>
+  <browser type="content-primary" flex="1" id="content-remote" remote="true" src="about:blank"/>
   <findbar id="FindToolbar" browserid="content"/>
 </window>
--- a/toolkit/content/tests/chrome/chrome.ini
+++ b/toolkit/content/tests/chrome/chrome.ini
@@ -63,16 +63,17 @@ support-files =
 [test_browser_drop.xul]
 skip-if = buildapp == 'mulet'
 [test_bug253481.xul]
 [test_bug263683.xul]
 [test_bug304188.xul]
 [test_bug331215.xul]
 [test_bug360220.xul]
 [test_bug360437.xul]
+skip-if = os == 'linux' # Bug 1264604
 [test_bug365773.xul]
 [test_bug366992.xul]
 [test_bug382990.xul]
 [test_bug409624.xul]
 [test_bug418874.xul]
 [test_bug429723.xul]
 [test_bug437844.xul]
 [test_bug457632.xul]
--- a/toolkit/content/tests/chrome/findbar_events_window.xul
+++ b/toolkit/content/tests/chrome/findbar_events_window.xul
@@ -1,164 +1,179 @@
 <?xml version="1.0"?>
 
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet
+  href="chrome://mochikit/content/tests/SimpleTest/test.css"
+  type="text/css"?>
 
 <window id="FindbarTest"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         width="600"
         height="600"
         onload="SimpleTest.executeSoon(startTest);"
         title="findbar events test">
 
   <script type="application/javascript"><![CDATA[
-    const Ci = Components.interfaces;
-    const Cc = Components.classes;
-    const Cr = Components.results;
+    const {interfaces: Ci, classes: Cc, results: Cr, utils: Cu} = Components;
+    Cu.import("resource://gre/modules/Task.jsm");
+    Cu.import("resource://testing-common/ContentTask.jsm");
+    ContentTask.setTestScope(window.opener.wrappedJSObject);
 
     var gFindBar = null;
     var gBrowser;
+    const kTimeout = 5000; // 5 seconds.
 
-    var imports = ["SimpleTest", "ok", "is"];
+    var imports = ["SimpleTest", "ok", "is", "info"];
     for (var name of imports) {
       window[name] = window.opener.wrappedJSObject[name];
     }
-
-    function finish() {
-      window.close();
-      SimpleTest.finish();
-    }
+    SimpleTest.requestLongerTimeout(2);
 
     function startTest() {
-      gFindBar = document.getElementById("FindToolbar");
-      gBrowser = document.getElementById("content");
-      gBrowser.addEventListener("pageshow", onPageShow, false);
-      gBrowser.loadURI('data:text/html,hello there');
+      Task.spawn(function* () {
+        gFindBar = document.getElementById("FindToolbar");
+        for (let browserId of ["content", "content-remote"]) {
+          yield startTestWithBrowser(browserId);
+        }
+      }).then(() => {
+        window.close();
+        SimpleTest.finish();
+      });
     }
 
-    var tests = [
-      testFind,
-      testFindAgain,
-      testCaseSensitivity,
-      testHighlight,
-      finish
-    ];
-
-    // Iterates through the above tests and takes care of passing the done
-    // callback for any async tests.
-    function nextTest() {
-      if (!tests.length) {
-        return;
-      }
-      var func = tests.shift();
-      if (!func.length) {
-        // Test isn't async advance to the next test here.
-        func();
-        SimpleTest.executeSoon(nextTest);
-      } else {
-        func(nextTest);
-      }
+    function* startTestWithBrowser(browserId) {
+      info("Starting test with browser '" + browserId + "'");
+      gBrowser = document.getElementById(browserId);
+      gFindBar.browser = gBrowser;
+      let promise = ContentTask.spawn(gBrowser, null, function* () {
+        return new Promise(resolve => {
+          addEventListener("DOMContentLoaded", function listener() {
+            removeEventListener("DOMContentLoaded", listener);
+            resolve();
+          });
+        });
+      });
+      gBrowser.loadURI("data:text/html,hello there");
+      yield promise;
+      yield onDocumentLoaded();
     }
 
-    function onPageShow() {
+    function* onDocumentLoaded() {
       gFindBar.open();
       gFindBar.onFindCommand();
-      nextTest();
+
+      yield testFind();
+      yield testFindAgain();
+      yield testCaseSensitivity();
+      yield testHighlight();
     }
 
-    function checkSelection(done) {
-      SimpleTest.executeSoon(function() {
-        var selected = gBrowser.contentWindow.getSelection();
-        is(String(selected), "", "No text is selected");
+    function checkSelection() {
+      return new Promise(resolve => {
+        SimpleTest.executeSoon(() => {
+          ContentTask.spawn(gBrowser, null, function* () {
+            let selected = content.getSelection();
+            Assert.equal(String(selected), "", "No text is selected");
 
-        var controller = gFindBar.browser.docShell.QueryInterface(Ci.nsIInterfaceRequestor)
-                                 .getInterface(Ci.nsISelectionDisplay)
-                                 .QueryInterface(Ci.nsISelectionController);
-        var selection = controller.getSelection(controller.SELECTION_FIND);
-        is(selection.rangeCount, 0, "No text is highlighted");
-        done();
+            let controller = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+                                     .getInterface(Ci.nsISelectionDisplay)
+                                     .QueryInterface(Ci.nsISelectionController);
+            let selection = controller.getSelection(controller.SELECTION_FIND);
+            Assert.equal(selection.rangeCount, 0, "No text is highlighted");
+          }).then(resolve);
+        });
       });
     }
 
-    function once(node, eventName, callback) {
-      node.addEventListener(eventName, function clb(e) {
-        node.removeEventListener(eventName, clb);
-        callback(e);
-      })
+    function once(node, eventName, preventDefault = true) {
+      return new Promise((resolve, reject) => {
+        let timeout = window.setTimeout(() => {
+          reject("Event wasn't fired within " + kTimeout + "ms for event '" +
+            eventName + "'.");
+        }, kTimeout);
+
+        node.addEventListener(eventName, function clb(e) {
+          window.clearTimeout(timeout);
+          node.removeEventListener(eventName, clb);
+          if (preventDefault)
+            e.preventDefault();
+          resolve(e);
+        });
+      });
     }
 
-    function testFind(done) {
-      var eventTriggered = false;
-      var query = "t";
-      once(gFindBar, "find", function(e) {
-        eventTriggered = true;
-        ok(e.detail.query === query, "find event query should match '" + query + "'");
-        e.preventDefault();
-        // Since we're preventing the default make sure nothing was selected.
-        checkSelection(done);
-      });
+    function* testFind() {
+      info("Testing normal find.");
+      let query = "t";
+      let promise = once(gFindBar, "find");
 
       // Put some text in the find box.
-      var event = document.createEvent("KeyEvents");
+      let event = document.createEvent("KeyEvents");
       event.initKeyEvent("keypress", true, true, null, false, false,
                          false, false, 0, query.charCodeAt(0));
       gFindBar._findField.inputField.dispatchEvent(event);
-      ok(eventTriggered, "find event should be triggered");
+
+      let e = yield promise;
+      ok(e.detail.query === query, "find event query should match '" + query + "'");
+      // Since we're preventing the default make sure nothing was selected.
+      yield checkSelection();
     }
 
-    function testFindAgain(done) {
-      var eventTriggered = false;
-      once(gFindBar, "findagain", function(e) {
-        eventTriggered = true;
-        e.preventDefault();
-        // Since we're preventing the default make sure nothing was selected.
-        checkSelection(done);
-      });
+    function testFindAgain() {
+      info("Testing repeating normal find.");
+      let promise = once(gFindBar, "findagain");
 
       gFindBar.onFindAgainCommand();
-      ok(eventTriggered, "findagain event should be triggered");
+
+      yield promise;
+      // Since we're preventing the default make sure nothing was selected.
+      yield checkSelection();
     }
 
-    function testCaseSensitivity() {
-      var eventTriggered = false;
-      once(gFindBar, "findcasesensitivitychange", function(e) {
-        eventTriggered = true;
-        ok(e.detail.caseSensitive, "find should be case sensitive");
-      });
+    function* testCaseSensitivity() {
+      info("Testing normal case sensitivity.");
+      let promise = once(gFindBar, "findcasesensitivitychange", false);
 
-      var matchCaseCheckbox = gFindBar.getElement("find-case-sensitive");
+      let matchCaseCheckbox = gFindBar.getElement("find-case-sensitive");
       matchCaseCheckbox.click();
-      ok(eventTriggered, "findcasesensitivitychange should be triggered");
+
+      let e = yield promise;
+      ok(e.detail.caseSensitive, "find should be case sensitive");
+
+      // Toggle it back to the original setting.
+      matchCaseCheckbox.click();
 
       // Changing case sensitivity does the search so clear the selected text
       // before the next test.
-      gBrowser.contentWindow.getSelection().removeAllRanges();
+      yield ContentTask.spawn(gBrowser, null, () => content.getSelection().removeAllRanges());
     }
 
-    function testHighlight(done) {
+    function* testHighlight() {
+      info("Testing find with highlight all.");
       // Update the find state so the highlight button is clickable.
       gFindBar.updateControlState(Ci.nsITypeAheadFind.FIND_FOUND, false);
-      var eventTriggered = false;
-      once(gFindBar, "findhighlightallchange", function(e) {
-        eventTriggered = true;
-        ok(e.detail.highlightAll, "find event should have highlight all set");
-        e.preventDefault();
-        // Since we're preventing the default make sure nothing was highlighted.
-        SimpleTest.executeSoon(function() {
-          checkSelection(done);
-        });
-      });
+
+      let promise = once(gFindBar, "findhighlightallchange");
+
+      let highlightButton = gFindBar.getElement("highlight");
+      if (!highlightButton.checked)
+        highlightButton.click();
 
-      var highlightButton = gFindBar.getElement("highlight");
-      if (!highlightButton.checked) {
+      let e = yield promise;
+      ok(e.detail.highlightAll, "find event should have highlight all set");
+      // Since we're preventing the default make sure nothing was highlighted.
+      yield checkSelection();
+
+      // Toggle it back to the original setting.
+      if (highlightButton.checked)
         highlightButton.click();
-      }
-      ok(eventTriggered, "findhighlightallchange should be triggered");
     }
   ]]></script>
 
   <browser type="content-primary" flex="1" id="content" src="about:blank"/>
+  <browser type="content-primary" flex="1" id="content-remote" remote="true" src="about:blank"/>
   <findbar id="FindToolbar" browserid="content"/>
 </window>
--- a/toolkit/modules/Console.jsm
+++ b/toolkit/modules/Console.jsm
@@ -101,16 +101,31 @@ function getCtorName(aObj) {
     return aObj.constructor.name;
   }
   // If that fails, use Objects toString which sometimes gives something
   // better than 'Object', and at least defaults to Object if nothing better
   return Object.prototype.toString.call(aObj).slice(8, -1);
 }
 
 /**
+ * Indicates whether an object is a JS or `Components.Exception` error.
+ *
+ * @param {object} aThing
+          The object to check
+ * @return {boolean}
+          Is this object an error?
+ */
+function isError(aThing) {
+  return aThing && (
+           (typeof aThing.name == "string" &&
+            aThing.name.startsWith("NS_ERROR_")) ||
+           getCtorName(aThing).endsWith("Error"));
+}
+
+/**
  * A single line stringification of an object designed for use by humans
  *
  * @param {any} aThing
  *        The object to be stringified
  * @param {boolean} aAllowNewLines
  * @return {string}
  *        A single line representation of aThing, which will generally be at
  *        most 80 chars long
@@ -119,16 +134,20 @@ function stringify(aThing, aAllowNewLine
   if (aThing === undefined) {
     return "undefined";
   }
 
   if (aThing === null) {
     return "null";
   }
 
+  if (isError(aThing)) {
+    return "Message: " + aThing;
+  }
+
   if (typeof aThing == "object") {
     let type = getCtorName(aThing);
     if (aThing instanceof Components.interfaces.nsIDOMNode && aThing.tagName) {
       return debugElement(aThing);
     }
     type = (type == "Object" ? "" : type + " ");
     let json;
     try {
@@ -198,19 +217,17 @@ function log(aThing) {
     else if (type == "Set") {
       let i = 0;
       reply += "Set\n";
       for (let value of aThing) {
         reply += logProperty('' + i, value);
         i++;
       }
     }
-    else if (type.match("Error$") ||
-             (typeof aThing.name == "string" &&
-              aThing.name.match("NS_ERROR_"))) {
+    else if (isError(aThing)) {
       reply += "  Message: " + aThing + "\n";
       if (aThing.stack) {
         reply += "  Stack:\n";
         var frame = aThing.stack;
         while (frame) {
           reply += "    " + frame + "\n";
           frame = frame.caller;
         }
--- a/toolkit/mozapps/update/updater/updater.cpp
+++ b/toolkit/mozapps/update/updater/updater.cpp
@@ -3837,31 +3837,34 @@ GetManifestContents(const NS_tchar *mani
 
   size_t r = ms.st_size;
   char *rb = mbuf;
   while (r) {
     const size_t count = mmin(SSIZE_MAX, r);
     size_t c = fread(rb, 1, count, mfile);
     if (c != count) {
       LOG(("GetManifestContents: error reading manifest file: " LOG_S, manifest));
+      free(mbuf);
       return nullptr;
     }
 
     r -= c;
     rb += c;
   }
   mbuf[ms.st_size] = '\0';
   rb = mbuf;
 
 #ifndef XP_WIN
   return rb;
 #else
   NS_tchar *wrb = (NS_tchar *) malloc((ms.st_size + 1) * sizeof(NS_tchar));
-  if (!wrb)
+  if (!wrb) {
+    free(mbuf);
     return nullptr;
+  }
 
   if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, rb, -1, wrb,
                            ms.st_size + 1)) {
     LOG(("GetManifestContents: error converting utf8 to utf16le: %d", GetLastError()));
     free(mbuf);
     free(wrb);
     return nullptr;
   }