merge fx-team to mozilla-central
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 03 Dec 2013 10:39:56 +0100
changeset 173110 41a3163ac71405b46341ef57e2cf63417d753a26
parent 173087 71088609c1f33e529518f6ad8cc23383844f4b1c (current diff)
parent 173109 5e2062d62bf627673ba1ed382f6f351c9538a816 (diff)
child 173111 dca219fe1936a5f4ac0c0df30914c9cb74117abd
child 173136 8648aa476eefd28ed305b6d0e648d9c802f4c327
push id3224
push userlsblakk@mozilla.com
push dateTue, 04 Feb 2014 01:06:49 +0000
treeherdermozilla-beta@60c04d0987f1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone28.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central
mobile/android/base/Makefile.in
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -842,8 +842,11 @@ pref("ril.cellbroadcast.disabled", false
 pref("b2g.neterror.url", "app://system.gaiamobile.org/net_error.html");
 
 // Enable Web Speech synthesis API
 pref("media.webspeech.synth.enabled", true);
 
 // Downloads API
 pref("dom.mozDownloads.enabled", true);
 pref("dom.downloads.max_retention_days", 7);
+
+// The URL of the Firefox Accounts auth server backend
+pref("identity.fxaccounts.auth.uri", "https://api-accounts.dev.lcip.org/v1");
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1330,8 +1330,11 @@ pref("dom.debug.propagate_gesture_events
 pref("geo.wifi.uri", "https://www.googleapis.com/geolocation/v1/geolocate?key=%GOOGLE_API_KEY%");
 
 // Necko IPC security checks only needed for app isolation for cookies/cache/etc:
 // currently irrelevant for desktop e10s
 pref("network.disable.ipc.security", true);
 
 // CustomizableUI debug logging.
 pref("browser.uiCustomization.debug", false);
+
+// The URL of the Firefox Accounts auth server backend
+pref("identity.fxaccounts.auth.uri", "https://api-accounts.dev.lcip.org/v1");
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -118,23 +118,24 @@ tabbrowser {
   transition: min-width 200ms ease-out,
               max-width 230ms ease-out;
 }
 
 .tab-background {
   /* Explicitly set the visibility to override the value (collapsed)
    * we inherit from #TabsToolbar[collapsed] upon opening a browser window. */
   visibility: visible;
-  /* This transition is only applied when opening a new tab. Closing tabs
-   * are just hidden so we don't need to adjust the delay for that. */
+  /* The transition is only delayed for opening tabs. */
   transition: visibility 0ms 25ms;
 }
 
-.tab-background[selected]:not([fadein]):not([pinned]) {
+.tab-background:not([fadein]):not([pinned]) {
   visibility: hidden;
+  /* Closing tabs are hidden without a delay. */
+  transition-delay: 0ms;
 }
 
 .tab-throbber:not([fadein]):not([pinned]),
 .tab-label:not([fadein]):not([pinned]),
 .tab-icon-image:not([fadein]):not([pinned]),
 .tab-close-button:not([fadein]):not([pinned]) {
   display: none;
 }
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -4158,16 +4158,17 @@ function onViewToolbarsPopupShowing(aEve
     var deadItem = popup.childNodes[i];
     if (deadItem.hasAttribute("toolbarId"))
       popup.removeChild(deadItem);
   }
 
   var firstMenuItem = aInsertPoint || popup.firstChild;
 
   let toolbarNodes = Array.slice(gNavToolbox.childNodes);
+  toolbarNodes = toolbarNodes.concat(gNavToolbox.externalToolbars);
 
   for (let toolbar of toolbarNodes) {
     let toolbarName = toolbar.getAttribute("toolbarname");
     if (toolbarName) {
       let menuItem = document.createElement("menuitem");
       let hidingAttribute = toolbar.getAttribute("type") == "menubar" ?
                             "autohide" : "collapsed";
       menuItem.setAttribute("id", "toggle_" + toolbar.id);
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -283,33 +283,44 @@ let CustomizableUIInternal = {
       // Guarantee this area exists in gFuturePlacements, to avoid checking it in
       // various places elsewhere.
       gFuturePlacements.set(aName, new Set());
     } else {
       this.restoreStateForArea(aName);
     }
   },
 
-  unregisterArea: function(aName) {
+  unregisterArea: function(aName, aDestroyPlacements) {
     if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
       throw new Error("Invalid area name");
     }
-    if (!gAreas.has(aName)) {
+    if (!gAreas.has(aName) && !gPlacements.has(aName)) {
       throw new Error("Area not registered");
     }
 
     // Move all the widgets out
     this.beginBatchUpdate();
     try {
       let placements = gPlacements.get(aName);
-      placements.forEach(this.removeWidgetFromArea, this);
+      if (placements) {
+        // Need to clone this array so removeWidgetFromArea doesn't modify it
+        placements = [...placements];
+        placements.forEach(this.removeWidgetFromArea, this);
+      }
 
       // Delete all remaining traces.
       gAreas.delete(aName);
-      gPlacements.delete(aName);
+      // Only destroy placements when necessary:
+      if (aDestroyPlacements) {
+        gPlacements.delete(aName);
+      } else {
+        // Otherwise we need to re-set them, as removeFromArea will have emptied
+        // them out:
+        gPlacements.set(aName, placements);
+      }
       gFuturePlacements.delete(aName);
       gBuildAreas.delete(aName);
     } finally {
       this.endBatchUpdate(true);
     }
   },
 
   registerToolbarNode: function(aToolbar, aExistingChildren) {
@@ -1201,22 +1212,25 @@ let CustomizableUIInternal = {
       if (node.id && !this.getPlacementOfWidget(node.id)) {
         widgets.add(node.id);
       }
     }
 
     return [...widgets];
   },
 
-  getPlacementOfWidget: function(aWidgetId, aOnlyRegistered) {
+  getPlacementOfWidget: function(aWidgetId, aOnlyRegistered, aDeadAreas) {
     if (aOnlyRegistered && !this.widgetExists(aWidgetId)) {
       return null;
     }
 
     for (let [area, placements] of gPlacements) {
+      if (!gAreas.has(area) && !aDeadAreas) {
+        continue;
+      }
       let index = placements.indexOf(aWidgetId);
       if (index != -1) {
         return { area: area, position: index };
       }
     }
 
     return null;
   },
@@ -1251,17 +1265,17 @@ let CustomizableUIInternal = {
       gFuturePlacements.get(aArea).add(aWidgetId);
       return;
     }
 
     if (this.isSpecialWidget(aWidgetId)) {
       aWidgetId = this.ensureSpecialWidgetId(aWidgetId);
     }
 
-    let oldPlacement = this.getPlacementOfWidget(aWidgetId);
+    let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
     if (oldPlacement && oldPlacement.area == aArea) {
       this.moveWidgetWithinArea(aWidgetId, aPosition);
       return;
     }
 
     // Do nothing if the widget is not allowed to move to the target area.
     if (!this.canWidgetMoveToArea(aWidgetId, aArea)) {
       return;
@@ -1299,17 +1313,17 @@ let CustomizableUIInternal = {
 
     gDirty = true;
     this.saveState();
 
     this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition);
   },
 
   removeWidgetFromArea: function(aWidgetId) {
-    let oldPlacement = this.getPlacementOfWidget(aWidgetId);
+    let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
     if (!oldPlacement) {
       return;
     }
 
     if (!this.isWidgetRemovable(aWidgetId)) {
       return;
     }
 
@@ -2040,18 +2054,18 @@ this.CustomizableUI = {
     CustomizableUIInternal.registerArea(aName, aProperties);
   },
   registerToolbarNode: function(aToolbar, aExistingChildren) {
     CustomizableUIInternal.registerToolbarNode(aToolbar, aExistingChildren);
   },
   registerMenuPanel: function(aPanel) {
     CustomizableUIInternal.registerMenuPanel(aPanel);
   },
-  unregisterArea: function(aName) {
-    CustomizableUIInternal.unregisterArea(aName);
+  unregisterArea: function(aName, aDestroyPlacements) {
+    CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements);
   },
   addWidgetToArea: function(aWidgetId, aArea, aPosition) {
     CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition);
   },
   removeWidgetFromArea: function(aWidgetId) {
     CustomizableUIInternal.removeWidgetFromArea(aWidgetId);
   },
   moveWidgetWithinArea: function(aWidgetId, aPosition) {
@@ -2231,17 +2245,21 @@ function WidgetGroupWrapper(aWidget) {
 
   this.__defineGetter__("instances", function() {
     // Can't use gBuildWindows here because some areas load lazily:
     let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
     if (!placement) {
       return [];
     }
     let area = placement.area;
-    return [this.forWindow(node.ownerDocument.defaultView) for (node of gBuildAreas.get(area))];
+    let buildAreas = gBuildAreas.get(area);
+    if (!buildAreas) {
+      return [];
+    }
+    return [this.forWindow(node.ownerDocument.defaultView) for (node of buildAreas)];
   });
 
   this.__defineGetter__("areaType", function() {
     return gAreas.get(aWidget.currentArea).get("type");
   });
 
   Object.freeze(this);
 }
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -35,9 +35,10 @@ skip-if = true
 [browser_934113_menubar_removable.js]
 # Because this test is about the menubar, it can't be run on mac
 skip-if = os == "mac"
 
 [browser_938980_navbar_collapsed.js]
 [browser_938995_indefaultstate_nonremovable.js]
 [browser_940946_removable_from_navbar_customizemode.js]
 [browser_941083_invalidate_wrapper_cache_createWidget.js]
+[browser_942581_unregisterArea_keeps_placements.js]
 [browser_panel_toggle.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_942581_unregisterArea_keeps_placements.js
@@ -0,0 +1,118 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const kToolbarName = "test-unregisterArea-placements-toolbar";
+const kTestWidgetPfx = "test-widget-for-unregisterArea-placements-";
+const kTestWidgetCount = 3;
+
+let gTests = [
+  {
+    desc: "unregisterArea should keep placements by default and restore them when re-adding the area",
+    run: function() {
+      let widgetIds = []
+      for (let i = 0; i < kTestWidgetCount; i++) {
+        let id = kTestWidgetPfx + i;
+        widgetIds.push(id);
+        let spec = {id: id, type: 'button', removable: true, label: "unregisterArea test", tooltiptext: "" + i};
+        CustomizableUI.createWidget(spec);
+      }
+      for (let i = kTestWidgetCount; i < kTestWidgetCount * 2; i++) {
+        let id = kTestWidgetPfx + i;
+        widgetIds.push(id);
+        createDummyXULButton(id, "unregisterArea XUL test " + i);
+      }
+      let toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds);
+      checkAbstractAndRealPlacements(toolbarNode, widgetIds);
+
+      // Now move one of them:
+      CustomizableUI.moveWidgetWithinArea(kTestWidgetPfx + kTestWidgetCount, 0);
+      // Clone the array so we know this is the modified one:
+      let modifiedWidgetIds = [...widgetIds];
+      let movedWidget = modifiedWidgetIds.splice(kTestWidgetCount, 1)[0];
+      modifiedWidgetIds.unshift(movedWidget);
+
+      // Check it:
+      checkAbstractAndRealPlacements(toolbarNode, modifiedWidgetIds);
+
+      // Then unregister
+      CustomizableUI.unregisterArea(kToolbarName);
+
+      // Check we tell the outside world no dangerous things:
+      checkWidgetFates(widgetIds);
+      // Only then remove the real node
+      toolbarNode.remove();
+
+      // Now move one of the items to the palette, and another to the navbar:
+      let lastWidget = modifiedWidgetIds.pop();
+      CustomizableUI.removeWidgetFromArea(lastWidget);
+      lastWidget = modifiedWidgetIds.pop();
+      CustomizableUI.addWidgetToArea(lastWidget, CustomizableUI.AREA_NAVBAR);
+
+      // Recreate ourselves with the default placements being the same:
+      toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds);
+      // Then check that after doing this, our actual placements match
+      // the modified list, not the default one.
+      checkAbstractAndRealPlacements(toolbarNode, modifiedWidgetIds);
+
+      // Now remove completely:
+      CustomizableUI.unregisterArea(kToolbarName, true);
+      checkWidgetFates(modifiedWidgetIds);
+      toolbarNode.remove();
+
+      // One more time:
+      // Recreate ourselves with the default placements being the same:
+      toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds);
+      // Should now be back to default:
+      checkAbstractAndRealPlacements(toolbarNode, widgetIds);
+      CustomizableUI.unregisterArea(kToolbarName, true);
+      checkWidgetFates(widgetIds);
+      toolbarNode.remove();
+
+      //XXXgijs: ensure cleanup function doesn't barf:
+      gAddedToolbars.delete(kToolbarName);
+
+      // Remove all the XUL widgets, destroy the others:
+      for (let widget of widgetIds) {
+        let widgetWrapper = CustomizableUI.getWidget(widget);
+        if (widgetWrapper.provider == CustomizableUI.PROVIDER_XUL) {
+          gNavToolbox.palette.querySelector("#" + widget).remove();
+        } else {
+          CustomizableUI.destroyWidget(widget);
+        }
+      }
+    },
+  }
+];
+
+function checkAbstractAndRealPlacements(aNode, aExpectedPlacements) {
+  assertAreaPlacements(kToolbarName, aExpectedPlacements);
+  let physicalWidgetIds = [node.id for (node of aNode.childNodes)];
+  placementArraysEqual(aNode.id, physicalWidgetIds, aExpectedPlacements);
+}
+
+function checkWidgetFates(aWidgetIds) {
+  for (let widget of aWidgetIds) {
+    ok(!CustomizableUI.getPlacementOfWidget(widget), "Widget should be in palette");
+    ok(!document.getElementById(widget), "Widget should not be in the DOM");
+    let widgetInPalette = !!gNavToolbox.palette.querySelector("#" + widget);
+    let widgetProvider = CustomizableUI.getWidget(widget).provider;
+    let widgetIsXULWidget = widgetProvider == CustomizableUI.PROVIDER_XUL;
+    is(widgetInPalette, widgetIsXULWidget, "Just XUL Widgets should be in the palette");
+  }
+}
+
+function asyncCleanup() {
+  yield resetCustomization();
+}
+
+function cleanup() {
+  removeCustomToolbars();
+}
+
+function test() {
+  waitForExplicitFinish();
+  registerCleanupFunction(cleanup);
+  runTests(gTests, asyncCleanup);
+}
+
--- a/browser/components/customizableui/test/head.js
+++ b/browser/components/customizableui/test/head.js
@@ -34,22 +34,23 @@ function createToolbarWithPlacements(id,
   let tb = document.createElementNS(kNSXUL, "toolbar");
   tb.id = id;
   tb.setAttribute("customizable", "true");
   CustomizableUI.registerArea(id, {
     type: CustomizableUI.TYPE_TOOLBAR,
     defaultPlacements: placements
   });
   gNavToolbox.appendChild(tb);
+  return tb;
 }
 
 function removeCustomToolbars() {
   CustomizableUI.reset();
   for (let toolbarId of gAddedToolbars) {
-    CustomizableUI.unregisterArea(toolbarId);
+    CustomizableUI.unregisterArea(toolbarId, true);
     document.getElementById(toolbarId).remove();
   }
   gAddedToolbars.clear();
 }
 
 function resetCustomization() {
   return CustomizableUI.reset();
 }
@@ -66,16 +67,20 @@ function isInWin8() {
 function addSwitchToMetroButtonInWindows8(areaPanelPlacements) {
   if (isInWin8()) {
     areaPanelPlacements.push("switch-to-metro-button");
   }
 }
 
 function assertAreaPlacements(areaId, expectedPlacements) {
   let actualPlacements = getAreaWidgetIds(areaId);
+  placementArraysEqual(areaId, actualPlacements, expectedPlacements);
+}
+
+function placementArraysEqual(areaId, actualPlacements, expectedPlacements) {
   is(actualPlacements.length, expectedPlacements.length,
      "Area " + areaId + " should have " + expectedPlacements.length + " items.");
   let minItems = Math.min(expectedPlacements.length, actualPlacements.length);
   for (let i = 0; i < minItems; i++) {
     if (typeof expectedPlacements[i] == "string") {
       is(actualPlacements[i], expectedPlacements[i],
          "Item " + i + " in " + areaId + " should match expectations.");
     } else if (expectedPlacements[i] instanceof RegExp) {
--- a/browser/devtools/profiler/cleopatra.js
+++ b/browser/devtools/profiler/cleopatra.js
@@ -1,15 +1,16 @@
 /* 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";
 
-let { defer }    = require("sdk/core/promise");
+let { Cu }       = require("chrome");
+let { defer }    = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
 let EventEmitter = require("devtools/shared/event-emitter");
 
 const { PROFILE_IDLE, PROFILE_COMPLETED, PROFILE_RUNNING } = require("devtools/profiler/consts");
 
 /**
  * An implementation of a profile visualization that uses Cleopatra.
  * It consists of an iframe with Cleopatra loaded in it and some
  * surrounding meta-data (such as UIDs).
--- a/browser/devtools/profiler/commands.js
+++ b/browser/devtools/profiler/commands.js
@@ -5,17 +5,17 @@
 const { Cu } = require("chrome");
 module.exports = [];
 
 Cu.import("resource://gre/modules/devtools/gcli.jsm");
 
 loader.lazyGetter(this, "gDevTools",
   () => Cu.import("resource:///modules/devtools/gDevTools.jsm", {}).gDevTools);
 
-var promise = require("sdk/core/promise");
+var { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 /*
  * 'profiler' command. Doesn't do anything.
  */
 gcli.addCommand({
   name: "profiler",
   description: gcli.lookup("profilerDesc"),
   manual: gcli.lookup("profilerManual")
--- a/browser/devtools/profiler/panel.js
+++ b/browser/devtools/profiler/panel.js
@@ -12,20 +12,20 @@ const {
   PROFILE_COMPLETED,
   SHOW_PLATFORM_DATA,
   L10N_BUNDLE
 } = require("devtools/profiler/consts");
 
 const { TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
 
 var EventEmitter = require("devtools/shared/event-emitter");
-var promise      = require("sdk/core/promise");
 var Cleopatra    = require("devtools/profiler/cleopatra");
 var Sidebar      = require("devtools/profiler/sidebar");
 var ProfilerController = require("devtools/profiler/controller");
+var { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 Cu.import("resource://gre/modules/devtools/Console.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 
--- a/browser/devtools/scratchpad/scratchpad-panel.js
+++ b/browser/devtools/scratchpad/scratchpad-panel.js
@@ -1,25 +1,26 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
+const {Cu} = require("chrome");
 const EventEmitter = require("devtools/shared/event-emitter");
-const promise = require("sdk/core/promise");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 
 function ScratchpadPanel(iframeWindow, toolbox) {
   let { Scratchpad } = iframeWindow;
   this._toolbox = toolbox;
   this.panelWin = iframeWindow;
   this.scratchpad = Scratchpad;
-  
+
   Scratchpad.target = this.target;
   Scratchpad.hideMenu();
 
   let deferred = promise.defer();
   this._readyObserver = deferred.promise;
   Scratchpad.addObserver({
     onReady: function() {
       Scratchpad.removeObserver(this);
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -28,21 +28,22 @@ const EVAL_FUNCTION_TIMEOUT      = 1000;
 
 const SCRATCHPAD_L10N = "chrome://browser/locale/devtools/scratchpad.properties";
 const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled";
 const PREF_RECENT_FILES_MAX = "devtools.scratchpad.recentFilesMax";
 
 const VARIABLES_VIEW_URL = "chrome://browser/content/devtools/widgets/VariablesView.xul";
 
 const require   = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
-const promise   = require("sdk/core/promise");
+
 const Telemetry = require("devtools/shared/telemetry");
 const Editor    = require("devtools/sourceeditor/editor");
 const TargetFactory = require("devtools/framework/target").TargetFactory;
 
+const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource:///modules/devtools/scratchpad-manager.jsm");
 Cu.import("resource://gre/modules/jsdebugger.jsm");
 Cu.import("resource:///modules/devtools/gDevTools.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
--- a/browser/devtools/scratchpad/test/head.js
+++ b/browser/devtools/scratchpad/test/head.js
@@ -1,24 +1,17 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
-let tempScope = {};
-
-Cu.import("resource://gre/modules/NetUtil.jsm", tempScope);
-Cu.import("resource://gre/modules/FileUtils.jsm", tempScope);
-Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", tempScope);
-
-
-let NetUtil = tempScope.NetUtil;
-let FileUtils = tempScope.FileUtils;
-let promise = tempScope.Promise;
+const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
+const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 let gScratchpadWindow; // Reference to the Scratchpad chrome window object
 
 /**
  * Open a Scratchpad window.
  *
  * @param function aReadyCallback
  *        Optional. The function you want invoked when the Scratchpad instance
--- a/browser/devtools/sourceeditor/editor.js
+++ b/browser/devtools/sourceeditor/editor.js
@@ -11,17 +11,17 @@ const TAB_SIZE    = "devtools.editor.tab
 const EXPAND_TAB  = "devtools.editor.expandtab";
 const L10N_BUNDLE = "chrome://browser/locale/devtools/sourceeditor.properties";
 const XUL_NS      = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 // Maximum allowed margin (in number of lines) from top or bottom of the editor
 // while shifting to a line which was initially out of view.
 const MAX_VERTICAL_OFFSET = 3;
 
-const promise = require("sdk/core/promise");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 const events  = require("devtools/shared/event-emitter");
 
 Cu.import("resource://gre/modules/Services.jsm");
 const L10N = Services.strings.createBundle(L10N_BUNDLE);
 
 // CM_STYLES, CM_SCRIPTS and CM_IFRAME represent the HTML,
 // JavaScript and CSS that is injected into an iframe in
 // order to initialize a CodeMirror instance.
--- a/browser/metro/base/content/browser.js
+++ b/browser/metro/base/content/browser.js
@@ -1303,68 +1303,63 @@ Tab.prototype = {
       }
       self._eventDeferred = null;
     }
     browser.addEventListener("pageshow", onPageShowEvent, true);
     browser.addEventListener("DOMWindowCreated", this, false);
     Elements.browsers.addEventListener("SizeChanged", this, false);
 
     browser.messageManager.addMessageListener("Content:StateChange", this);
-    Services.obs.addObserver(this, "metro_viewstate_changed", false);
 
     if (aOwner)
       this._copyHistoryFrom(aOwner);
     this._loadUsingParams(browser, aURI, aParams);
   },
 
   updateViewport: function (aEvent) {
     // <meta name=viewport> is not yet supported; just use the browser size.
     this.browser.setWindowSize(this.browser.clientWidth, this.browser.clientHeight);
   },
 
   handleEvent: function (aEvent) {
     switch (aEvent.type) {
       case "DOMWindowCreated":
+        this.updateViewport();
+        break;
       case "SizeChanged":
         this.updateViewport();
+        this._delayUpdateThumbnail();
         break;
     }
   },
 
   receiveMessage: function(aMessage) {
     switch (aMessage.name) {
       case "Content:StateChange":
         // update the thumbnail now...
         this.updateThumbnail();
         // ...and in a little while to capture page after load.
         if (aMessage.json.stateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
-          clearTimeout(this._updateThumbnailTimeout);
-          this._updateThumbnailTimeout = setTimeout(() => {
-            this.updateThumbnail();
-          }, kTabThumbnailDelayCapture);
+          this._delayUpdateThumbnail();
         }
         break;
     }
   },
 
-  observe: function BrowserUI_observe(aSubject, aTopic, aData) {
-    switch (aTopic) {
-      case "metro_viewstate_changed":
-        if (aData !== "snapped") {
-          this.updateThumbnail();
-        }
-        break;
-    }
+  _delayUpdateThumbnail: function() {
+    clearTimeout(this._updateThumbnailTimeout);
+    this._updateThumbnailTimeout = setTimeout(() => {
+      this.updateThumbnail();
+    }, kTabThumbnailDelayCapture);
   },
 
   destroy: function destroy() {
     this._browser.messageManager.removeMessageListener("Content:StateChange", this);
     this._browser.removeEventListener("DOMWindowCreated", this, false);
     Elements.browsers.removeEventListener("SizeChanged", this, false);
-    Services.obs.removeObserver(this, "metro_viewstate_changed", false);
     clearTimeout(this._updateThumbnailTimeout);
 
     Elements.tabList.removeTab(this._chromeTab);
     this._chromeTab = null;
     this._destroyBrowser();
   },
 
   resurrect: function resurrect() {
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -255,17 +255,17 @@ Desktop browser's sync prefs.
             <toolbarbutton id="stop-button" class="urlbar-button"
                            command="cmd_stop"/>
           </hbox>
 
           <stack id="toolbar-contextual">
             <observes element="bcast_windowState" attribute="*"/>
             <observes element="bcast_urlbarState" attribute="*"/>
             <hbox id="toolbar-context-page" pack="end">
-              <circularprogressindicator id="download-progress"
+              <circularprogressindicator id="download-progress" class="appbar-primary"
                                      oncommand="MetroDownloadsView.onDownloadButton()"/>
               <toolbarbutton id="star-button" class="appbar-primary"
                              type="checkbox"
                              oncommand="Appbar.onStarButton()"/>
               <toolbarbutton id="pin-button" class="appbar-primary"
                              type="checkbox"
                              oncommand="Appbar.onPinButton()"/>
               <toolbarbutton id="menu-button" class="appbar-primary"
--- a/browser/metro/theme/browser.css
+++ b/browser/metro/theme/browser.css
@@ -736,26 +736,17 @@ documenttab[selected] .documenttab-selec
   visibility: collapse;
 }
 
 #download-progress:not([progress]) {
   visibility: collapse;
 }
 
 #download-progress {
-  width: 40px;
-  height: 40px;
   list-style-image: url(chrome://browser/skin/images/navbar-download.png);
-  -moz-image-region: rect(0px, 40px, 40px, 0px);
-}
-#download-progress:hover {
-  -moz-image-region: rect(0px, 80px, 40px, 40px);
-}
-#download-progress:active {
-  -moz-image-region: rect(0px, 120px, 40px, 80px);
 }
 
 #pin-button {
   list-style-image: url(chrome://browser/skin/images/navbar-pin.png);
 }
 
 #star-button {
   list-style-image: url(chrome://browser/skin/images/navbar-star.png);
--- a/browser/metro/theme/platform.css
+++ b/browser/metro/theme/platform.css
@@ -783,18 +783,18 @@ appbar toolbar[labelled] toolbarbutton {
 appbar toolbar[labelled] toolbarbutton > .toolbarbutton-text {
   display: block;
   padding-top: @metro_spacing_small@;
   font-size: 0.75rem;
 }
 
 /* Sprites */
 
-.appbar-primary > .toolbarbutton-icon,
-.appbar-secondary > .toolbarbutton-icon {
+.appbar-primary .toolbarbutton-icon,
+.appbar-secondary .toolbarbutton-icon {
   width: 40px;
   height: 40px;
 }
 
 /* Primary sprite format: one button per sprite.
 
    States from left to right:
    normal, hover, active/toggled, toggled+hover, toggled+active. */
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -695,16 +695,20 @@ menuitem:not([type]):not(.menuitem-toolt
 
 #urlbar-display {
   margin-top: 0;
   margin-bottom: 0;
   -moz-margin-start: 0;
   color: GrayText;
 }
 
+#search-container {
+  min-width: calc(54px + 11ch);
+}
+
 %include ../shared/identity-block.inc.css
 
 #page-proxy-favicon {
   margin-top: 2px;
   margin-bottom: 2px;
   -moz-margin-start: 4px;
   -moz-margin-end: 3px;
   -moz-image-region: rect(0, 16px, 16px, 0);
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1669,16 +1669,20 @@ toolbar .toolbarbutton-1:not([type="menu
   min-width: 8px;
   width: 8px;
   background-image: none;
   margin: 0 -4px;
   position: relative;
   height: 22px;
 }
 
+#search-container {
+  min-width: calc(54px + 11ch);
+}
+
 %include ../shared/identity-block.inc.css
 
 #page-proxy-favicon {
   margin: 0px;
   padding: 0px;
   -moz-image-region: rect(0, 16px, 16px, 0);
 }
 
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -951,16 +951,20 @@ html|*.urlbar-input:-moz-lwtheme::-moz-p
 
 #urlbar-display {
   margin-top: 0;
   margin-bottom: 0;
   -moz-margin-start: 0;
   color: GrayText;
 }
 
+#search-container {
+  min-width: calc(54px + 11ch);
+}
+
 /* identity box */
 
 #identity-box {
   padding: 2px;
   font-size: .9em;
 }
 
 #identity-box:-moz-locale-dir(ltr) {
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -254,16 +254,18 @@ AC_SUBST([STLPORT_LIBS])
 AC_DEFUN([MOZ_ANDROID_SDK],
 [
 
 MOZ_ARG_WITH_STRING(android-sdk,
 [  --with-android-sdk=DIR
                           location where the Android SDK can be found (base directory, e.g. .../android/platforms/android-6)],
     android_sdk=$withval)
 
+android_sdk_root=$withval/../../
+
 case "$target" in
 *-android*|*-linuxandroid*)
     if test -z "$android_sdk" ; then
         AC_MSG_ERROR([You must specify --with-android-sdk=/path/to/sdk when targeting Android.])
     else
         if ! test -e "$android_sdk"/source.properties ; then
             AC_MSG_ERROR([The path in --with-android-sdk isn't valid (source.properties hasn't been found).])
         fi
@@ -279,43 +281,45 @@ case "$target" in
             AC_MSG_ERROR([Unexpected error: the found android api value isn't a number! (found $android_api_level)])
         fi
 
         if test $android_api_level -lt $1 ; then
             AC_MSG_ERROR([The given Android SDK provides API level $android_api_level ($1 or higher required).])
         fi
     fi
 
-    android_tools="$android_sdk"/../../tools
-    android_platform_tools="$android_sdk"/../../platform-tools
+    android_tools="$android_sdk_root"/tools
+    android_platform_tools="$android_sdk_root"/platform-tools
     if test ! -d "$android_platform_tools" ; then
         android_platform_tools="$android_sdk"/tools # SDK Tools < r8
     fi
     # The build tools got moved around to different directories in
     # SDK Tools r22.  Try to locate them.
     android_build_tools=""
     for suffix in android-4.3 19.0.0 18.1.0 18.0.1 18.0.0 17.0.0 android-4.2.2; do
-        tools_directory="$android_sdk/../../build-tools/$suffix"
+        tools_directory="$android_sdk_root/build-tools/$suffix"
         if test -d "$tools_directory" ; then
             android_build_tools="$tools_directory"
             break
         fi
     done
     if test -z "$android_build_tools" ; then
         android_build_tools="$android_platform_tools" # SDK Tools < r22
     fi
     ANDROID_SDK="${android_sdk}"
-    if test -e "${android_sdk}/../../extras/android/compatibility/v4/android-support-v4.jar" ; then
-        ANDROID_COMPAT_LIB="${android_sdk}/../../extras/android/compatibility/v4/android-support-v4.jar"
+    ANDROID_SDK_ROOT="${android_sdk_root}"
+    if test -e "${ANDROID_SDK_ROOT}/extras/android/compatibility/v4/android-support-v4.jar" ; then
+        ANDROID_COMPAT_LIB="${ANDROID_SDK_ROOT}/extras/android/compatibility/v4/android-support-v4.jar"
     else
-        ANDROID_COMPAT_LIB="${android_sdk}/../../extras/android/support/v4/android-support-v4.jar";
+        ANDROID_COMPAT_LIB="${ANDROID_SDK_ROOT}/extras/android/support/v4/android-support-v4.jar";
     fi
     ANDROID_TOOLS="${android_tools}"
     ANDROID_PLATFORM_TOOLS="${android_platform_tools}"
     ANDROID_BUILD_TOOLS="${android_build_tools}"
+    AC_SUBST(ANDROID_SDK_ROOT)
     AC_SUBST(ANDROID_SDK)
     AC_SUBST(ANDROID_COMPAT_LIB)
     if ! test -e $ANDROID_COMPAT_LIB ; then
         AC_MSG_ERROR([You must download the Android support library when targeting Android.   Run the Android SDK tool and install Android Support Library under Extras.  See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_COMPAT_LIB)])
     fi
 
     MOZ_PATH_PROG(ZIPALIGN, zipalign, :, [$ANDROID_TOOLS])
     MOZ_PATH_PROG(DX, dx, :, [$ANDROID_BUILD_TOOLS])
--- a/js/src/build/autoconf/android.m4
+++ b/js/src/build/autoconf/android.m4
@@ -254,16 +254,18 @@ AC_SUBST([STLPORT_LIBS])
 AC_DEFUN([MOZ_ANDROID_SDK],
 [
 
 MOZ_ARG_WITH_STRING(android-sdk,
 [  --with-android-sdk=DIR
                           location where the Android SDK can be found (base directory, e.g. .../android/platforms/android-6)],
     android_sdk=$withval)
 
+android_sdk_root=$withval/../../
+
 case "$target" in
 *-android*|*-linuxandroid*)
     if test -z "$android_sdk" ; then
         AC_MSG_ERROR([You must specify --with-android-sdk=/path/to/sdk when targeting Android.])
     else
         if ! test -e "$android_sdk"/source.properties ; then
             AC_MSG_ERROR([The path in --with-android-sdk isn't valid (source.properties hasn't been found).])
         fi
@@ -279,43 +281,45 @@ case "$target" in
             AC_MSG_ERROR([Unexpected error: the found android api value isn't a number! (found $android_api_level)])
         fi
 
         if test $android_api_level -lt $1 ; then
             AC_MSG_ERROR([The given Android SDK provides API level $android_api_level ($1 or higher required).])
         fi
     fi
 
-    android_tools="$android_sdk"/../../tools
-    android_platform_tools="$android_sdk"/../../platform-tools
+    android_tools="$android_sdk_root"/tools
+    android_platform_tools="$android_sdk_root"/platform-tools
     if test ! -d "$android_platform_tools" ; then
         android_platform_tools="$android_sdk"/tools # SDK Tools < r8
     fi
     # The build tools got moved around to different directories in
     # SDK Tools r22.  Try to locate them.
     android_build_tools=""
     for suffix in android-4.3 19.0.0 18.1.0 18.0.1 18.0.0 17.0.0 android-4.2.2; do
-        tools_directory="$android_sdk/../../build-tools/$suffix"
+        tools_directory="$android_sdk_root/build-tools/$suffix"
         if test -d "$tools_directory" ; then
             android_build_tools="$tools_directory"
             break
         fi
     done
     if test -z "$android_build_tools" ; then
         android_build_tools="$android_platform_tools" # SDK Tools < r22
     fi
     ANDROID_SDK="${android_sdk}"
-    if test -e "${android_sdk}/../../extras/android/compatibility/v4/android-support-v4.jar" ; then
-        ANDROID_COMPAT_LIB="${android_sdk}/../../extras/android/compatibility/v4/android-support-v4.jar"
+    ANDROID_SDK_ROOT="${android_sdk_root}"
+    if test -e "${ANDROID_SDK_ROOT}/extras/android/compatibility/v4/android-support-v4.jar" ; then
+        ANDROID_COMPAT_LIB="${ANDROID_SDK_ROOT}/extras/android/compatibility/v4/android-support-v4.jar"
     else
-        ANDROID_COMPAT_LIB="${android_sdk}/../../extras/android/support/v4/android-support-v4.jar";
+        ANDROID_COMPAT_LIB="${ANDROID_SDK_ROOT}/extras/android/support/v4/android-support-v4.jar";
     fi
     ANDROID_TOOLS="${android_tools}"
     ANDROID_PLATFORM_TOOLS="${android_platform_tools}"
     ANDROID_BUILD_TOOLS="${android_build_tools}"
+    AC_SUBST(ANDROID_SDK_ROOT)
     AC_SUBST(ANDROID_SDK)
     AC_SUBST(ANDROID_COMPAT_LIB)
     if ! test -e $ANDROID_COMPAT_LIB ; then
         AC_MSG_ERROR([You must download the Android support library when targeting Android.   Run the Android SDK tool and install Android Support Library under Extras.  See https://developer.android.com/tools/extras/support-library.html for more info. (looked for $ANDROID_COMPAT_LIB)])
     fi
 
     MOZ_PATH_PROG(ZIPALIGN, zipalign, :, [$ANDROID_TOOLS])
     MOZ_PATH_PROG(DX, dx, :, [$ANDROID_BUILD_TOOLS])
--- a/mobile/android/app/mobile.js
+++ b/mobile/android/app/mobile.js
@@ -795,10 +795,13 @@ pref("snav.enabled", true);
 pref("browser.snippets.updateUrl", "https://snippets.mozilla.com/json/%SNIPPETS_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/");
 
 // How frequently we check for new snippets, in seconds (1 day)
 pref("browser.snippets.updateInterval", 86400);
 
 // URL used to check for user's country code
 pref("browser.snippets.geoUrl", "https://geo.mozilla.org/country.json");
 
+// URL used to ping metrics with stats about which snippets have been shown
+pref("browser.snippets.statsUrl", "https://snippets-stats.mozilla.org/mobile");
+
 // This pref requires a restart to take effect.
 pref("browser.snippets.enabled", false);
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -81,19 +81,29 @@ endif
 
 include $(topsrcdir)/config/config.mk
 
 # Note that we're going to set up a dependency directly between embed_android.dex and the java files
 # Instead of on the .class files, since more than one .class file might be produced per .java file
 # Sync dependencies are provided in a single jar. Sync classes themselves are delivered as source,
 # because Android resource classes must be compiled together in order to avoid overlapping resource
 # indices.
-classes.dex: $(ALL_JARS)
+
+classes.dex: proguard-jars
 	@echo 'DX classes.dex'
-	$(DX) --dex --output=classes.dex $(ALL_JARS) $(ANDROID_COMPAT_LIB)
+	$(DX) --dex --output=classes.dex jars-proguarded $(ANDROID_COMPAT_LIB)
+
+ifdef MOZ_DEBUG
+PROGUARD_PASSES=1
+else
+PROGUARD_PASSES=6
+endif
+
+proguard-jars: $(ALL_JARS)
+	java -jar $(ANDROID_SDK_ROOT)/tools/proguard/lib/proguard.jar @$(topsrcdir)/mobile/android/config/proguard.cfg -optimizationpasses $(PROGUARD_PASSES) -injars $(subst ::,:,$(subst $(NULL) ,:,$(ALL_JARS))) -outjars jars-proguarded -libraryjars $(ANDROID_SDK)/android.jar:$(ANDROID_COMPAT_LIB)
 
 CLASSES_WITH_JNI= \
     org.mozilla.gecko.GeckoAppShell \
     org.mozilla.gecko.GeckoJavaSampler \
     org.mozilla.gecko.gfx.NativePanZoomController \
     org.mozilla.gecko.ANRReporter \
     $(NULL)
 
--- a/mobile/android/components/Snippets.js
+++ b/mobile/android/components/Snippets.js
@@ -4,25 +4,29 @@
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Home", "resource://gre/modules/Home.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "gEncoder", function() { return new gChromeWin.TextEncoder(); });
 XPCOMUtils.defineLazyGetter(this, "gDecoder", function() { return new gChromeWin.TextDecoder(); });
 
 const SNIPPETS_ENABLED = Services.prefs.getBoolPref("browser.snippets.enabled");
 
 // URL to fetch snippets, in the urlFormatter service format.
 const SNIPPETS_UPDATE_URL_PREF = "browser.snippets.updateUrl";
 
+// URL to send stats data to metrics.
+const SNIPPETS_STATS_URL_PREF = "browser.snippets.statsUrl";
+
 // URL to fetch country code, a value that's cached and refreshed once per month.
 const SNIPPETS_GEO_URL_PREF = "browser.snippets.geoUrl";
 
 // Timestamp when we last updated the user's country code.
 const SNIPPETS_GEO_LAST_UPDATE_PREF = "browser.snippets.geoLastUpdate";
 
 // Pref where we'll cache the user's country.
 const SNIPPETS_COUNTRY_CODE_PREF = "browser.snippets.countryCode";
@@ -33,16 +37,30 @@ const SNIPPETS_GEO_UPDATE_INTERVAL_MS = 
 // Should be bumped up if the snippets content format changes.
 const SNIPPETS_VERSION = 1;
 
 XPCOMUtils.defineLazyGetter(this, "gSnippetsURL", function() {
   let updateURL = Services.prefs.getCharPref(SNIPPETS_UPDATE_URL_PREF).replace("%SNIPPETS_VERSION%", SNIPPETS_VERSION);
   return Services.urlFormatter.formatURL(updateURL);
 });
 
+// Where we cache snippets data
+XPCOMUtils.defineLazyGetter(this, "gSnippetsPath", function() {
+  return OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
+});
+
+XPCOMUtils.defineLazyGetter(this, "gStatsURL", function() {
+  return Services.prefs.getCharPref(SNIPPETS_STATS_URL_PREF);
+});
+
+// Where we store stats about which snippets have been shown
+XPCOMUtils.defineLazyGetter(this, "gStatsPath", function() {
+  return OS.Path.join(OS.Constants.Path.profileDir, "snippets-stats.txt");
+});
+
 XPCOMUtils.defineLazyGetter(this, "gGeoURL", function() {
   return Services.prefs.getCharPref(SNIPPETS_GEO_URL_PREF);
 });
 
 XPCOMUtils.defineLazyGetter(this, "gCountryCode", function() {
   try {
     return Services.prefs.getCharPref(SNIPPETS_COUNTRY_CODE_PREF);
   } catch (e) {
@@ -104,28 +122,26 @@ function updateSnippets() {
 }
 
 /**
  * Caches snippets server response text to `snippets.json` in profile directory.
  *
  * @param response responseText returned from snippets server
  */
 function cacheSnippets(response) {
-  let path = OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
   let data = gEncoder.encode(response);
-  let promise = OS.File.writeAtomic(path, data, { tmpPath: path + ".tmp" });
+  let promise = OS.File.writeAtomic(gSnippetsPath, data, { tmpPath: gSnippetsPath + ".tmp" });
   promise.then(null, e => Cu.reportError("Error caching snippets: " + e));
 }
 
 /**
  * Loads snippets from cached `snippets.json`.
  */
 function loadSnippetsFromCache() {
-  let path = OS.Path.join(OS.Constants.Path.profileDir, "snippets.json");
-  let promise = OS.File.read(path);
+  let promise = OS.File.read(gSnippetsPath);
   promise.then(array => updateBanner(gDecoder.decode(array)), e => {
     // If snippets.json doesn't exist, update data from the server.
     if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
       update();
     } else {
       Cu.reportError("Error loading snippets from cache: " + e);
     }
   });
@@ -162,25 +178,98 @@ function updateBanner(response) {
     }
     let id = Home.banner.add({
       text: message.text,
       icon: message.icon,
       onclick: function() {
         gChromeWin.BrowserApp.addTab(message.url);
       },
       onshown: function() {
-        // XXX: 10% of the time, let the metrics server know which message was shown (bug 937373)
+        // 10% of the time, record the snippet id and a timestamp
+        if (Math.random() < .1) {
+          writeStat(message.id, new Date().toISOString());
+        }
       }
     });
     // Keep track of the message we added so that we can remove it later.
     gMessageIds.push(id);
   });
 }
 
 /**
+ * Appends snippet id and timestamp to the end of `snippets-stats.txt`.
+ *
+ * @param snippetId unique id for snippet, sent from snippets server
+ * @param timestamp in ISO8601
+ */
+function writeStat(snippetId, timestamp) {
+  let data = gEncoder.encode(snippetId + "," + timestamp + ";");
+
+  Task.spawn(function() {
+    try {
+      let file = yield OS.File.open(gStatsPath, { append: true, write: true });
+      try {
+        yield file.write(data);
+      } finally {
+        yield file.close();
+      }
+    } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
+      // If the file doesn't exist yet, create it.
+      yield OS.File.writeAtomic(gStatsPath, data, { tmpPath: gStatsPath + ".tmp" });
+    }
+  }).then(null, e => Cu.reportError("Error writing snippets stats: " + e));
+}
+
+/**
+ * Reads snippets stats data from `snippets-stats.txt` and sends the data to metrics.
+ */
+function sendStats() {
+  let promise = OS.File.read(gStatsPath);
+  promise.then(array => sendStatsRequest(gDecoder.decode(array)), e => {
+    if (e instanceof OS.File.Error && e.becauseNoSuchFile) {
+      // If the file doesn't exist, there aren't any stats to send.
+    } else {
+      Cu.reportError("Error eading snippets stats: " + e);
+    }
+  });
+}
+
+/**
+ * Sends stats to metrics about which snippets have been shown.
+ * Appends snippet ids and timestamps as parameters to a GET request.
+ * e.g. https://snippets-stats.mozilla.org/mobile?s1=3825&t1=2013-11-17T18:27Z&s2=6326&t2=2013-11-18T18:27Z
+ *
+ * @param data contents of stats data file
+ */
+function sendStatsRequest(data) {
+  let params = [];
+  let stats = data.split(";");
+
+  // The last item in the array will be an empty string, so stop before then.
+  for (let i = 0; i < stats.length - 1; i++) {
+    let stat = stats[i].split(",");
+    params.push("s" + i + "=" + encodeURIComponent(stat[0]));
+    params.push("t" + i + "=" + encodeURIComponent(stat[1]));
+  }
+
+  let url = gStatsURL + "?" + params.join("&");
+
+  // Remove the file after succesfully sending the data.
+  _httpGetRequest(url, removeStats);
+}
+
+/**
+ * Removes text file where we store snippets stats.
+ */
+function removeStats() {
+  let promise = OS.File.remove(gStatsPath);
+  promise.then(null, e => Cu.reportError("Error removing snippets stats: " + e));
+}
+
+/**
  * Helper function to make HTTP GET requests.
  *
  * @param url where we send the request
  * @param callback function that is called with the xhr responseText
  */
 function _httpGetRequest(url, callback) {
   let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
   try {
@@ -222,12 +311,13 @@ Snippets.prototype = {
   },
 
   // By default, this timer fires once every 24 hours. See the "browser.snippets.updateInterval" pref.
   notify: function(timer) {
     if (!SNIPPETS_ENABLED) {
       return;
     }
     update();
+    sendStats();
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Snippets]);
new file mode 100644
--- /dev/null
+++ b/mobile/android/config/proguard.cfg
@@ -0,0 +1,201 @@
+# Dalvik renders preverification unuseful (Would just slightly bloat the file).
+-dontpreverify
+
+# Uncomment to have Proguard list dead code detected during the run - useful for cleaning up the codebase.
+# -printusage
+
+-dontskipnonpubliclibraryclassmembers
+-verbose
+-allowaccessmodification
+
+# Preserve all fundamental application classes.
+-keep public class * extends android.app.Activity
+-keep public class * extends android.app.Application
+-keep public class * extends android.app.Service
+-keep public class * extends android.app.backup.BackupAgentHelper
+-keep public class * extends android.content.BroadcastReceiver
+-keep public class * extends android.content.ContentProvider
+-keep public class * extends android.preference.Preference
+-keep public class * extends org.mozilla.gecko.sync.syncadapter.SyncAdapter
+-keep class org.mozilla.gecko.sync.syncadapter.SyncAdapter
+
+# Preserve all native method names and the names of their classes.
+-keepclasseswithmembernames class * {
+    native <methods>;
+}
+
+-keepclasseswithmembers class * {
+    public <init>(android.content.Context, android.util.AttributeSet, int);
+}
+
+-keepclassmembers class * extends android.app.Activity {
+   public void *(android.view.View);
+}
+
+# Preserve enums. (For awful reasons, the runtime accesses them using introspection...)
+-keepclassmembers enum * {
+     *;
+}
+
+#
+# Rules from ProGuard's Android example:
+# http://proguard.sourceforge.net/manual/examples.html#androidapplication
+#
+
+# Switch off some optimizations that trip older versions of the Dalvik VM.
+
+-optimizations !code/simplification/arithmetic
+
+# Keep a fixed source file attribute and all line number tables to get line
+# numbers in the stack traces.
+# You can comment this out if you're not interested in stack traces.
+
+-renamesourcefileattribute SourceFile
+-keepattributes SourceFile,LineNumberTable
+
+# RemoteViews might need annotations.
+
+-keepattributes *Annotation*
+
+# Preserve all View implementations, their special context constructors, and
+# their setters.
+
+-keep public class * extends android.view.View {
+    public <init>(android.content.Context);
+    public <init>(android.content.Context, android.util.AttributeSet);
+    public <init>(android.content.Context, android.util.AttributeSet, int);
+    public void set*(...);
+}
+
+# Preserve all classes that have special context constructors, and the
+# constructors themselves.
+
+-keepclasseswithmembers class * {
+    public <init>(android.content.Context, android.util.AttributeSet);
+}
+
+# Preserve the special fields of all Parcelable implementations.
+
+-keepclassmembers class * implements android.os.Parcelable {
+    static android.os.Parcelable$Creator CREATOR;
+}
+
+# Preserve static fields of inner classes of R classes that might be accessed
+# through introspection.
+
+-keepclassmembers class **.R$* {
+  public static <fields>;
+}
+
+# Preserve the required interface from the License Verification Library
+# (but don't nag the developer if the library is not used at all).
+
+-keep public interface com.android.vending.licensing.ILicensingService
+
+-dontnote com.android.vending.licensing.ILicensingService
+
+# The Android Compatibility library references some classes that may not be
+# present in all versions of the API, but we know that's ok.
+
+-dontwarn android.support.**
+
+# Preserve all native method names and the names of their classes.
+
+-keepclasseswithmembernames class * {
+    native <methods>;
+}
+
+#
+# Mozilla-specific rules
+#
+# Merging classes can generate dex warnings about anonymous inner classes.
+-optimizations !class/merging/horizontal
+-optimizations !class/merging/vertical
+
+# Keep miscellaneous targets.
+
+# Keep the annotation.
+-keep @interface org.mozilla.gecko.mozglue.JNITarget
+
+# Keep classes tagged with the annotation.
+-keep @org.mozilla.gecko.mozglue.JNITarget class *
+
+# Keep all members of an annotated class.
+-keepclassmembers @org.mozilla.gecko.mozglue.JNITarget class * {
+    *;
+}
+
+# Keep annotated members of any class.
+-keepclassmembers class * {
+    @org.mozilla.gecko.mozglue.JNITarget *;
+}
+
+# Keep classes which contain at least one annotated element. Split over two directives
+# because, according to the developer of ProGuard, "the option -keepclasseswithmembers
+# doesn't combine well with the '*' wildcard" (And, indeed, using it causes things to
+# be deleted that we want to keep.)
+-keepclasseswithmembers class * {
+    @org.mozilla.gecko.mozglue.JNITarget <methods>;
+}
+-keepclasseswithmembers class * {
+    @org.mozilla.gecko.mozglue.JNITarget <fields>;
+}
+
+# Keep Robocop targets. TODO: Can omit these from release builds. Also, Bug 916507.
+
+# Same formula as above...
+-keep @interface org.mozilla.gecko.mozglue.RobocopTarget
+-keep @org.mozilla.gecko.mozglue.RobocopTarget class *
+-keepclassmembers class * {
+    @org.mozilla.gecko.mozglue.RobocopTarget *;
+}
+-keepclassmembers @org.mozilla.gecko.mozglue.RobocopTarget class * {
+    *;
+}
+-keepclasseswithmembers class * {
+    @org.mozilla.gecko.mozglue.RobocopTarget <methods>;
+}
+-keepclasseswithmembers class * {
+    @org.mozilla.gecko.mozglue.RobocopTarget <fields>;
+}
+
+# Keep WebRTC targets.
+-keep @interface org.mozilla.gecko.mozglue.WebRTCJNITarget
+-keep @org.mozilla.gecko.mozglue.WebRTCJNITarget class *
+-keepclassmembers class * {
+    @org.mozilla.gecko.mozglue.WebRTCJNITarget *;
+}
+-keepclassmembers @org.mozilla.gecko.mozglue.WebRTCJNITarget class * {
+    *;
+}
+-keepclasseswithmembers class * {
+    @org.mozilla.gecko.mozglue.WebRTCJNITarget <methods>;
+}
+-keepclasseswithmembers class * {
+    @org.mozilla.gecko.mozglue.WebRTCJNITarget <fields>;
+}
+
+# Keep generator-targeted entry points.
+-keep @interface org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI
+-keep @org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI class *
+-keepclassmembers class * {
+    @org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI *;
+}
+-keepclasseswithmembers class * {
+    @org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI <methods>;
+}
+-keepclasseswithmembers class * {
+    @org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI <fields>;
+}
+
+-keep @interface org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI
+-keep @org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI class *
+-keepclassmembers @org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI class * {
+    *;
+}
+
+# Disable obfuscation because it makes exception stack traces more difficult to read.
+-dontobfuscate
+
+# Suppress warnings about missing descriptor classes.
+#-dontnote **,!ch.boye.**,!org.mozilla.gecko.sync.**
--- a/mozglue/build/BionicGlue.cpp
+++ b/mozglue/build/BionicGlue.cpp
@@ -4,16 +4,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include <pthread.h>
 #include <string.h>
 #include <stdlib.h>
 #include <time.h>
 #include <unistd.h>
 #include <android/log.h>
+#include <sys/syscall.h>
 
 #include "mozilla/Alignment.h"
 
 #include <vector>
 
 #define NS_EXPORT __attribute__ ((visibility("default")))
 
 #if ANDROID_VERSION < 17 || defined(MOZ_WIDGET_ANDROID)
@@ -123,17 +124,26 @@ WRAP(fork)(void)
   }
   return pid;
 }
 #endif
 
 extern "C" NS_EXPORT int
 WRAP(raise)(int sig)
 {
-  return pthread_kill(pthread_self(), sig);
+  // Bug 741272: Bionic incorrectly uses kill(), which signals the
+  // process, and thus could signal another thread (and let this one
+  // return "successfully" from raising a fatal signal).
+  //
+  // Bug 943170: POSIX specifies pthread_kill(pthread_self(), sig) as
+  // equivalent to raise(sig), but Bionic also has a bug with these
+  // functions, where a forked child will kill its parent instead.
+
+  extern pid_t gettid(void);
+  return syscall(__NR_tgkill, getpid(), gettid(), sig);
 }
 
 /*
  * The following wrappers for PR_Xxx are needed until we can get
  * PR_DuplicateEnvironment landed in NSPR.
  * See see bug 772734 and bug 773414.
  *
  * We can't #include the pr headers here, and we can't call any of the
--- a/services/common/tests/unit/head_helpers.js
+++ b/services/common/tests/unit/head_helpers.js
@@ -30,16 +30,36 @@ function do_check_throws(aFunc, aResult,
     aFunc();
   } catch (e) {
     do_check_eq(e.result, aResult, aStack);
     return;
   }
   do_throw("Expected result " + aResult + ", none thrown.", aStack);
 }
 
+
+/**
+ * Test whether specified function throws exception with expected
+ * result.
+ *
+ * @param func
+ *        Function to be tested.
+ * @param message
+ *        Message of expected exception. <code>null</code> for no throws.
+ */
+function do_check_throws_message(aFunc, aResult) {
+  try {
+    aFunc();
+  } catch (e) {
+    do_check_eq(e.message, aResult);
+    return;
+  }
+  do_throw("Expected an error, none thrown.");
+}
+
 /**
  * Print some debug message to the console. All arguments will be printed,
  * separated by spaces.
  *
  * @param [arg0, arg1, arg2, ...]
  *        Any number of arguments to print out
  * @usage _("Hello World") -> prints "Hello World"
  * @usage _(1, 2, 3) -> prints "1 2 3"
--- a/services/common/utils.js
+++ b/services/common/utils.js
@@ -199,16 +199,24 @@ this.CommonUtils = {
   bytesAsHex: function bytesAsHex(bytes) {
     let hex = "";
     for (let i = 0; i < bytes.length; i++) {
       hex += ("0" + bytes[i].charCodeAt().toString(16)).slice(-2);
     }
     return hex;
   },
 
+  hexToBytes: function hexToBytes(str) {
+    let bytes = [];
+    for (let i = 0; i < str.length - 1; i += 2) {
+      bytes.push(parseInt(str.substr(i, 2), 16));
+    }
+    return String.fromCharCode.apply(String, bytes);
+  },
+
   /**
    * Base32 encode (RFC 4648) a string
    */
   encodeBase32: function encodeBase32(bytes) {
     const key = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
     let quanta = Math.floor(bytes.length / 5);
     let leftover = bytes.length % 5;
 
--- a/services/crypto/modules/utils.js
+++ b/services/crypto/modules/utils.js
@@ -6,16 +6,30 @@ const {classes: Cc, interfaces: Ci, resu
 
 this.EXPORTED_SYMBOLS = ["CryptoUtils"];
 
 Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 this.CryptoUtils = {
+  xor: function xor(a, b) {
+    let bytes = [];
+
+    if (a.length != b.length) {
+      throw new Error("can't xor unequal length strings: "+a.length+" vs "+b.length);
+    }
+
+    for (let i = 0; i < a.length; i++) {
+      bytes[i] = a.charCodeAt(i) ^ b.charCodeAt(i);
+    }
+
+    return String.fromCharCode.apply(String, bytes);
+  },
+
   /**
    * Generate a string of random bytes.
    */
   generateRandomBytes: function generateRandomBytes(length) {
     let rng = Cc["@mozilla.org/security/random-generator;1"]
                 .createInstance(Ci.nsIRandomGenerator);
     let bytes = rng.generateRandomBytes(length);
     return CommonUtils.byteArrayToString(bytes);
@@ -105,16 +119,32 @@ this.CryptoUtils = {
   makeHMACHasher: function makeHMACHasher(type, key) {
     let hasher = Cc["@mozilla.org/security/hmac;1"]
                    .createInstance(Ci.nsICryptoHMAC);
     hasher.init(type, key);
     return hasher;
   },
 
   /**
+   * HMAC-based Key Derivation (RFC 5869).
+   */
+  hkdf: function hkdf(ikm, xts, info, len) {
+    const BLOCKSIZE = 256 / 8;
+    if (typeof xts === undefined)
+      xts = String.fromCharCode(0, 0, 0, 0,  0, 0, 0, 0,
+                                0, 0, 0, 0,  0, 0, 0, 0,
+                                0, 0, 0, 0,  0, 0, 0, 0,
+                                0, 0, 0, 0,  0, 0, 0, 0);
+    let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
+                                       CryptoUtils.makeHMACKey(xts));
+    let prk = CryptoUtils.digestBytes(ikm, h);
+    return CryptoUtils.hkdfExpand(prk, info, len);
+  },
+
+  /**
    * HMAC-based Key Derivation Step 2 according to RFC 5869.
    */
   hkdfExpand: function hkdfExpand(prk, info, len) {
     const BLOCKSIZE = 256 / 8;
     let h = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
                                        CryptoUtils.makeHMACKey(prk));
     let T = "";
     let Tn = "";
@@ -453,29 +483,29 @@ this.CryptoUtils = {
       host: uri.asciiHost.toLowerCase(), // This includes punycoding.
       port: port.toString(10),
       hash: options.hash,
       ext: options.ext,
     };
 
     let contentType = CryptoUtils.stripHeaderAttributes(options.contentType);
 
-    if (!artifacts.hash &&
-        options.hasOwnProperty("payload") &&
-        options.payload) {
+    if (!artifacts.hash && options.hasOwnProperty("payload")
+        && options.payload) {
       let hasher = Cc["@mozilla.org/security/hash;1"]
                      .createInstance(Ci.nsICryptoHash);
       hasher.init(hash_algo);
       CryptoUtils.updateUTF8("hawk.1.payload\n", hasher);
       CryptoUtils.updateUTF8(contentType+"\n", hasher);
       CryptoUtils.updateUTF8(options.payload, hasher);
       CryptoUtils.updateUTF8("\n", hasher);
       let hash = hasher.finish(false);
-      // HAWK specifies this .hash to include trailing "==" padding.
-      let hash_b64 = CommonUtils.encodeBase64URL(hash, true);
+      // HAWK specifies this .hash to use +/ (not _-) and include the
+      // trailing "==" padding.
+      let hash_b64 = btoa(hash);
       artifacts.hash = hash_b64;
     }
 
     let requestString = ("hawk.1.header"        + "\n" +
                          artifacts.ts.toString(10) + "\n" +
                          artifacts.nonce        + "\n" +
                          artifacts.method       + "\n" +
                          artifacts.resource     + "\n" +
--- a/services/crypto/tests/unit/test_utils_hkdfExpand.js
+++ b/services/crypto/tests/unit/test_utils_hkdfExpand.js
@@ -88,21 +88,33 @@ function extract_hex(salt, ikm) {
 }
 
 function expand_hex(prk, info, len) {
   prk = _hexToString(prk);
   info = _hexToString(info);
   return CommonUtils.bytesAsHex(CryptoUtils.hkdfExpand(prk, info, len));
 }
 
+function hkdf_hex(ikm, salt, info, len) {
+  ikm = _hexToString(ikm);
+  if (salt)
+    salt = _hexToString(salt);
+  info = _hexToString(info);
+  return CommonUtils.bytesAsHex(CryptoUtils.hkdf(ikm, salt, info, len));
+}
+
 function run_test() {
   _("Verifying Test Case 1");
   do_check_eq(extract_hex(tc1.salt, tc1.IKM), tc1.PRK);
   do_check_eq(expand_hex(tc1.PRK, tc1.info, tc1.L), tc1.OKM);
+  do_check_eq(hkdf_hex(tc1.IKM, tc1.salt, tc1.info, tc1.L), tc1.OKM);
 
   _("Verifying Test Case 2");
   do_check_eq(extract_hex(tc2.salt, tc2.IKM), tc2.PRK);
   do_check_eq(expand_hex(tc2.PRK, tc2.info, tc2.L), tc2.OKM);
+  do_check_eq(hkdf_hex(tc2.IKM, tc2.salt, tc2.info, tc2.L), tc2.OKM);
 
   _("Verifying Test Case 3");
   do_check_eq(extract_hex(tc3.salt, tc3.IKM), tc3.PRK);
   do_check_eq(expand_hex(tc3.PRK, tc3.info, tc3.L), tc3.OKM);
+  do_check_eq(hkdf_hex(tc3.IKM, tc3.salt, tc3.info, tc3.L), tc3.OKM);
+  do_check_eq(hkdf_hex(tc3.IKM, undefined, tc3.info, tc3.L), tc3.OKM);
 }
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/FxAccountsClient.jsm
@@ -0,0 +1,330 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["FxAccountsClient"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-crypto/utils.js");
+
+// Default can be changed by the preference 'identity.fxaccounts.auth.uri'
+let _host = "https://api-accounts.dev.lcip.org/v1";
+try {
+  _host = Services.prefs.getCharPref("identity.fxaccounts.auth.uri");
+} catch(keepDefault) {}
+
+const HOST = _host;
+const PREFIX_NAME = "identity.mozilla.com/picl/v1/";
+
+const XMLHttpRequest =
+  Components.Constructor("@mozilla.org/xmlextras/xmlhttprequest;1");
+
+
+function stringToHex(str) {
+  let encoder = new TextEncoder("utf-8");
+  let bytes = encoder.encode(str);
+  return bytesToHex(bytes);
+}
+
+// XXX Sadly, CommonUtils.bytesAsHex doesn't handle typed arrays.
+function bytesToHex(bytes) {
+  let hex = [];
+  for (let i = 0; i < bytes.length; i++) {
+    hex.push((bytes[i] >>> 4).toString(16));
+    hex.push((bytes[i] & 0xF).toString(16));
+  }
+  return hex.join("");
+}
+
+this.FxAccountsClient = function(host = HOST) {
+  this.host = host;
+};
+
+this.FxAccountsClient.prototype = {
+  /**
+   * Create a new Firefox Account and authenticate
+   *
+   * @param email
+   *        The email address for the account (utf8)
+   * @param password
+   *        The user's password
+   * @return Promise
+   *        Returns a promise that resolves to an object:
+   *        {
+   *          uid: the user's unique ID
+   *          sessionToken: a session token
+   *        }
+   */
+  signUp: function (email, password) {
+    let uid;
+    let hexEmail = stringToHex(email);
+    let uidPromise = this._request("/raw_password/account/create", "POST", null,
+                          {email: hexEmail, password: password});
+
+    return uidPromise.then((result) => {
+      uid = result.uid;
+      return this.signIn(email, password)
+        .then(function(result) {
+          result.uid = uid;
+          return result;
+        });
+    });
+  },
+
+  /**
+   * Authenticate and create a new session with the Firefox Account API server
+   *
+   * @param email
+   *        The email address for the account (utf8)
+   * @param password
+   *        The user's password
+   * @return Promise
+   *        Returns a promise that resolves to an object:
+   *        {
+   *          uid: the user's unique ID
+   *          sessionToken: a session token
+   *          isVerified: flag indicating verification status of the email
+   *        }
+   */
+  signIn: function signIn(email, password) {
+    let hexEmail = stringToHex(email);
+    return this._request("/raw_password/session/create", "POST", null,
+                         {email: hexEmail, password: password});
+  },
+
+  /**
+   * Destroy the current session with the Firefox Account API server
+   *
+   * @param sessionTokenHex
+   *        The session token endcoded in hex
+   * @return Promise
+   */
+  signOut: function (sessionTokenHex) {
+    return this._request("/session/destroy", "POST",
+      this._deriveHawkCredentials(sessionTokenHex, "sessionToken"));
+  },
+
+  /**
+   * Check the verification status of the user's FxA email address
+   *
+   * @param sessionTokenHex
+   *        The current session token endcoded in hex
+   * @return Promise
+   */
+  recoveryEmailStatus: function (sessionTokenHex) {
+    return this._request("/recovery_email/status", "GET",
+      this._deriveHawkCredentials(sessionTokenHex, "sessionToken"));
+  },
+
+  /**
+   * Retrieve encryption keys
+   *
+   * @param keyFetchTokenHex
+   *        A one-time use key fetch token encoded in hex
+   * @return Promise
+   *        Returns a promise that resolves to an object:
+   *        {
+   *          kA: an encryption key for recevorable data
+   *          wrapKB: an encryption key that requires knowledge of the user's password
+   *        }
+   */
+  accountKeys: function (keyFetchTokenHex) {
+    let creds = this._deriveHawkCredentials(keyFetchTokenHex, "keyFetchToken");
+    let keyRequestKey = creds.extra.slice(0, 32);
+    let morecreds = CryptoUtils.hkdf(keyRequestKey, undefined,
+                                     PREFIX_NAME + "account/keys", 3 * 32);
+    let respHMACKey = morecreds.slice(0, 32);
+    let respXORKey = morecreds.slice(32, 96);
+
+    return this._request("/account/keys", "GET", creds).then(resp => {
+      if (!resp.bundle) {
+        throw new Error("failed to retrieve keys");
+      }
+
+      let bundle = CommonUtils.hexToBytes(resp.bundle);
+      let mac = bundle.slice(-32);
+
+      let hasher = CryptoUtils.makeHMACHasher(Ci.nsICryptoHMAC.SHA256,
+        CryptoUtils.makeHMACKey(respHMACKey));
+
+      let bundleMAC = CryptoUtils.digestBytes(bundle.slice(0, -32), hasher);
+      if (mac !== bundleMAC) {
+        throw new Error("error unbundling encryption keys");
+      }
+
+      let keyAWrapB = CryptoUtils.xor(respXORKey, bundle.slice(0, 64));
+
+      return {
+        kA: keyAWrapB.slice(0, 32),
+        wrapKB: keyAWrapB.slice(32)
+      };
+    });
+  },
+
+  /**
+   * Sends a public key to the FxA API server and returns a signed certificate
+   *
+   * @param sessionTokenHex
+   *        The current session token endcoded in hex
+   * @param serializedPublicKey
+   *        A public key (usually generated by jwcrypto)
+   * @param lifetime
+   *        The lifetime of the certificate
+   * @return Promise
+   *        Returns a promise that resolves to the signed certificate. The certificate
+   *        can be used to generate a Persona assertion.
+   */
+  signCertificate: function (sessionTokenHex, serializedPublicKey, lifetime) {
+    let creds = this._deriveHawkCredentials(sessionTokenHex, "sessionToken");
+
+    let body = { publicKey: serializedPublicKey,
+                 duration: lifetime };
+    return Promise.resolve()
+      .then(_ => this._request("/certificate/sign", "POST", creds, body))
+      .then(resp => resp.cert,
+            err => {dump("HAWK.signCertificate error: " + err + "\n");
+                    throw err;});
+  },
+
+  /**
+   * Determine if an account exists
+   *
+   * @param email
+   *        The email address to check
+   * @return Promise
+   *        The promise resolves to true if the account exists, or false
+   *        if it doesn't. The promise is rejected on other errors.
+   */
+  accountExists: function (email) {
+    let hexEmail = stringToHex(email);
+    return this._request("/auth/start", "POST", null, { email: hexEmail })
+      .then(
+        // the account exists
+        (result) => true,
+        (err) => {
+          // the account doesn't exist
+          if (err.errno === 102) {
+            return false;
+          }
+          // propogate other request errors
+          throw err;
+        }
+      );
+  },
+
+  /**
+   * The FxA auth server expects requests to certain endpoints to be authorized using Hawk.
+   * Hawk credentials are derived using shared secrets, which depend on the context
+   * (e.g. sessionToken vs. keyFetchToken).
+   *
+   * @param tokenHex
+   *        The current session token endcoded in hex
+   * @param context
+   *        A context for the credentials
+   * @param size
+   *        The size in bytes of the expected derived buffer
+   * @return credentials
+   *        Returns an object:
+   *        {
+   *          algorithm: sha256
+   *          id: the Hawk id (from the first 32 bytes derived)
+   *          key: the Hawk key (from bytes 32 to 64)
+   *          extra: size - 64 extra bytes
+   *        }
+   */
+  _deriveHawkCredentials: function (tokenHex, context, size) {
+    let token = CommonUtils.hexToBytes(tokenHex);
+    let out = CryptoUtils.hkdf(token, undefined, PREFIX_NAME + context, size || 3 * 32);
+
+    return {
+      algorithm: "sha256",
+      key: out.slice(32, 64),
+      extra: out.slice(64),
+      id: CommonUtils.bytesAsHex(out.slice(0, 32))
+    };
+  },
+
+  /**
+   * A general method for sending raw API calls to the FxA auth server.
+   * All request bodies and responses are JSON.
+   *
+   * @param path
+   *        API endpoint path
+   * @param method
+   *        The HTTP request method
+   * @param credentials
+   *        Hawk credentials
+   * @param jsonPayload
+   *        A JSON payload
+   * @return Promise
+   *        Returns a promise that resolves to the JSON response of the API call,
+   *        or is rejected with an error. Error responses have the following properties:
+   *        {
+   *          "code": 400, // matches the HTTP status code
+   *          "errno": 107, // stable application-level error number
+   *          "error": "Bad Request", // string description of the error type
+   *          "message": "the value of salt is not allowed to be undefined",
+   *          "info": "https://docs.dev.lcip.og/errors/1234" // link to more info on the error
+   *        }
+   */
+  _request: function hawkRequest(path, method, credentials, jsonPayload) {
+    let deferred = Promise.defer();
+    let xhr = new XMLHttpRequest({mozSystem: true});
+    let URI = this.host + path;
+    let payload;
+
+    xhr.mozBackgroundRequest = true;
+
+    if (jsonPayload) {
+      payload = JSON.stringify(jsonPayload);
+    }
+
+    xhr.open(method, URI);
+    xhr.channel.loadFlags = Ci.nsIChannel.LOAD_BYPASS_CACHE |
+                            Ci.nsIChannel.INHIBIT_CACHING;
+
+    // When things really blow up, reconstruct an error object that follows the general format
+    // of the server on error responses.
+    function constructError(err) {
+      return { error: err, message: xhr.statusText, code: xhr.status, errno: xhr.status };
+    }
+
+    xhr.onerror = function() {
+      deferred.reject(constructError('Request failed'));
+    };
+
+    xhr.onload = function onload() {
+      try {
+        let response = JSON.parse(xhr.responseText);
+        if (xhr.status !== 200 || response.error) {
+          // In this case, the response is an object with error information.
+          return deferred.reject(response);
+        }
+        deferred.resolve(response);
+      } catch (e) {
+        deferred.reject(constructError(e));
+      }
+    };
+
+    let uri = Services.io.newURI(URI, null, null);
+
+    if (credentials) {
+      let header = CryptoUtils.computeHAWK(uri, method, {
+                          credentials: credentials,
+                          payload: payload,
+                          contentType: "application/json"
+                        });
+      xhr.setRequestHeader("authorization", header.field);
+    }
+
+    xhr.setRequestHeader("Content-Type", "application/json");
+    xhr.send(payload);
+
+    return deferred.promise;
+  },
+};
+
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_DIRS += ['tests']
+EXTRA_JS_MODULES += ['FxAccountsClient.jsm']
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPCSHELL_TESTS_MANIFESTS += ['xpcshell/xpcshell.ini']
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/head.js
@@ -0,0 +1,18 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+"use strict";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+(function initFxAccountsTestingInfrastructure() {
+  do_get_profile();
+
+  let ns = {};
+  Cu.import("resource://testing-common/services-common/logging.js", ns);
+
+  ns.initTestLogging("Trace");
+}).call(this);
+
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/test_client.js
@@ -0,0 +1,231 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/FxAccountsClient.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://services-common/utils.js");
+
+function run_test() {
+  run_next_test();
+}
+
+function deferredStop(server) {
+    let deferred = Promise.defer();
+    server.stop(deferred.resolve);
+    return deferred.promise;
+}
+
+add_test(function test_hawk_credentials() {
+  let client = new FxAccountsClient();
+
+  let sessionToken = "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf";
+  let result = client._deriveHawkCredentials(sessionToken, "session");
+
+  do_check_eq(result.id, "639503a218ffbb62983e9628be5cd64a0438d0ae81b2b9dadeb900a83470bc6b");
+  do_check_eq(CommonUtils.bytesAsHex(result.key), "3a0188943837ab228fe74e759566d0e4837cbcc7494157aac4da82025b2811b2");
+
+  run_next_test();
+});
+
+add_task(function test_authenticated_get_request() {
+
+  let message = "{\"msg\": \"Great Success!\"}";
+  let credentials = {
+    id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
+    key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
+    algorithm: "sha256"
+  };
+  let method = "GET";
+
+  let server = httpd_setup({"/foo": function(request, response) {
+      do_check_true(request.hasHeader("Authorization"));
+
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  let client = new FxAccountsClient(server.baseURI);
+
+  let result = yield client._request("/foo", method, credentials);
+  do_check_eq("Great Success!", result.msg);
+
+  yield deferredStop(server);
+});
+
+add_task(function test_authenticated_post_request() {
+  let credentials = {
+    id: "eyJleHBpcmVzIjogMTM2NTAxMDg5OC4x",
+    key: "qTZf4ZFpAMpMoeSsX3zVRjiqmNs=",
+    algorithm: "sha256"
+  };
+  let method = "POST";
+
+  let server = httpd_setup({"/foo": function(request, response) {
+      do_check_true(request.hasHeader("Authorization"));
+
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "application/json");
+      response.bodyOutputStream.writeFrom(request.bodyInputStream, request.bodyInputStream.available());
+    }
+  });
+
+  let client = new FxAccountsClient(server.baseURI);
+
+  let result = yield client._request("/foo", method, credentials, {foo: "bar"});
+  do_check_eq("bar", result.foo);
+
+  yield deferredStop(server);
+});
+
+add_task(function test_500_error() {
+
+  let message = "<h1>Ooops!</h1>";
+  let method = "GET";
+
+  let server = httpd_setup({"/foo": function(request, response) {
+      response.setStatusLine(request.httpVersion, 500, "Internal Server Error");
+      response.bodyOutputStream.write(message, message.length);
+    }
+  });
+
+  let client = new FxAccountsClient(server.baseURI);
+
+  try {
+    yield client._request("/foo", method);
+  } catch (e) {
+    do_check_eq(500, e.code);
+    do_check_eq("Internal Server Error", e.message);
+  }
+
+  yield deferredStop(server);
+});
+
+add_task(function test_api_endpoints() {
+  let sessionMessage = JSON.stringify({sessionToken: "NotARealToken"});
+  let creationMessage = JSON.stringify({uid: "NotARealUid"});
+  let signoutMessage = JSON.stringify({});
+  let certSignMessage = JSON.stringify({cert: {bar: "baz"}});
+  let emailStatus = JSON.stringify({verified: true});
+
+  let authStarts = 0;
+
+  function writeResp(response, msg) {
+    response.bodyOutputStream.write(msg, msg.length);
+  }
+
+  let server = httpd_setup(
+    {
+      "/raw_password/account/create": function(request, response) {
+        let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+        let jsonBody = JSON.parse(body);
+        do_check_eq(jsonBody.email, "796f75406578616d706c652e636f6d");
+        do_check_eq(jsonBody.password, "biggersecret");
+
+        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.bodyOutputStream.write(creationMessage, creationMessage.length);
+      },
+      "/raw_password/session/create": function(request, response) {
+        let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+        let jsonBody = JSON.parse(body);
+        if (jsonBody.password === "bigsecret") {
+          do_check_eq(jsonBody.email, "6dc3a9406578616d706c652e636f6d");
+        } else if (jsonBody.password === "biggersecret") {
+          do_check_eq(jsonBody.email, "796f75406578616d706c652e636f6d");
+        }
+
+        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.bodyOutputStream.write(sessionMessage, sessionMessage.length);
+      },
+      "/recovery_email/status": function(request, response) {
+        do_check_true(request.hasHeader("Authorization"));
+        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.bodyOutputStream.write(emailStatus, emailStatus.length);
+      },
+      "/session/destroy": function(request, response) {
+        do_check_true(request.hasHeader("Authorization"));
+        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.bodyOutputStream.write(signoutMessage, signoutMessage.length);
+      },
+      "/certificate/sign": function(request, response) {
+        do_check_true(request.hasHeader("Authorization"));
+        let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+        let jsonBody = JSON.parse(body);
+        do_check_eq(JSON.parse(jsonBody.publicKey).foo, "bar");
+        do_check_eq(jsonBody.duration, 600);
+        response.setStatusLine(request.httpVersion, 200, "OK");
+        response.bodyOutputStream.write(certSignMessage, certSignMessage.length);
+      },
+      "/auth/start": function(request, response) {
+        if (authStarts === 0) {
+          response.setStatusLine(request.httpVersion, 200, "OK");
+          writeResp(response, JSON.stringify({}));
+        } else if (authStarts === 1) {
+          response.setStatusLine(request.httpVersion, 400, "NOT OK");
+          writeResp(response, JSON.stringify({errno: 102, error: "no such account"}));
+        } else if (authStarts === 2) {
+          response.setStatusLine(request.httpVersion, 400, "NOT OK");
+          writeResp(response, JSON.stringify({errno: 107, error: "boom"}));
+        }
+        authStarts++;
+      },
+    }
+  );
+
+  let client = new FxAccountsClient(server.baseURI);
+  let result = undefined;
+
+  result = yield client.signUp('you@example.com', 'biggersecret');
+  do_check_eq("NotARealUid", result.uid);
+
+  result = yield client.signIn('mé@example.com', 'bigsecret');
+  do_check_eq("NotARealToken", result.sessionToken);
+
+  result = yield client.signOut('NotARealToken');
+  do_check_eq(typeof result, "object");
+
+  result = yield client.recoveryEmailStatus('NotARealToken');
+  do_check_eq(result.verified, true);
+
+  result = yield client.signCertificate('NotARealToken', JSON.stringify({foo: "bar"}), 600);
+  do_check_eq("baz", result.bar);
+
+  result = yield client.accountExists('hey@example.com');
+  do_check_eq(result, true);
+  result = yield client.accountExists('hey2@example.com');
+  do_check_eq(result, false);
+  try {
+    result = yield client.accountExists('hey3@example.com');
+  } catch(e) {
+    do_check_eq(e.errno, 107);
+  }
+
+  yield deferredStop(server);
+});
+
+add_task(function test_error_response() {
+  let errorMessage = JSON.stringify({error: "Oops", code: 400, errno: 99});
+
+  let server = httpd_setup(
+    {
+      "/raw_password/session/create": function(request, response) {
+        let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
+
+        response.setStatusLine(request.httpVersion, 400, "NOT OK");
+        response.bodyOutputStream.write(errorMessage, errorMessage.length);
+      },
+    }
+  );
+
+  let client = new FxAccountsClient(server.baseURI);
+
+  try {
+    let result = yield client.signIn('mé@example.com', 'bigsecret');
+  } catch(result) {
+    do_check_eq("Oops", result.error);
+    do_check_eq(400, result.code);
+    do_check_eq(99, result.errno);
+  }
+
+  yield deferredStop(server);
+});
new file mode 100644
--- /dev/null
+++ b/services/fxaccounts/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+head = head.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
+tail =
+
+[test_client.js]
+
--- a/services/moz.build
+++ b/services/moz.build
@@ -2,16 +2,17 @@
 # vim: set filetype=python:
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 PARALLEL_DIRS += [
     'common',
     'crypto',
+    'fxaccounts',
 ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
     # MOZ_SERVICES_HEALTHREPORT and therefore MOZ_DATA_REPORTING are
     # defined on Android, but these features are implemented using Java.
     if CONFIG['MOZ_SERVICES_HEALTHREPORT']:
         PARALLEL_DIRS += ['healthreport']
 
--- a/toolkit/components/places/PlacesDBUtils.jsm
+++ b/toolkit/components/places/PlacesDBUtils.jsm
@@ -878,39 +878,39 @@ this.PlacesDBUtils = {
         query:     "SELECT count(*) FROM moz_bookmarks "
                  + "WHERE TYPE = :type_folder "
                  + "AND parent NOT IN (0, :places_root, :tags_folder) " },
 
       { histogram: "PLACES_KEYWORDS_COUNT",
         query:     "SELECT count(*) FROM moz_keywords " },
 
       { histogram: "PLACES_SORTED_BOOKMARKS_PERC",
-        query:     "SELECT ROUND(( "
+        query:     "SELECT IFNULL(ROUND(( "
                  +   "SELECT count(*) FROM moz_bookmarks b "
                  +   "JOIN moz_bookmarks t ON t.id = b.parent "
                  +   "AND t.parent <> :tags_folder AND t.parent > :places_root "
                  +   "WHERE b.type  = :type_bookmark "
                  +   ") * 100 / ( "
                  +   "SELECT count(*) FROM moz_bookmarks b "
                  +   "JOIN moz_bookmarks t ON t.id = b.parent "
                  +   "AND t.parent <> :tags_folder "
                  +   "WHERE b.type = :type_bookmark "
-                 + ")) " },
+                 + ")), 0) " },
 
       { histogram: "PLACES_TAGGED_BOOKMARKS_PERC",
-        query:     "SELECT ROUND(( "
+        query:     "SELECT IFNULL(ROUND(( "
                  +   "SELECT count(*) FROM moz_bookmarks b "
                  +   "JOIN moz_bookmarks t ON t.id = b.parent "
                  +   "AND t.parent = :tags_folder "
                  +   ") * 100 / ( "
                  +   "SELECT count(*) FROM moz_bookmarks b "
                  +   "JOIN moz_bookmarks t ON t.id = b.parent "
                  +   "AND t.parent <> :tags_folder "
                  +   "WHERE b.type = :type_bookmark "
-                 + ")) " },
+                 + ")), 0) " },
 
       { histogram: "PLACES_DATABASE_FILESIZE_MB",
         callback: function () {
           let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
           DBFile.append("places.sqlite");
           return parseInt(DBFile.fileSize / BYTES_PER_MEBIBYTE);
         }
       },
@@ -965,23 +965,22 @@ this.PlacesDBUtils = {
     function reportResult(aProbe, aValue) {
       outstandingProbes--;
 
       try {
         let value = aValue;
         if ("callback" in aProbe) {
           value = aProbe.callback(value);
         }
-        if (isFinite(value)) {
-          probeValues[aProbe.histogram] = value;
-          Services.telemetry.getHistogramById(aProbe.histogram)
-                            .add(value);
-        }
+        probeValues[aProbe.histogram] = value;
+        Services.telemetry.getHistogramById(aProbe.histogram).add(value);
       } catch (ex) {
-        Components.utils.reportError(ex);
+        Components.utils.reportError("Error adding value " + value +
+                                     " to histogram " + aProbe.histogram +
+                                     ": " + ex);
       }
 
       if (!outstandingProbes && aHealthReportCallback) {
         try {
           aHealthReportCallback(probeValues);
         } catch (ex) {
           Components.utils.reportError(ex);
         }
--- a/toolkit/modules/WindowsPrefSync.jsm
+++ b/toolkit/modules/WindowsPrefSync.jsm
@@ -67,16 +67,25 @@ this.WindowsPrefSync = {
    */
   desktopControlledPrefs: ["app.update.auto",
     "app.update.enabled",
     "app.update.service.enabled",
     "app.update.metro.enabled",
     "browser.sessionstore.resume_session_once"],
 
   /**
+   * Returns the base path where registry sync prefs are stored.
+   */
+  get prefRegistryPath() {
+    let profileService = Cc["@mozilla.org/toolkit/profile-service;1"].
+      createInstance(Ci.nsIToolkitProfileService);
+    return PREF_BASE_KEY + profileService.selectedProfile.name + "\\";
+  },
+
+  /**
    * The following preferences will be pushed to registry from Metro
    * Firefox and pulled in from Desktop Firefox.
    *
    * browser.sessionstore.resume_session_once is mainly for the switch to Metro
    * and switch to Desktop feature.
    */
   metroControlledPrefs: ["browser.sessionstore.resume_session_once"],
 
@@ -107,18 +116,17 @@ this.WindowsPrefSync = {
         prefFunc = "getBoolPref";
       else if (prefType == Ci.nsIPrefBranch.PREF_STRING)
         prefFunc = "getCharPref";
       else
         throw "Unsupported pref type";
 
       let prefValue = Services.prefs[prefFunc](aPrefName);
       registry.create(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
-        PREF_BASE_KEY + prefType,
-        Ci.nsIWindowsRegKey.ACCESS_WRITE);
+        this.prefRegistryPath + prefType, Ci.nsIWindowsRegKey.ACCESS_WRITE);
       // Always write as string, but the registry subfolder will determine
       // how Metro interprets that string value.
       registry.writeStringValue(aPrefName, prefValue);
     } catch (ex) {
       Cu.reportError("Couldn't push pref " + aPrefName + ": " + ex);
     } finally {
       registry.close();
     }
@@ -126,17 +134,17 @@ this.WindowsPrefSync = {
 
   /**
    * Pulls in all shared prefs from the registry
    */
   pullSharedPrefs: function() {
     function pullSharedPrefType(prefType, prefFunc) {
       try {
         registry.create(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
-          PREF_BASE_KEY + prefType,
+          self.prefRegistryPath + prefType,
           Ci.nsIWindowsRegKey.ACCESS_ALL);
         for (let i = 0; i < registry.valueCount; i++) {
           let prefName = registry.getValueName(i);
           let prefValue = registry.readStringValue(prefName);
           if (prefType == Ci.nsIPrefBranch.PREF_BOOL) {
             prefValue = prefValue == "true";
           }
           if (self.prefListToPull.indexOf(prefName) != -1) {
--- a/widget/android/AndroidBridge.cpp
+++ b/widget/android/AndroidBridge.cpp
@@ -58,18 +58,18 @@ class AndroidRefable {
 
 // This isn't in AndroidBridge.h because including StrongPointer.h there is gross
 static android::sp<AndroidRefable> (*android_SurfaceTexture_getNativeWindow)(JNIEnv* env, jobject surfaceTexture) = nullptr;
 
 jclass AndroidBridge::GetClassGlobalRef(JNIEnv* env, const char* className)
 {
     jobject classLocalRef = env->FindClass(className);
     if (!classLocalRef) {
-        ALOG(">>> FATAL JNI ERROR! FindClass(className=\"%s\") failed. Did "
-             "ProGuard optimize away a non-public class?", className);
+        ALOG(">>> FATAL JNI ERROR! FindClass(className=\"%s\") failed. Did ProGuard optimize away something it shouldn't have?",
+             className);
         env->ExceptionDescribe();
         MOZ_CRASH();
     }
     jobject classGlobalRef = env->NewGlobalRef(classLocalRef);
     if (!classGlobalRef) {
         env->ExceptionDescribe();
         MOZ_CRASH();
     }
@@ -80,60 +80,60 @@ jclass AndroidBridge::GetClassGlobalRef(
 }
 
 jmethodID AndroidBridge::GetMethodID(JNIEnv* env, jclass jClass,
                               const char* methodName, const char* methodType)
 {
    jmethodID methodID = env->GetMethodID(jClass, methodName, methodType);
    if (!methodID) {
        ALOG(">>> FATAL JNI ERROR! GetMethodID(methodName=\"%s\", "
-            "methodType=\"%s\") failed. Did ProGuard optimize away a non-"
-            "public method?", methodName, methodType);
+            "methodType=\"%s\") failed. Did ProGuard optimize away something it shouldn't have?",
+            methodName, methodType);
        env->ExceptionDescribe();
        MOZ_CRASH();
    }
    return methodID;
 }
 
 jmethodID AndroidBridge::GetStaticMethodID(JNIEnv* env, jclass jClass,
                                const char* methodName, const char* methodType)
 {
   jmethodID methodID = env->GetStaticMethodID(jClass, methodName, methodType);
   if (!methodID) {
       ALOG(">>> FATAL JNI ERROR! GetStaticMethodID(methodName=\"%s\", "
-           "methodType=\"%s\") failed. Did ProGuard optimize away a non-"
-           "public method?", methodName, methodType);
+           "methodType=\"%s\") failed. Did ProGuard optimize away something it shouldn't have?",
+           methodName, methodType);
       env->ExceptionDescribe();
       MOZ_CRASH();
   }
   return methodID;
 }
 
 jfieldID AndroidBridge::GetFieldID(JNIEnv* env, jclass jClass,
                            const char* fieldName, const char* fieldType)
 {
     jfieldID fieldID = env->GetFieldID(jClass, fieldName, fieldType);
     if (!fieldID) {
         ALOG(">>> FATAL JNI ERROR! GetFieldID(fieldName=\"%s\", "
-             "fieldType=\"%s\") failed. Did ProGuard optimize away a non-"
-             "public field?", fieldName, fieldType);
+             "fieldType=\"%s\") failed. Did ProGuard optimize away something it shouldn't have?",
+             fieldName, fieldType);
         env->ExceptionDescribe();
         MOZ_CRASH();
     }
     return fieldID;
 }
 
 jfieldID AndroidBridge::GetStaticFieldID(JNIEnv* env, jclass jClass,
                            const char* fieldName, const char* fieldType)
 {
     jfieldID fieldID = env->GetStaticFieldID(jClass, fieldName, fieldType);
     if (!fieldID) {
         ALOG(">>> FATAL JNI ERROR! GetStaticFieldID(fieldName=\"%s\", "
-             "fieldType=\"%s\") failed. Did ProGuard optimize away a non-"
-             "public field?", fieldName, fieldType);
+             "fieldType=\"%s\") failed. Did ProGuard optimize away something it shouldn't have?",
+             fieldName, fieldType);
         env->ExceptionDescribe();
         MOZ_CRASH();
     }
     return fieldID;
 }
 
 void
 AndroidBridge::ConstructBridge(JNIEnv *jEnv)
--- a/widget/windows/winrt/FrameworkView.cpp
+++ b/widget/windows/winrt/FrameworkView.cpp
@@ -409,17 +409,17 @@ FrameworkView::OnWindowSizeChanged(ICore
   return S_OK;
 }
 
 HRESULT
 FrameworkView::OnWindowActivated(ICoreWindow* aSender, IWindowActivatedEventArgs* aArgs)
 {
   LogFunction();
   if (mShuttingDown || !mWidget)
-    return E_FAIL;
+    return S_OK;
   CoreWindowActivationState state;
   aArgs->get_WindowActivationState(&state);
   mWinActiveState = !(state == CoreWindowActivationState::CoreWindowActivationState_Deactivated);
   SendActivationEvent();
   return S_OK;
 }
 
 HRESULT
--- a/widget/windows/winrt/MetroInput.cpp
+++ b/widget/windows/winrt/MetroInput.cpp
@@ -491,16 +491,18 @@ MetroInput::OnPointerPressed(UI::Core::I
   if (mTouches.Count() == 1) {
     // If this is the first touchstart of a touch session reset some
     // tracking flags.
     mContentConsumingTouch = false;
     mApzConsumingTouch = false;
     mRecognizerWantsEvents = true;
     mCancelable = true;
     mCanceledIds.Clear();
+  } else {
+    mCancelable = false;
   }
 
   InitTouchEventTouchList(touchEvent);
   DispatchAsyncTouchEvent(touchEvent);
 
   if (ShouldDeliverInputToRecognizer()) {
     mGestureRecognizer->ProcessDownEvent(currentPoint.Get());
   }
@@ -1136,28 +1138,39 @@ MetroInput::DeliverNextQueuedTouchEvent(
    * Notes:
    * - never rely on the contents of mTouches here, since this is a delayed
    *   callback. mTouches will likely have been modified.
    */
 
   // Test for chrome vs. content target. To do this we only use the first touch
   // point since that will be the input batch target. Cache this for touch events
   // since HitTestChrome has to send a dom event.
-  if (mCancelable && event->message == NS_TOUCH_START && mTouches.Count() == 1) {
+  if (mCancelable && event->message == NS_TOUCH_START) {
     nsRefPtr<Touch> touch = event->touches[0];
     LayoutDeviceIntPoint pt = LayoutDeviceIntPoint::FromUntyped(touch->mRefPoint);
     bool apzIntersect = mWidget->ApzHitTest(mozilla::ScreenIntPoint(pt.x, pt.y));
     mChromeHitTestCacheForTouch = (apzIntersect && HitTestChrome(pt));
   }
 
   // If this event is destined for chrome, deliver it directly there bypassing
   // the apz.
   if (mChromeHitTestCacheForTouch) {
     DUMP_TOUCH_IDS("DOM(1)", event);
     mWidget->DispatchEvent(event, status);
+    if (mCancelable) {
+      // Disable gesture based events (taps, swipes, rotation) if
+      // preventDefault is called on touchstart.
+      if (nsEventStatus_eConsumeNoDefault == status) {
+        mRecognizerWantsEvents = false;
+        mGestureRecognizer->CompleteGesture();
+      }
+      if (event->message == NS_TOUCH_MOVE) {
+        mCancelable = false;
+      }
+    }
     return;
   }
 
   // If we have yet to deliver the first touch start and touch move, deliver the
   // event to both content and the apz. Ignore the apz's return result since we
   // give content the option of saying it wants to consume touch for both events.
   if (mCancelable) {
     WidgetTouchEvent transformedEvent(*event);