merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Fri, 15 Apr 2016 11:39:35 +0200
changeset 316981 755a175b3cc735b575a9e3dd4fcfcd4c7a63695c
parent 316936 10f66b3164570b2183333262fa91a16004cbb908 (current diff)
parent 316980 3e85a84c7d68632e1098cc9374787dc8f54ce24f (diff)
child 317127 afd82f887093e5e9e4015115ca5795ec82a6f732
push id9480
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 17:12:58 +0000
treeherdermozilla-aurora@0d6a91c76a9e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone48.0a1
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;
   }