Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 19 May 2016 21:42:22 -0400
changeset 298197 c67dc1f9fab86d4f2cf3224307809c44fe3ce820
parent 298195 7b3ca5c5381c9df61729197263115eaf69ea98ce (current diff)
parent 298196 2013e1255bf82c59a3d90ebc69895f62e9a180f7 (diff)
child 298198 f70b8561b4796217c3328dfc97b61e8ae934c1dd
push id77063
push userryanvm@gmail.com
push dateFri, 20 May 2016 01:43:53 +0000
treeherdermozilla-inbound@f70b8561b479 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone49.0a1
first release with
nightly linux32
c67dc1f9fab8 / 49.0a1 / 20160520030251 / files
nightly linux64
c67dc1f9fab8 / 49.0a1 / 20160520030251 / files
nightly mac
c67dc1f9fab8 / 49.0a1 / 20160520030251 / files
nightly win32
c67dc1f9fab8 / 49.0a1 / 20160520030251 / files
nightly win64
c67dc1f9fab8 / 49.0a1 / 20160520030251 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c. a=merge
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -1467,28 +1467,28 @@ var BookmarkingUI = {
       let title = node.title;
       let icon = node.icon;
 
       let item =
         document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
                                  "menuitem");
       item.setAttribute("label", title || uri);
       item.setAttribute("targetURI", uri);
-      item.setAttribute("context", "hideRecentlyBookmarked");
+      item.setAttribute("simulated-places-node", true);
       item.setAttribute("class", "menuitem-iconic menuitem-with-favicon bookmark-item " +
                                  aExtraCSSClass);
       item.addEventListener("command", onItemCommand);
       if (icon) {
         item.setAttribute("image", icon);
       }
+      item._placesNode = node;
       fragment.appendChild(item);
     }
     root.containerOpen = false;
     aHeaderItem.parentNode.insertBefore(fragment, aHeaderItem.nextSibling);
-    aHeaderItem.setAttribute("context", "hideRecentlyBookmarked");
   },
 
   showRecentlyBookmarked() {
     Services.prefs.setBoolPref(this.RECENTLY_BOOKMARKED_PREF, true);
   },
 
   hideRecentlyBookmarked() {
     Services.prefs.setBoolPref(this.RECENTLY_BOOKMARKED_PREF, false);
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -402,23 +402,16 @@
                 accesskey="&showRecentlyBookmarked.accesskey;"
                 oncommand="BookmarkingUI.showRecentlyBookmarked();"
                 closemenu="single"
                 ignoreitem="true"
                 ordinal="2"
                 hidden="true"/>
     </menupopup>
 
-    <menupopup id="hideRecentlyBookmarked">
-      <menuitem label="&hideRecentlyBookmarked.label;"
-                accesskey="&hideRecentlyBookmarked.accesskey;"
-                oncommand="BookmarkingUI.hideRecentlyBookmarked();"
-                closemenu="single"/>
-    </menupopup>
-
     <panel id="ctrlTab-panel" class="KUI-panel" hidden="true" norestorefocus="true" level="top">
       <hbox>
         <button class="ctrlTab-preview" flex="1"/>
         <button class="ctrlTab-preview" flex="1"/>
         <button class="ctrlTab-preview" flex="1"/>
         <button class="ctrlTab-preview" flex="1"/>
         <button class="ctrlTab-preview" flex="1"/>
         <button class="ctrlTab-preview" flex="1"/>
--- a/browser/components/places/PlacesUIUtils.jsm
+++ b/browser/components/places/PlacesUIUtils.jsm
@@ -607,18 +607,20 @@ this.PlacesUIUtils = {
    * history or in bookmarks).
    *
    * @param aNode
    *        a node, except the root node of a query.
    * @return true if the aNode represents a removable entry, false otherwise.
    */
   canUserRemove: function (aNode) {
     let parentNode = aNode.parent;
-    if (!parentNode)
-      throw new Error("canUserRemove doesn't accept root nodes");
+    if (!parentNode) {
+      // canUserRemove doesn't accept root nodes.
+      return false;
+    }
 
     // If it's not a bookmark, we can remove it unless it's a child of a
     // livemark.
     if (aNode.itemId == -1) {
       // Rather than executing a db query, checking the existence of the feedURI
       // annotation, detect livemark children by the fact that they are the only
       // direct non-bookmark children of bookmark folders.
       return !PlacesUtils.nodeIsFolder(parentNode);
--- a/browser/components/places/content/browserPlacesViews.js
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -191,17 +191,17 @@ PlacesViewBase.prototype = {
     let container = this._resultNode;
     let orientation = Ci.nsITreeView.DROP_BEFORE;
     let tagName = null;
 
     let selectedNode = this.selectedNode;
     if (selectedNode) {
       let popup = document.popupNode;
       if (!popup._placesNode || popup._placesNode == this._resultNode ||
-          popup._placesNode.itemId == -1) {
+          popup._placesNode.itemId == -1 || !selectedNode.parent) {
         // If a static menuitem is selected, or if the root node is selected,
         // the insertion point is inside the folder, at the end.
         container = selectedNode;
         orientation = Ci.nsITreeView.DROP_ON;
       }
       else {
         // In all other cases the insertion point is before that node.
         container = selectedNode.parent;
@@ -889,17 +889,18 @@ PlacesViewBase.prototype = {
       // Menus that have static content at the end, but are initially empty,
       // use a special "builder" attribute to figure out where to start
       // inserting places nodes.
       if (child.getAttribute("builder") == "end") {
         aPopup.insertBefore(aPopup._endMarker, child);
         break;
       }
 
-      if (child._placesNode && !firstNonStaticNodeFound) {
+      if (child._placesNode && !child.hasAttribute("simulated-places-node") &&
+          !firstNonStaticNodeFound) {
         firstNonStaticNodeFound = true;
         aPopup.insertBefore(aPopup._startMarker, child);
       }
     }
     if (!firstNonStaticNodeFound) {
       aPopup.insertBefore(aPopup._startMarker, aPopup._endMarker);
     }
   },
--- a/browser/components/places/content/controller.js
+++ b/browser/components/places/content/controller.js
@@ -451,18 +451,16 @@ PlacesController.prototype = {
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
           nodeData["separator"] = true;
           break;
         case Ci.nsINavHistoryResultNode.RESULT_TYPE_URI:
           nodeData["link"] = true;
           uri = NetUtil.newURI(node.uri);
           if (PlacesUtils.nodeIsBookmark(node)) {
             nodeData["bookmark"] = true;
-            PlacesUtils.nodeIsTagQuery(node.parent)
-
             var parentNode = node.parent;
             if (parentNode) {
               if (PlacesUtils.nodeIsTagQuery(parentNode))
                 nodeData["tagChild"] = true;
               else if (this.hasCachedLivemarkInfo(parentNode))
                 nodeData["livemarkChild"] = true;
             }
           }
--- a/browser/components/places/tests/browser/browser_views_liveupdate.js
+++ b/browser/components/places/tests/browser/browser_views_liveupdate.js
@@ -317,17 +317,17 @@ function getNodeForToolbarItem(aItemId, 
   var toolbar = document.getElementById("PlacesToolbarItems");
 
   function findNode(aContainer) {
     var children = aContainer.childNodes;
     for (var i = 0, staticNodes = 0; i < children.length; i++) {
       var child = children[i];
 
       // Is this a Places node?
-      if (!child._placesNode) {
+      if (!child._placesNode || child.hasAttribute("simulated-places-node")) {
         staticNodes++;
         continue;
       }
 
       if (child._placesNode.itemId == aItemId) {
         let valid = aValidator ? aValidator(child) : true;
         return [child._placesNode, i - staticNodes, valid];
       }
@@ -360,17 +360,17 @@ function getNodeForMenuItem(aItemId, aVa
   var menu = document.getElementById("bookmarksMenu");
 
   function findNode(aContainer) {
     var children = aContainer.childNodes;
     for (var i = 0, staticNodes = 0; i < children.length; i++) {
       var child = children[i];
 
       // Is this a Places node?
-      if (!child._placesNode) {
+      if (!child._placesNode || child.hasAttribute("simulated-places-node")) {
         staticNodes++;
         continue;
       }
 
       if (child._placesNode.itemId == aItemId) {
         let valid = aValidator ? aValidator(child) : true;
         return [child._placesNode, i - staticNodes, valid];
       }
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -377,26 +377,19 @@ Toolbox.prototype = {
         this._URL = location;
       }
 
       this.browserRequire = BrowserLoader({
         window: this.doc.defaultView,
         useOnlyShared: true
       }).require;
 
-      this.React = this.browserRequire(
-        "devtools/client/shared/vendor/react");
-      this.ReactDOM = this.browserRequire(
-        "devtools/client/shared/vendor/react-dom");
-
       iframe.setAttribute("aria-label", toolboxStrings("toolbox.label"));
       let domHelper = new DOMHelpers(iframe.contentWindow);
       domHelper.onceDOMReady(() => {
-        // Build the Notification box as soon as the DOM is ready.
-        this._buildNotificationBox();
         domReady.resolve();
       }, this._URL);
 
       // Optimization: fire up a few other things before waiting on
       // the iframe being ready (makes startup faster)
 
       // Load the toolbox-level actor fronts and utilities now
       yield this._target.makeRemote();
@@ -482,16 +475,28 @@ Toolbox.prototype = {
       if (DevToolsUtils.testing) {
         yield performanceFrontConnection;
       }
 
       this.emit("ready");
     }.bind(this)).then(null, console.error.bind(console));
   },
 
+  /**
+   * loading React modules when needed (to avoid performance penalties
+   * during Firefox start up time).
+   */
+  get React() {
+    return this.browserRequire("devtools/client/shared/vendor/react");
+  },
+
+  get ReactDOM() {
+    return this.browserRequire("devtools/client/shared/vendor/react-dom");
+  },
+
   _pingTelemetry: function () {
     this._telemetry.toolOpened("toolbox");
 
     this._telemetry.logOncePerBrowserVersion(OS_HISTOGRAM, system.getOSCPU());
     this._telemetry.logOncePerBrowserVersion(OS_IS_64_BITS, system.is64Bit ? 1 : 0);
     this._telemetry.logOncePerBrowserVersion(SCREENSIZE_HISTOGRAM, system.getScreenDimensions());
   },
 
@@ -807,29 +812,33 @@ Toolbox.prototype = {
     if (toolDefinition.onkey &&
         ((this.currentToolId === toolId) ||
           (toolId == "webconsole" && this.splitConsole))) {
       toolDefinition.onkey(this.getCurrentPanel(), this);
     }
   },
 
   /**
-   * Build the notification box. Called every time the host changes.
+   * Build the notification box as soon as needed.
    */
-  _buildNotificationBox: function () {
-    let { NotificationBox, PriorityLevels } =
-      this.browserRequire("devtools/client/shared/components/notification-box");
+  get notificationBox() {
+    if (!this._notificationBox) {
+      let { NotificationBox, PriorityLevels } =
+        this.browserRequire(
+          "devtools/client/shared/components/notification-box");
 
-    NotificationBox = this.React.createFactory(NotificationBox);
+      NotificationBox = this.React.createFactory(NotificationBox);
 
-    // Render NotificationBox and assign priority levels to it.
-    let box = this.doc.getElementById("toolbox-notificationbox");
-    this.notificationBox = Object.assign(
-      this.ReactDOM.render(NotificationBox({}), box),
-      PriorityLevels);
+      // Render NotificationBox and assign priority levels to it.
+      let box = this.doc.getElementById("toolbox-notificationbox");
+      this._notificationBox = Object.assign(
+        this.ReactDOM.render(NotificationBox({}), box),
+        PriorityLevels);
+    }
+    return this._notificationBox;
   },
 
   /**
    * Build the buttons for changing hosts. Called every time
    * the host changes.
    */
   _buildDockButtons: function () {
     let dockBox = this.doc.getElementById("toolbox-dock-buttons");
@@ -2093,17 +2102,17 @@ Toolbox.prototype = {
 
         outstanding.push(panel.destroy());
       } catch (e) {
         // We don't want to stop here if any panel fail to close.
         console.error("Panel " + id + ":", e);
       }
     }
 
-    this.React = this.ReactDOM = this.browserRequire = null;
+    this.browserRequire = null;
 
     // Now that we are closing the toolbox we can re-enable the cache settings
     // and disable the service workers testing settings for the current tab.
     // FF41+ automatically cleans up state in actor on disconnect.
     if (this.target.activeTab && !this.target.activeTab.traits.noTabReconfigureOnClose) {
       this.target.activeTab.reconfigure({
         "cacheDisabled": false,
         "serviceWorkersTestingEnabled": false
--- a/devtools/client/responsivedesign/test/browser_responsiveui_touch.js
+++ b/devtools/client/responsivedesign/test/browser_responsiveui_touch.js
@@ -1,45 +1,148 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const TEST_URI = "http://mochi.test:8888/browser/devtools/client/" +
                  "responsivedesign/test/touch.html";
+const layoutReflowSynthMouseMove = "layout.reflow.synthMouseMove";
+const domViewportEnabled = "dom.meta-viewport.enabled";
 
 add_task(function* () {
   let tab = yield addTab(TEST_URI);
   let {rdm} = yield openRDM(tab);
+  yield pushPrefs([layoutReflowSynthMouseMove, false]);
   yield testWithNoTouch();
   yield rdm.enableTouch();
   yield testWithTouch();
   yield rdm.disableTouch();
   yield testWithNoTouch();
   yield closeRDM(rdm);
 });
 
 function* testWithNoTouch() {
   let div = content.document.querySelector("div");
-  let x = 2, y = 2;
-  yield BrowserTestUtils.synthesizeMouse("div", x, y,
-        { type: "mousedown", isSynthesized: false }, gBrowser.selectedBrowser);
-  x += 20; y += 10;
+  let x = 0, y = 0;
+
+  info("testWithNoTouch: Initial test parameter and mouse mouse outside div element");
+  x = -1, y = -1;
   yield BrowserTestUtils.synthesizeMouse("div", x, y,
         { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
-  is(div.style.transform, "none", "touch shouldn't work");
+  div.style.transform = "none";
+  div.style.backgroundColor = "";
+
+  info("testWithNoTouch: Move mouse into the div element");
+  yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousemove", isSynthesized: false },
+        gBrowser.selectedBrowser);
+  is(div.style.backgroundColor, "red", "mouseenter or mouseover should work");
+
+  info("testWithNoTouch: Drag the div element");
+  yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousedown", isSynthesized: false },
+        gBrowser.selectedBrowser);
+  x = 100; y = 100;
+  yield BrowserTestUtils.synthesizeMouse("div", x, y,
+        { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+  is(div.style.transform, "none", "touchmove shouldn't work");
   yield BrowserTestUtils.synthesizeMouse("div", x, y,
         { type: "mouseup", isSynthesized: false }, gBrowser.selectedBrowser);
+
+  info("testWithNoTouch: Move mouse out of the div element");
+  x = -1; y = -1;
+  yield BrowserTestUtils.synthesizeMouse("div", x, y,
+        { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+  is(div.style.backgroundColor, "blue", "mouseout or mouseleave should work");
+
+  info("testWithNoTouch: Click the div element");
+  yield synthesizeClick(div);
+  is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work");
 }
 
 function* testWithTouch() {
   let div = content.document.querySelector("div");
-  let x = 2, y = 2;
+  let x = 0, y = 0;
+
+  info("testWithTouch: Initial test parameter and mouse mouse outside div element");
+  x = -1, y = -1;
   yield BrowserTestUtils.synthesizeMouse("div", x, y,
-        { type: "mousedown", isSynthesized: false }, gBrowser.selectedBrowser);
-  x += 20; y += 10;
+        { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+  div.style.transform = "none";
+  div.style.backgroundColor = "";
+
+  info("testWithTouch: Move mouse into the div element");
+  yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousemove", isSynthesized: false },
+        gBrowser.selectedBrowser);
+  isnot(div.style.backgroundColor, "red", "mouseenter or mouseover should not work");
+
+  info("testWithTouch: Drag the div element");
+  yield BrowserTestUtils.synthesizeMouseAtCenter("div", { type: "mousedown", isSynthesized: false },
+        gBrowser.selectedBrowser);
+  x = 100; y = 100;
+  yield BrowserTestUtils.synthesizeMouse("div", x, y,
+        { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
+  isnot(div.style.transform, "none", "touchmove should work");
+  yield BrowserTestUtils.synthesizeMouse("div", x, y,
+        { type: "mouseup", isSynthesized: false }, gBrowser.selectedBrowser);
+
+  info("testWithTouch: Move mouse out of the div element");
+  x = -1; y = -1;
   yield BrowserTestUtils.synthesizeMouse("div", x, y,
         { type: "mousemove", isSynthesized: false }, gBrowser.selectedBrowser);
-  is(div.style.transform, "translate(20px, 10px)", "touch should work");
-  yield BrowserTestUtils.synthesizeMouse("div", x, y,
-        { type: "mouseup", isSynthesized: false }, gBrowser.selectedBrowser);
-  is(div.style.transform, "none", "end event should work");
+  isnot(div.style.backgroundColor, "blue", "mouseout or mouseleave should not work");
+
+  yield testWithMetaViewportEnabled();
+  yield testWithMetaViewportDisabled();
 }
+
+function* testWithMetaViewportEnabled() {
+  yield pushPrefs([domViewportEnabled, true]);
+  let meta = content.document.querySelector("meta[name=viewport]");
+  let div = content.document.querySelector("div");
+  div.dataset.isDelay = "false";
+
+  info("testWithMetaViewportEnabled: click the div element with <meta name='viewport'>");
+  meta.content = "";
+  yield synthesizeClick(div);
+  is(div.dataset.isDelay, "true", "300ms delay between touch events and mouse events should work");
+
+  info("testWithMetaViewportEnabled: click the div element with <meta name='viewport' content='user-scalable=no'>");
+  meta.content = "user-scalable=no";
+  yield synthesizeClick(div);
+  is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work");
+
+  info("testWithMetaViewportEnabled: click the div element with <meta name='viewport' content='minimum-scale=maximum-scale'>");
+  meta.content = "minimum-scale=maximum-scale";
+  yield synthesizeClick(div);
+  is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work");
+
+  info("testWithMetaViewportEnabled: click the div element with <meta name='viewport' content='width=device-width'>");
+  meta.content = "width=device-width";
+  yield synthesizeClick(div);
+  is(div.dataset.isDelay, "false", "300ms delay between touch events and mouse events should not work");
+}
+
+function* testWithMetaViewportDisabled() {
+  yield pushPrefs([domViewportEnabled, false]);
+  let meta = content.document.querySelector("meta[name=viewport]");
+  let div = content.document.querySelector("div");
+  div.dataset.isDelay = "false";
+
+  info("testWithMetaViewportDisabled: click the div element with <meta name='viewport'>");
+  meta.content = "";
+  yield synthesizeClick(div);
+  is(div.dataset.isDelay, "true", "300ms delay between touch events and mouse events should work");
+}
+
+function synthesizeClick(element) {
+  let waitForClickEvent = BrowserTestUtils.waitForEvent(element, "click");
+  BrowserTestUtils.synthesizeMouseAtCenter(element, { type: "mousedown", isSynthesized: false },
+        gBrowser.selectedBrowser);
+  BrowserTestUtils.synthesizeMouseAtCenter(element, { type: "mouseup", isSynthesized: false },
+        gBrowser.selectedBrowser);
+  return waitForClickEvent;
+}
+
+function pushPrefs(...aPrefs) {
+  let deferred = promise.defer();
+  SpecialPowers.pushPrefEnv({"set": aPrefs}, deferred.resolve);
+  return deferred.promise;
+}
--- a/devtools/client/responsivedesign/test/touch.html
+++ b/devtools/client/responsivedesign/test/touch.html
@@ -1,40 +1,85 @@
 <!DOCTYPE html>
 
-<meta charset=utf-8 />
+<meta charset="utf-8" />
+<meta name="viewport" />
 <title>test</title>
 
 
 <style>
   div {
     border:1px solid red;
     width: 100px; height: 100px;
   }
 </style>
 
-<div></div>
+<div data-is-delay="false"></div>
 
 <script>
   var div = document.querySelector("div");
   var initX, initY;
+  var previousEvent = "", touchendTime = 0;
+  var updatePreviousEvent = function(e){
+    previousEvent = e.type;
+  };
 
   div.style.transform = "none";
+  div.style.backgroundColor = "";
 
   div.addEventListener("touchstart", function(evt) {
     var touch = evt.changedTouches[0];
     initX = touch.pageX;
     initY = touch.pageY;
+    updatePreviousEvent(evt);
   }, true);
 
   div.addEventListener("touchmove", function(evt) {
     var touch = evt.changedTouches[0];
     var deltaX = touch.pageX - initX;
     var deltaY = touch.pageY - initY;
     div.style.transform = "translate(" + deltaX + "px, " + deltaY + "px)";
+    updatePreviousEvent(evt);
   }, true);
 
   div.addEventListener("touchend", function(evt) {
     if (!evt.touches.length) {
       div.style.transform = "none";
     }
+    touchendTime = new Date().getTime();
+    updatePreviousEvent(evt);
   }, true);
+
+  div.addEventListener("mouseenter", function(evt) {
+    div.style.backgroundColor = "red";
+    updatePreviousEvent(evt);
+  }, true);
+  div.addEventListener("mouseover", function(evt) {
+    div.style.backgroundColor = "red";
+    updatePreviousEvent(evt);
+  }, true);
+
+  div.addEventListener("mouseout", function(evt) {
+    div.style.backgroundColor = "blue";
+    updatePreviousEvent(evt);
+  }, true);
+
+  div.addEventListener("mouseleave", function(evt) {
+    div.style.backgroundColor = "blue";
+    updatePreviousEvent(evt);
+  }, true);
+
+  div.addEventListener("mousedown", function(evt){
+    if (previousEvent === "touchend" && touchendTime !== 0) {
+      let now = new Date().getTime();
+      div.dataset.isDelay = ((now - touchendTime) >= 300) ? true : false;
+    } else {
+      div.dataset.isDelay = false;
+    }
+    updatePreviousEvent(evt);
+  }, true);
+
+  div.addEventListener("mousemove", updatePreviousEvent, true);
+
+  div.addEventListener("mouseup", updatePreviousEvent, true);
+
+  div.addEventListener("click", updatePreviousEvent, true);
 </script>
--- a/devtools/shared/touch/simulator-content.js
+++ b/devtools/shared/touch/simulator-content.js
@@ -41,16 +41,20 @@ try {
  */
 var simulator = {
   events: [
     "mousedown",
     "mousemove",
     "mouseup",
     "touchstart",
     "touchend",
+    "mouseenter",
+    "mouseover",
+    "mouseout",
+    "mouseleave"
   ],
 
   messages: [
     "TouchEventSimulator:Start",
     "TouchEventSimulator:Stop",
   ],
 
   contextMenuTimeout: null,
@@ -133,16 +137,24 @@ var simulator = {
         evt.mozInputSource != Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE ||
         evt.isSynthesized) {
       return;
     }
 
     let eventTarget = this.target;
     let type = "";
     switch (evt.type) {
+      case "mouseenter":
+      case "mouseover":
+      case "mouseout":
+      case "mouseleave":
+        // Don't propagate events which are not related to touch events
+        evt.stopPropagation();
+        break;
+
       case "mousedown":
         this.target = evt.target;
 
         this.contextMenuTimeout =
           this.sendContextMenu(evt.target, evt.pageX, evt.pageY);
 
         this.cancelClick = false;
         this.startX = evt.pageX;
@@ -152,16 +164,18 @@ var simulator = {
         // won't be dispatched to something else.
         evt.target.setCapture(false);
 
         type = "touchstart";
         break;
 
       case "mousemove":
         if (!eventTarget) {
+          // Don't propagate mousemove event when touchstart event isn't fired
+          evt.stopPropagation();
           return;
         }
 
         if (!this.cancelClick) {
           if (Math.abs(this.startX - evt.pageX) > threshold ||
               Math.abs(this.startY - evt.pageY) > threshold) {
             this.cancelClick = true;
             content.clearTimeout(this.contextMenuTimeout);
@@ -203,17 +217,17 @@ var simulator = {
         content.setTimeout(function dispatchMouseEvents(self) {
           try {
             self.fireMouseEvent("mousedown", evt);
             self.fireMouseEvent("mousemove", evt);
             self.fireMouseEvent("mouseup", evt);
           } catch (e) {
             console.error("Exception in touch event helper: " + e);
           }
-        }, 0, this);
+        }, this.getDelayBeforeMouseEvent(evt), this);
         return;
     }
 
     let target = eventTarget || this.target;
     if (target && type) {
       this.sendTouchEvent(evt, target, type);
     }
 
@@ -300,12 +314,59 @@ var simulator = {
     target.dispatchEvent(touchEvent);
   },
 
   getContent(target) {
     let win = (target && target.ownerDocument)
       ? target.ownerDocument.defaultView
       : null;
     return win;
+  },
+
+  getDelayBeforeMouseEvent(evt) {
+    // On mobile platforms, Firefox inserts a 300ms delay between
+    // touch events and accompanying mouse events, except if the
+    // content window is not zoomable and the content window is
+    // auto-zoomed to device-width.
+
+    // If the preference dom.meta-viewport.enabled is set to false,
+    // we couldn't read viewport's information from getViewportInfo().
+    // So we always simulate 300ms delay when the
+    // dom.meta-viewport.enabled is false.
+    let savedMetaViewportEnabled =
+      Services.prefs.getBoolPref("dom.meta-viewport.enabled");
+    if (!savedMetaViewportEnabled) {
+      return 300;
+    }
+
+    let content = this.getContent(evt.target);
+    if (!content) {
+      return 0;
+    }
+
+    let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
+                       .getInterface(Ci.nsIDOMWindowUtils);
+
+    let allowZoom = {},
+        minZoom = {},
+        maxZoom = {},
+        autoSize = {};
+
+    utils.getViewportInfo(content.innerWidth, content.innerHeight, {},
+                          allowZoom, minZoom, maxZoom, {}, {}, autoSize);
+
+    // FIXME: On Safari and Chrome mobile platform, if the css property
+    // touch-action set to none or manipulation would also suppress 300ms
+    // delay. But Firefox didn't support this property now, we can't get
+    // this value from utils.getVisitedDependentComputedStyle() to check
+    // if we should suppress 300ms delay.
+    if (!allowZoom.value ||                   // user-scalable = no
+        minZoom.value === maxZoom.value ||    // minimum-scale = maximum-scale
+        autoSize.value                        // width = device-width
+    ) {
+      return 0;
+    } else {
+      return 300;
+    }
   }
 };
 
 simulator.init();
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -77,16 +77,17 @@ import org.mozilla.gecko.tabqueue.TabQue
 import org.mozilla.gecko.tabs.TabHistoryController;
 import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
 import org.mozilla.gecko.tabs.TabHistoryFragment;
 import org.mozilla.gecko.tabs.TabHistoryPage;
 import org.mozilla.gecko.tabs.TabsPanel;
 import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
 import org.mozilla.gecko.telemetry.TelemetryDispatcher;
 import org.mozilla.gecko.telemetry.UploadTelemetryCorePingCallback;
+import org.mozilla.gecko.telemetry.measurements.SessionMeasurements;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.ToolbarProgressView;
 import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
 import org.mozilla.gecko.updater.UpdateServiceHelper;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.Clipboard;
@@ -314,16 +315,17 @@ public class BrowserApp extends GeckoApp
             (BrowserAppDelegate) new ReaderViewBookmarkPromotion(),
             (BrowserAppDelegate) new ContentNotificationsDelegate()
     ));
 
     @NonNull
     private SearchEngineManager searchEngineManager; // Contains reference to Context - DO NOT LEAK!
 
     private TelemetryDispatcher mTelemetryDispatcher;
+    private final SessionMeasurements mSessionMeasurements = new SessionMeasurements();
 
     private boolean mHasResumed;
 
     @Override
     public View onCreateView(final String name, final Context context, final AttributeSet attrs) {
         final View view;
         if (BrowserToolbar.class.getName().equals(name)) {
             view = BrowserToolbar.create(context, attrs);
@@ -996,16 +998,17 @@ public class BrowserApp extends GeckoApp
     public void onResume() {
         super.onResume();
         if (mIsAbortingAppLaunch) {
             return;
         }
 
         // Needed for Adjust to get accurate session measurements
         AdjustConstants.getAdjustHelper().onResume();
+        mSessionMeasurements.recordSessionStart();
 
         if (!mHasResumed) {
             EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
                     "Prompt:ShowTop");
             mHasResumed = true;
         }
 
         processTabQueue();
@@ -1020,16 +1023,20 @@ public class BrowserApp extends GeckoApp
         super.onPause();
         if (mIsAbortingAppLaunch) {
             return;
         }
 
         // Needed for Adjust to get accurate session measurements
         AdjustConstants.getAdjustHelper().onPause();
 
+        // onStart/onStop is ideal over onResume/onPause. However, onStop is not guaranteed to be called and
+        // dealing with that possibility adds a lot of complexity that we don't want to handle at this point.
+        mSessionMeasurements.recordSessionEnd(this);
+
         if (mHasResumed) {
             // Register for Prompt:ShowTop so we can foreground this activity even if it's hidden.
             EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this,
                 "Prompt:ShowTop");
             mHasResumed = false;
         }
 
         for (BrowserAppDelegate delegate : delegates) {
@@ -3891,33 +3898,41 @@ public class BrowserApp extends GeckoApp
     @Override
     public void onSearch(SearchEngine engine, final String text, final TelemetryContract.Method method) {
         // Don't store searches that happen in private tabs. This assumes the user can only
         // perform a search inside the currently selected tab, which is true for searches
         // that come from SearchEngineRow.
         if (!Tabs.getInstance().getSelectedTab().isPrivate()) {
             storeSearchQuery(text);
         }
-        recordSearch(GeckoSharedPrefs.forProfile(this), engine.getEngineIdentifier(), method);
+
+        // We don't use SearchEngine.getEngineIdentifier because it can
+        // return a custom search engine name, which is a privacy concern.
+        final String identifierToRecord = (engine.identifier != null) ? engine.identifier : "other";
+        recordSearch(GeckoSharedPrefs.forProfile(this), identifierToRecord, method);
         openUrlAndStopEditing(text, engine.name);
     }
 
     // BrowserSearch.OnEditSuggestionListener
     @Override
     public void onEditSuggestion(String suggestion) {
         mBrowserToolbar.onEditSuggestion(suggestion);
     }
 
     @Override
     public int getLayout() { return R.layout.gecko_app; }
 
     public TelemetryDispatcher getTelemetryDispatcher() {
         return mTelemetryDispatcher;
     }
 
+    public SessionMeasurements getSessionMeasurementDelegate() {
+        return mSessionMeasurements;
+    }
+
     // For use from tests only.
     @RobocopTarget
     public ReadingListHelper getReadingListHelper() {
         return mReadingListHelper;
     }
 
     /**
      * Launch UI that lets the user update Firefox.
--- a/mobile/android/base/java/org/mozilla/gecko/Telemetry.java
+++ b/mobile/android/base/java/org/mozilla/gecko/Telemetry.java
@@ -164,21 +164,21 @@ public class Telemetry {
      * @param method A non-null method (if null is desired, consider using Method.NONE)
      */
     private static void sendUIEvent(final String eventName, final Method method,
             final long timestamp, final String extras) {
         if (method == null) {
             throw new IllegalArgumentException("Expected non-null method - use Method.NONE?");
         }
 
-        String logString = "SendUIEvent: event = " + eventName + " method = " + method + " timestamp = " + timestamp;
-        if (!AppConstants.MOZILLA_OFFICIAL) {
-            logString += " extras = " + extras;
+        if (!AppConstants.RELEASE_BUILD) {
+            final String logString = "SendUIEvent: event = " + eventName + " method = " + method + " timestamp = " +
+                    timestamp + " extras = " + extras;
+            Log.d(LOGTAG, logString);
         }
-        Log.d(LOGTAG, logString);
         final GeckoEvent geckoEvent = GeckoEvent.createTelemetryUIEvent(
                 eventName, method.toString(), timestamp, extras);
         GeckoAppShell.sendEventToGecko(geckoEvent);
     }
 
     public static void sendUIEvent(final Event event, final Method method, final long timestamp,
             final String extras) {
         sendUIEvent(event.toString(), method, timestamp, extras);
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/UploadTelemetryCorePingCallback.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/UploadTelemetryCorePingCallback.java
@@ -12,16 +12,17 @@ import android.support.annotation.Worker
 import android.util.Log;
 import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.distribution.DistributionStoreCallback;
 import org.mozilla.gecko.search.SearchEngineManager;
 import org.mozilla.gecko.sync.ExtendedJSONObject;
 import org.mozilla.gecko.telemetry.measurements.SearchCountMeasurements;
+import org.mozilla.gecko.telemetry.measurements.SessionMeasurements.SessionMeasurementsContainer;
 import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import java.io.IOException;
 import java.lang.ref.WeakReference;
 
 /**
@@ -69,21 +70,25 @@ public class UploadTelemetryCorePingCall
                     clientID = profile.getClientId();
                 } catch (final IOException e) {
                     Log.w(LOGTAG, "Unable to get client ID to generate core ping: " + e);
                     return;
                 }
 
                 // Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile.
                 final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfileName(activity, profile.getName());
+                final SessionMeasurementsContainer sessionMeasurementsContainer =
+                        activity.getSessionMeasurementDelegate().getAndResetSessionMeasurements(activity);
                 final TelemetryCorePingBuilder pingBuilder = new TelemetryCorePingBuilder(activity)
                         .setClientID(clientID)
                         .setDefaultSearchEngine(TelemetryCorePingBuilder.getEngineIdentifier(engine))
                         .setProfileCreationDate(TelemetryCorePingBuilder.getProfileCreationDate(activity, profile))
-                        .setSequenceNumber(TelemetryCorePingBuilder.getAndIncrementSequenceNumber(sharedPrefs));
+                        .setSequenceNumber(TelemetryCorePingBuilder.getAndIncrementSequenceNumber(sharedPrefs))
+                        .setSessionCount(sessionMeasurementsContainer.sessionCount)
+                        .setSessionDuration(sessionMeasurementsContainer.elapsedSeconds);
                 maybeSetOptionalMeasurements(sharedPrefs, pingBuilder);
 
                 activity.getTelemetryDispatcher().queuePingForUpload(activity, pingBuilder);
             }
         });
     }
 
     private static void maybeSetOptionalMeasurements(final SharedPreferences sharedPrefs, final TelemetryCorePingBuilder pingBuilder) {
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/measurements/SessionMeasurements.java
@@ -0,0 +1,99 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.telemetry.measurements;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.support.annotation.UiThread;
+import android.support.annotation.VisibleForTesting;
+import org.mozilla.gecko.GeckoSharedPrefs;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A class to measure the number of user sessions & their durations. It was created for use with the
+ * telemetry core ping. A session is the time between {@link #recordSessionStart()} and
+ * {@link #recordSessionEnd(Context)}.
+ *
+ * This class is thread-safe, provided the thread annotations are followed. Under the hood, this class uses
+ * SharedPreferences & because there is no atomic getAndSet operation, we synchronize access to it.
+ */
+public class SessionMeasurements {
+    @VisibleForTesting static final String PREF_SESSION_COUNT = "measurements-session-count";
+    @VisibleForTesting static final String PREF_SESSION_DURATION = "measurements-session-duration";
+
+    private boolean sessionStarted = false;
+    private long timeAtSessionStartNano = -1;
+
+    @UiThread // we assume this will be called on the same thread as session end so we don't have to synchronize sessionStarted.
+    public void recordSessionStart() {
+        if (sessionStarted) {
+            throw new IllegalStateException("Trying to start session but it is already started");
+        }
+        sessionStarted = true;
+        timeAtSessionStartNano = getSystemTimeNano();
+    }
+
+    @UiThread // we assume this will be called on the same thread as session start so we don't have to synchronize sessionStarted.
+    public void recordSessionEnd(final Context context) {
+        if (!sessionStarted) {
+            throw new IllegalStateException("Expected session to be started before session end is called");
+        }
+        sessionStarted = false;
+
+        final long sessionElapsedSeconds = TimeUnit.NANOSECONDS.toSeconds(getSystemTimeNano() - timeAtSessionStartNano);
+        final SharedPreferences sharedPrefs = getSharedPreferences(context);
+        synchronized (this) {
+            final int sessionCount = sharedPrefs.getInt(PREF_SESSION_COUNT, 0);
+            final long totalElapsedSeconds = sharedPrefs.getLong(PREF_SESSION_DURATION, 0);
+            sharedPrefs.edit()
+                    .putInt(PREF_SESSION_COUNT, sessionCount + 1)
+                    .putLong(PREF_SESSION_DURATION, totalElapsedSeconds + sessionElapsedSeconds)
+                    .apply();
+        }
+    }
+
+    /**
+     * Gets the session measurements since the last time the measurements were last retrieved.
+     */
+    public synchronized SessionMeasurementsContainer getAndResetSessionMeasurements(final Context context) {
+        final SharedPreferences sharedPrefs = getSharedPreferences(context);
+        final int sessionCount = sharedPrefs.getInt(PREF_SESSION_COUNT, 0);
+        final long totalElapsedSeconds = sharedPrefs.getLong(PREF_SESSION_DURATION, 0);
+        sharedPrefs.edit()
+                .putInt(PREF_SESSION_COUNT, 0)
+                .putLong(PREF_SESSION_DURATION, 0)
+                .apply();
+        return new SessionMeasurementsContainer(sessionCount, totalElapsedSeconds);
+    }
+
+    @VisibleForTesting SharedPreferences getSharedPreferences(final Context context) {
+        return GeckoSharedPrefs.forProfile(context);
+    }
+
+    /**
+     * Returns (roughly) the system uptime in nanoseconds. A less coupled implementation would
+     * take this value from the caller of recordSession*, however, we do this internally to ensure
+     * the caller uses both a time system consistent between the start & end calls and uses the
+     * appropriate time system (i.e. not wall time, which can change when the clock is changed).
+     */
+    @VisibleForTesting long getSystemTimeNano() { // TODO: necessary?
+        return System.nanoTime();
+    }
+
+    public static final class SessionMeasurementsContainer {
+        /** The number of sessions. */
+        public final int sessionCount;
+        /** The number of seconds elapsed in ALL sessions included in {@link #sessionCount}. */
+        public final long elapsedSeconds;
+
+        private SessionMeasurementsContainer(final int sessionCount, final long elapsedSeconds) {
+            this.sessionCount = sessionCount;
+            this.elapsedSeconds = elapsedSeconds;
+        }
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/pingbuilders/TelemetryCorePingBuilder.java
@@ -37,32 +37,34 @@ import java.util.concurrent.TimeUnit;
  *
  * See https://gecko.readthedocs.org/en/latest/toolkit/components/telemetry/telemetry/core-ping.html
  * for details on the core ping.
  */
 public class TelemetryCorePingBuilder extends TelemetryPingBuilder {
     private static final String LOGTAG = StringUtils.safeSubstring(TelemetryCorePingBuilder.class.getSimpleName(), 0, 23);
 
     private static final String NAME = "core";
-    private static final int VERSION_VALUE = 6; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
+    private static final int VERSION_VALUE = 7; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
     private static final String OS_VALUE = "Android";
 
     private static final String ARCHITECTURE = "arch";
     private static final String CLIENT_ID = "clientId";
     private static final String DEFAULT_SEARCH_ENGINE = "defaultSearch";
     private static final String DEVICE = "device";
     private static final String DISTRIBUTION_ID = "distributionId";
     private static final String EXPERIMENTS = "experiments";
     private static final String LOCALE = "locale";
     private static final String OS_ATTR = "os";
     private static final String OS_VERSION = "osversion";
     private static final String PING_CREATION_DATE = "created";
     private static final String PROFILE_CREATION_DATE = "profileDate";
     private static final String SEARCH_COUNTS = "searches";
     private static final String SEQ = "seq";
+    private static final String SESSION_COUNT = "sessions";
+    private static final String SESSION_DURATION = "durations";
     private static final String TIMEZONE_OFFSET = "tz";
     private static final String VERSION_ATTR = "v";
 
     public TelemetryCorePingBuilder(final Context context) {
         initPayloadConstants(context);
     }
 
     private void initPayloadConstants(final Context context) {
@@ -170,16 +172,36 @@ public class TelemetryCorePingBuilder ex
             // Since this is an increasing value, it's possible we can overflow into negative values and get into a
             // crash loop so we don't crash on invalid arg - we can investigate if we see negative values on the server.
             Log.w(LOGTAG, "Expected positive sequence number. Received: " + seq);
         }
         payload.put(SEQ, seq);
         return this;
     }
 
+    public TelemetryCorePingBuilder setSessionCount(final int sessionCount) {
+        if (sessionCount < 0) {
+            // Since this is an increasing value, it's possible we can overflow into negative values and get into a
+            // crash loop so we don't crash on invalid arg - we can investigate if we see negative values on the server.
+            Log.w(LOGTAG, "Expected positive session count. Received: " + sessionCount);
+        }
+        payload.put(SESSION_COUNT, sessionCount);
+        return this;
+    }
+
+    public TelemetryCorePingBuilder setSessionDuration(final long sessionDuration) {
+        if (sessionDuration < 0) {
+            // Since this is an increasing value, it's possible we can overflow into negative values and get into a
+            // crash loop so we don't crash on invalid arg - we can investigate if we see negative values on the server.
+            Log.w(LOGTAG, "Expected positive session duration. Received: " + sessionDuration);
+        }
+        payload.put(SESSION_DURATION, sessionDuration);
+        return this;
+    }
+
     /**
      * Gets the sequence number from shared preferences and increments it in the prefs. This method
      * is not thread safe.
      */
     @WorkerThread // synchronous shared prefs write.
     public static int getAndIncrementSequenceNumber(final SharedPreferences sharedPrefsForProfile) {
         final int seq = sharedPrefsForProfile.getInt(TelemetryConstants.PREF_SEQ_COUNT, 1);
 
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -575,16 +575,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'tabs/TabsGridLayout.java',
     'tabs/TabsLayoutAdapter.java',
     'tabs/TabsLayoutItemView.java',
     'tabs/TabsListLayout.java',
     'tabs/TabsPanel.java',
     'tabs/TabsPanelThumbnailView.java',
     'Telemetry.java',
     'telemetry/measurements/SearchCountMeasurements.java',
+    'telemetry/measurements/SessionMeasurements.java',
     'telemetry/pingbuilders/TelemetryCorePingBuilder.java',
     'telemetry/pingbuilders/TelemetryPingBuilder.java',
     'telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java',
     'telemetry/schedulers/TelemetryUploadScheduler.java',
     'telemetry/stores/TelemetryJSONFilePingStore.java',
     'telemetry/stores/TelemetryPingStore.java',
     'telemetry/TelemetryConstants.java',
     'telemetry/TelemetryDispatcher.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/telemetry/measurements/TestSessionMeasurements.java
@@ -0,0 +1,124 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.telemetry.measurements;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.telemetry.measurements.SessionMeasurements.SessionMeasurementsContainer;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * Tests the session measurements class.
+ */
+@RunWith(TestRunner.class)
+public class TestSessionMeasurements {
+
+    private SessionMeasurements testMeasurements;
+    private SharedPreferences sharedPrefs;
+    private Context context;
+
+    @Before
+    public void setUp() throws Exception {
+        testMeasurements = spy(SessionMeasurements.class);
+        sharedPrefs = RuntimeEnvironment.application.getSharedPreferences(
+                TestSessionMeasurements.class.getSimpleName(), Context.MODE_PRIVATE);
+        doReturn(sharedPrefs).when(testMeasurements).getSharedPreferences(any(Context.class));
+
+        context = RuntimeEnvironment.application;
+    }
+
+    private void assertSessionCount(final String postfix, final int expectedSessionCount) {
+        final int actual = sharedPrefs.getInt(SessionMeasurements.PREF_SESSION_COUNT, -1);
+        assertEquals("Expected number of sessions occurred " + postfix, expectedSessionCount, actual);
+    }
+
+    private void assertSessionDuration(final String postfix, final long expectedSessionDuration) {
+        final long actual = sharedPrefs.getLong(SessionMeasurements.PREF_SESSION_DURATION, -1);
+        assertEquals("Expected session duration received " + postfix, expectedSessionDuration, actual);
+    }
+
+    private void mockGetSystemTimeNanosToReturn(final long value) {
+        doReturn(value).when(testMeasurements).getSystemTimeNano();
+    }
+
+    @Test
+    public void testRecordSessionStartAndEndCalledOnce() throws Exception {
+        final long expectedElapsedSeconds = 4;
+        mockGetSystemTimeNanosToReturn(0);
+        testMeasurements.recordSessionStart();
+        mockGetSystemTimeNanosToReturn(TimeUnit.SECONDS.toNanos(expectedElapsedSeconds));
+        testMeasurements.recordSessionEnd(context);
+
+        final String postfix = "after recordSessionStart/End called once";
+        assertSessionCount(postfix, 1);
+        assertSessionDuration(postfix, expectedElapsedSeconds);
+    }
+
+    @Test
+    public void testRecordSessionStartAndEndCalledTwice() throws Exception {
+        final long expectedElapsedSeconds = 100;
+        mockGetSystemTimeNanosToReturn(0L);
+        for (int i = 1; i <= 2; ++i) {
+            testMeasurements.recordSessionStart();
+            mockGetSystemTimeNanosToReturn(TimeUnit.SECONDS.toNanos((expectedElapsedSeconds / 2) * i));
+            testMeasurements.recordSessionEnd(context);
+        }
+
+        final String postfix = "after recordSessionStart/End called twice";
+        assertSessionCount(postfix, 2);
+        assertSessionDuration(postfix, expectedElapsedSeconds);
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testRecordSessionStartThrowsIfSessionAlreadyStarted() throws Exception {
+        // First call will start the session, next expected to throw.
+        for (int i = 0; i < 2; ++i) {
+            testMeasurements.recordSessionStart();
+        }
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testRecordSessionEndThrowsIfCalledBeforeSessionStarted() {
+        testMeasurements.recordSessionEnd(context);
+    }
+
+    @Test // assumes the underlying format in SessionMeasurements
+    public void testGetAndResetSessionMeasurementsReturnsSetData() throws Exception {
+        final int expectedSessionCount = 42;
+        final long expectedSessionDuration = 1234567890;
+        sharedPrefs.edit()
+                .putInt(SessionMeasurements.PREF_SESSION_COUNT, expectedSessionCount)
+                .putLong(SessionMeasurements.PREF_SESSION_DURATION, expectedSessionDuration)
+                .apply();
+
+        final SessionMeasurementsContainer actual = testMeasurements.getAndResetSessionMeasurements(context);
+        assertEquals("Returned session count matches expected", expectedSessionCount, actual.sessionCount);
+        assertEquals("Returned session duration matches expected", expectedSessionDuration, actual.elapsedSeconds);
+    }
+
+    @Test
+    public void testGetAndResetSessionMeasurementsResetsData() throws Exception {
+        sharedPrefs.edit()
+                .putInt(SessionMeasurements.PREF_SESSION_COUNT, 10)
+                .putLong(SessionMeasurements.PREF_SESSION_DURATION, 10)
+                .apply();
+
+        testMeasurements.getAndResetSessionMeasurements(context);
+        final String postfix = "is reset after retrieval";
+        assertSessionCount(postfix, 0);
+        assertSessionDuration(postfix, 0);
+    }
+}
\ No newline at end of file
--- a/services/fxaccounts/FxAccounts.jsm
+++ b/services/fxaccounts/FxAccounts.jsm
@@ -41,16 +41,17 @@ var publicProperties = [
   "getAccountsSignInURI",
   "getAccountsSignUpURI",
   "getAssertion",
   "getDeviceId",
   "getKeys",
   "getSignedInUser",
   "getOAuthToken",
   "getSignedInUserProfile",
+  "invalidateCertificate",
   "loadAndPoll",
   "localtimeOffsetMsec",
   "now",
   "promiseAccountsForceSigninURI",
   "promiseAccountsChangeProfileURI",
   "promiseAccountsManageURI",
   "removeCachedOAuthToken",
   "resendVerificationEmail",
@@ -593,16 +594,24 @@ FxAccountsInternal.prototype = {
           return this.getAssertionFromCert(data, keyPair, certificate, audience);
         }
       );
     }).catch(err =>
       this._handleTokenError(err)
     ).then(result => currentState.resolve(result));
   },
 
+  /**
+   * Invalidate the FxA certificate, so that it will be refreshed from the server
+   * the next time it is needed.
+   */
+  invalidateCertificate() {
+    return this.currentAccountState.updateUserAccountData({ cert: null });
+  },
+
   getDeviceId() {
     return this.currentAccountState.getUserAccountData()
       .then(data => {
         if (data) {
           if (!data.deviceId || !data.deviceRegistrationVersion ||
               data.deviceRegistrationVersion < this.DEVICE_REGISTRATION_VERSION) {
             // There is no device id or the device registration is outdated.
             // Either way, we should register the device with FxA
--- a/services/sync/modules/browserid_identity.js
+++ b/services/sync/modules/browserid_identity.js
@@ -564,17 +564,17 @@ this.BrowserIDManager.prototype = {
       return this._fxaService.getKeys().then(
         newUserData => {
           userData = newUserData;
           this._updateSignedInUser(userData); // throws if the user changed.
         }
       );
     }
 
-    let getToken = (tokenServerURI, assertion) => {
+    let getToken = assertion => {
       log.debug("Getting a token");
       let deferred = Promise.defer();
       let cb = function (err, token) {
         if (err) {
           return deferred.reject(err);
         }
         log.debug("Successfully got a sync token");
         return deferred.resolve(token);
@@ -592,17 +592,28 @@ this.BrowserIDManager.prototype = {
       return fxa.getAssertion(audience);
     };
 
     // wait until the account email is verified and we know that
     // getAssertion() will return a real assertion (not null).
     return fxa.whenVerified(this._signedInUser)
       .then(() => maybeFetchKeys())
       .then(() => getAssertion())
-      .then(assertion => getToken(tokenServerURI, assertion))
+      .then(assertion => getToken(assertion))
+      .catch(err => {
+        // If we get a 401 fetching the token it may be that our certificate
+        // needs to be regenerated.
+        if (!err.response || err.response.status !== 401) {
+          return Promise.reject(err);
+        }
+        log.warn("Token server returned 401, refreshing certificate and retrying token fetch");
+        return fxa.invalidateCertificate()
+          .then(() => getAssertion())
+          .then(assertion => getToken(assertion))
+      })
       .then(token => {
         // TODO: Make it be only 80% of the duration, so refresh the token
         // before it actually expires. This is to avoid sync storage errors
         // otherwise, we get a nasty notification bar briefly. Bug 966568.
         token.expiration = this._now() + (token.duration * 1000) * 0.80;
         if (!this._syncKeyBundle) {
           // We are given kA/kB as hex.
           this._syncKeyBundle = deriveKeyBundle(Utils.hexToBytes(userData.kB));
--- a/services/sync/tests/unit/test_browserid_identity.js
+++ b/services/sync/tests/unit/test_browserid_identity.js
@@ -11,16 +11,17 @@ Cu.import("resource://testing-common/ser
 Cu.import("resource://testing-common/services/sync/fxa_utils.js");
 Cu.import("resource://services-common/hawkclient.js");
 Cu.import("resource://gre/modules/FxAccounts.jsm");
 Cu.import("resource://gre/modules/FxAccountsClient.jsm");
 Cu.import("resource://gre/modules/FxAccountsCommon.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/constants.js");
+Cu.import("resource://services-common/tokenserverclient.js");
 
 const SECOND_MS = 1000;
 const MINUTE_MS = SECOND_MS * 60;
 const HOUR_MS = MINUTE_MS * 60;
 
 var identityConfig = makeIdentityConfig();
 var browseridManager = new BrowserIDManager();
 configureFxAccountIdentity(browseridManager, identityConfig);
@@ -431,16 +432,84 @@ add_task(function* test_getTokenErrors()
   });
   browseridManager = Service.identity;
   yield browseridManager.initializeWithCurrentIdentity();
   yield Assert.rejects(browseridManager.whenReadyToAuthenticate.promise,
                        "should reject due to non-JSON response");
   Assert.equal(Status.login, LOGIN_FAILED_NETWORK_ERROR, "login state is LOGIN_FAILED_NETWORK_ERROR");
 });
 
+add_task(function* test_refreshCertificateOn401() {
+  _("BrowserIDManager refreshes the FXA certificate after a 401.");
+  var identityConfig = makeIdentityConfig();
+  var browseridManager = new BrowserIDManager();
+  // Use the real `_getAssertion` method that calls
+  // `mockFxAClient.signCertificate`.
+  let fxaInternal = makeFxAccountsInternalMock(identityConfig);
+  delete fxaInternal._getAssertion;
+  configureFxAccountIdentity(browseridManager, identityConfig, fxaInternal);
+  browseridManager._fxaService.internal.initialize();
+
+  let getCertCount = 0;
+
+  let MockFxAccountsClient = function() {
+    FxAccountsClient.apply(this);
+  };
+  MockFxAccountsClient.prototype = {
+    __proto__: FxAccountsClient.prototype,
+    signCertificate() {
+      ++getCertCount;
+    }
+  };
+
+  let mockFxAClient = new MockFxAccountsClient();
+  browseridManager._fxaService.internal._fxAccountsClient = mockFxAClient;
+
+  let didReturn401 = false;
+  let didReturn200 = false;
+  let mockTSC = mockTokenServer(() => {
+    if (getCertCount <= 1) {
+      didReturn401 = true;
+      return {
+        status: 401,
+        headers: {"content-type": "application/json"},
+        body: JSON.stringify({}),
+      };
+    } else {
+      didReturn200 = true;
+      return {
+        status: 200,
+        headers: {"content-type": "application/json"},
+        body: JSON.stringify({
+          id:           "id",
+          key:          "key",
+          api_endpoint: "http://example.com/",
+          uid:          "uid",
+          duration:     300,
+        })
+      };
+    }
+  });
+
+  browseridManager._tokenServerClient = mockTSC;
+
+  yield browseridManager.initializeWithCurrentIdentity();
+  yield browseridManager.whenReadyToAuthenticate.promise;
+
+  do_check_eq(getCertCount, 2);
+  do_check_true(didReturn401);
+  do_check_true(didReturn200);
+  do_check_true(browseridManager.account);
+  do_check_true(browseridManager._token);
+  do_check_true(browseridManager.hasValidToken());
+  do_check_true(browseridManager.account);
+});
+
+
+
 add_task(function* test_getTokenErrorWithRetry() {
   _("tokenserver sends an observer notification on various backoff headers.");
 
   // Set Sync's backoffInterval to zero - after we simulated the backoff header
   // it should reflect the value we sent.
   Status.backoffInterval = 0;
   _("Arrange for a 503 with a Retry-After header.");
   initializeIdentityWithTokenServerResponse({
@@ -788,8 +857,34 @@ function* initializeIdentityWithHAWKResp
 function getTimestamp(hawkAuthHeader) {
   return parseInt(/ts="(\d+)"/.exec(hawkAuthHeader)[1], 10) * SECOND_MS;
 }
 
 function getTimestampDelta(hawkAuthHeader, now=Date.now()) {
   return Math.abs(getTimestamp(hawkAuthHeader) - now);
 }
 
+function mockTokenServer(func) {
+  let requestLog = Log.repository.getLogger("testing.mock-rest");
+  if (!requestLog.appenders.length) { // might as well see what it says :)
+    requestLog.addAppender(new Log.DumpAppender());
+    requestLog.level = Log.Level.Trace;
+  }
+  function MockRESTRequest(url) {};
+  MockRESTRequest.prototype = {
+    _log: requestLog,
+    setHeader: function() {},
+    get: function(callback) {
+      this.response = func();
+      callback.call(this);
+    }
+  }
+  // The mocked TokenServer client which will get the response.
+  function MockTSC() { }
+  MockTSC.prototype = new TokenServerClient();
+  MockTSC.prototype.constructor = MockTSC;
+  MockTSC.prototype.newRESTRequest = function(url) {
+    return new MockRESTRequest(url);
+  }
+  // Arrange for the same observerPrefix as browserid_identity uses.
+  MockTSC.prototype.observerPrefix = "weave:service";
+  return new MockTSC();
+}
--- a/testing/mozharness/configs/b2g/desktop_linux32.py
+++ b/testing/mozharness/configs/b2g/desktop_linux32.py
@@ -15,19 +15,16 @@ config = {
         'setup-mock',
         'build',
         'upload-files',
         'sendchange',
         'check-test',
     ],
     "buildbot_json_path": "buildprops.json",
     'exes': {
-        'hgtool.py': os.path.join(
-            os.getcwd(), 'build', 'tools', 'buildfarm', 'utils', 'hgtool.py'
-        ),
         "buildbot": "/tools/buildbot/bin/buildbot",
     },
     'app_ini_path': '%(obj_dir)s/dist/bin/application.ini',
     # mock shtuff
     'mock_mozilla_dir':  '/builds/mock_mozilla',
     'mock_target': 'mozilla-centos6-x86_64',
     'mock_files': [
         ('/home/cltbld/.ssh', '/home/mock_mozilla/.ssh'),
--- a/testing/mozharness/configs/b2g/desktop_macosx64.py
+++ b/testing/mozharness/configs/b2g/desktop_macosx64.py
@@ -12,19 +12,16 @@ config = {
         'build',
         'upload-files',
         'sendchange',
         'check-test',
     ],
     "buildbot_json_path": "buildprops.json",
     'exes': {
         'python2.7': sys.executable,
-        'hgtool.py': os.path.join(
-            os.getcwd(), 'build', 'tools', 'buildfarm', 'utils', 'hgtool.py'
-        ),
         "buildbot": "/tools/buildbot/bin/buildbot",
     },
     'app_ini_path': '%(obj_dir)s/dist/bin/application.ini',
     'enable_ccache': True,
     'vcs_share_base': '/builds/hg-shared',
     'objdir': 'obj-firefox',
     'tooltool_script': ["/builds/tooltool.py"],
     'tooltool_bootstrap': "setup.sh",
--- a/testing/mozharness/configs/b2g/desktop_windows32.py
+++ b/testing/mozharness/configs/b2g/desktop_windows32.py
@@ -16,22 +16,16 @@ config = {
         'build',
         'upload-files',
         'sendchange',
         'check-test',
     ],
     "buildbot_json_path": "buildprops.json",
     'exes': {
         'python2.7': sys.executable,
-        'hgtool.py': [
-            sys.executable,
-            os.path.join(
-                os.getcwd(), 'build', 'tools', 'buildfarm', 'utils', 'hgtool.py'
-            )
-        ],
         'gittool.py': [
             sys.executable,
             os.path.join(
                 os.getcwd(), 'build', 'tools', 'buildfarm', 'utils', 'gittool.py'
             )
         ],
         "buildbot": [
             sys.executable,
--- a/testing/mozharness/configs/b2g/releng-emulator.py
+++ b/testing/mozharness/configs/b2g/releng-emulator.py
@@ -31,18 +31,17 @@ config = {
             "ssh_user": "ffxbld",
             "upload_remote_host": "upload.ffxbld.productdelivery.prod.mozaws.net",
             "post_upload_cmd": "post_upload.py --tinderbox-builds-dir %(branch)s-%(target)s -p b2g -i %(buildid)s --revision %(revision)s --release-to-tinderbox-dated-builds",
             "post_upload_nightly_cmd": "post_upload.py --tinderbox-builds-dir %(branch)s-%(target)s -b %(branch)s-%(target)s -p b2g -i %(buildid)s --revision %(revision)s --release-to-tinderbox-dated-builds --release-to-latest --release-to-dated",
         },
     },
     "gittool_share_base": "/builds/git-shared/git",
     "gittool_base_mirror_urls": [],
-    "hgtool_share_base": "/builds/hg-shared",
-    "hgtool_base_bundle_urls": ["https://ftp-ssl.mozilla.org/pub/mozilla.org/firefox/bundles"],
+    "vcs_share_base": "/builds/hg-shared",
     "sendchange_masters": ["buildbot-master81.build.mozilla.org:9301"],
     "exes": {
         "tooltool.py": "/tools/tooltool.py",
         "buildbot": "/tools/buildbot/bin/buildbot",
     },
     "manifest": {
         "upload_remote_host": "stage.mozilla.org",
         "upload_remote_basepath": "/pub/mozilla.org/b2g/manifests/nightly/%(version)s",
--- a/testing/mozharness/configs/b2g/releng-fota-eng.py
+++ b/testing/mozharness/configs/b2g/releng-fota-eng.py
@@ -31,18 +31,17 @@ config = {
             "ssh_user": "ffxbld",
             "upload_remote_host": "upload.ffxbld.productdelivery.prod.mozaws.net",
             "post_upload_cmd": "post_upload.py --tinderbox-builds-dir %(branch)s-%(target)s -p b2g -i %(buildid)s --revision %(revision)s --release-to-tinderbox-dated-builds",
             "post_upload_nightly_cmd": "post_upload.py --tinderbox-builds-dir %(branch)s-%(target)s -b %(branch)s-%(target)s -p b2g -i %(buildid)s --revision %(revision)s --release-to-tinderbox-dated-builds --release-to-latest --release-to-dated",
         },
     },
     "gittool_share_base": "/builds/git-shared/git",
     "gittool_base_mirror_urls": [],
-    "hgtool_share_base": "/builds/hg-shared",
-    "hgtool_base_bundle_urls": ["https://ftp-ssl.mozilla.org/pub/mozilla.org/firefox/bundles"],
+    "vcs_share_base": "/builds/hg-shared",
     "exes": {
         "tooltool.py": "/tools/tooltool.py",
         "python": "/tools/python27/bin/python2.7",
     },
     "manifest": {
         "upload_remote_host": "stage.mozilla.org",
         "upload_remote_basepath": "/pub/mozilla.org/b2g/manifests/nightly/%(version)s",
         "depend_upload_remote_basepath": "/pub/mozilla.org/b2g/manifests/depend/%(branch)s/%(platform)s/%(buildid)s",
--- a/testing/mozharness/configs/b2g/releng-fota-updates.py
+++ b/testing/mozharness/configs/b2g/releng-fota-updates.py
@@ -32,18 +32,17 @@ config = {
             "ssh_user": "ffxbld",
             "upload_remote_host": "upload.ffxbld.productdelivery.prod.mozaws.net",
             "post_upload_cmd": "post_upload.py --tinderbox-builds-dir %(branch)s-%(target)s -p b2g -i %(buildid)s --revision %(revision)s --release-to-tinderbox-dated-builds",
             "post_upload_nightly_cmd": "post_upload.py --tinderbox-builds-dir %(branch)s-%(target)s -b %(branch)s-%(target)s -p b2g -i %(buildid)s --revision %(revision)s --release-to-tinderbox-dated-builds --release-to-latest --release-to-dated",
         },
     },
     "gittool_share_base": "/builds/git-shared/git",
     "gittool_base_mirror_urls": [],
-    "hgtool_share_base": "/builds/hg-shared",
-    "hgtool_base_bundle_urls": ["https://ftp-ssl.mozilla.org/pub/mozilla.org/firefox/bundles"],
+    "vcs_share_base": "/builds/hg-shared",
     "exes": {
         "tooltool.py": "/tools/tooltool.py",
     },
     "manifest": {
         "upload_remote_host": "stage.mozilla.org",
         "upload_remote_basepath": "/pub/mozilla.org/b2g/manifests/nightly/%(version)s",
         "depend_upload_remote_basepath": "/pub/mozilla.org/b2g/manifests/depend/%(branch)s/%(platform)s/%(buildid)s",
         "ssh_key": os.path.expanduser("~/.ssh/b2gbld_dsa"),
--- a/testing/mozharness/configs/b2g/releng-otoro-eng.py
+++ b/testing/mozharness/configs/b2g/releng-otoro-eng.py
@@ -31,18 +31,17 @@ config = {
             "ssh_user": "ffxbld",
             "upload_remote_host": "upload.ffxbld.productdelivery.prod.mozaws.net",
             "post_upload_cmd": "post_upload.py --tinderbox-builds-dir %(branch)s-%(target)s -p b2g -i %(buildid)s --revision %(revision)s --release-to-tinderbox-dated-builds",
             "post_upload_nightly_cmd": "post_upload.py --tinderbox-builds-dir %(branch)s-%(target)s -b %(branch)s-%(target)s -p b2g -i %(buildid)s --revision %(revision)s --release-to-tinderbox-dated-builds --release-to-latest --release-to-dated",
         },
     },
     "gittool_share_base": "/builds/git-shared/git",
     "gittool_base_mirror_urls": [],
-    "hgtool_share_base": "/builds/hg-shared",
-    "hgtool_base_bundle_urls": ["https://ftp-ssl.mozilla.org/pub/mozilla.org/firefox/bundles"],
+    "vcs_share_base": "/builds/hg-shared",
     "exes": {
         "tooltool.py": "/tools/tooltool.py",
         "python": "/tools/python27/bin/python2.7",
     },
     "manifest": {
         "upload_remote_host": "stage.mozilla.org",
         "upload_remote_basepath": "/pub/mozilla.org/b2g/manifests/nightly/%(version)s",
         "depend_upload_remote_basepath": "/pub/mozilla.org/b2g/manifests/depend/%(branch)s/%(platform)s/%(buildid)s",
--- a/testing/mozharness/configs/b2g/releng-otoro.py
+++ b/testing/mozharness/configs/b2g/releng-otoro.py
@@ -31,18 +31,17 @@ config = {
             "ssh_user": "ffxbld",
             "upload_remote_host": "upload.ffxbld.productdelivery.prod.mozaws.net",
             "post_upload_cmd": "post_upload.py --tinderbox-builds-dir %(branch)s-%(target)s -p b2g -i %(buildid)s --revision %(revision)s --release-to-tinderbox-dated-builds",
             "post_upload_nightly_cmd": "post_upload.py --tinderbox-builds-dir %(branch)s-%(target)s -b %(branch)s-%(target)s -p b2g -i %(buildid)s --revision %(revision)s --release-to-tinderbox-dated-builds --release-to-latest --release-to-dated",
         },
     },
     "gittool_share_base": "/builds/git-shared/git",
     "gittool_base_mirror_urls": [],
-    "hgtool_share_base": "/builds/hg-shared",
-    "hgtool_base_bundle_urls": ["https://ftp-ssl.mozilla.org/pub/mozilla.org/firefox/bundles"],
+    "vcs_share_base": "/builds/hg-shared",
     "exes": {
         "tooltool.py": "/tools/tooltool.py",
     },
     "manifest": {
         "upload_remote_host": "stage.mozilla.org",
         "upload_remote_basepath": "/pub/mozilla.org/b2g/manifests/nightly/%(version)s",
         "depend_upload_remote_basepath": "/pub/mozilla.org/b2g/manifests/depend/%(branch)s/%(platform)s/%(buildid)s",
         "ssh_key": os.path.expanduser("~/.ssh/b2gbld_dsa"),
--- a/testing/mozharness/configs/b2g/releng-private-updates.py
+++ b/testing/mozharness/configs/b2g/releng-private-updates.py
@@ -32,18 +32,17 @@ config = {
             "ssh_user": "ffxbld",
             "upload_remote_host": "upload.ffxbld.productdelivery.prod.mozaws.net",
             "post_upload_cmd": "post_upload.py --tinderbox-builds-dir %(branch)s-%(target)s -p b2g -i %(buildid)s --revision %(revision)s --release-to-tinderbox-dated-builds",
             "post_upload_nightly_cmd": "post_upload.py --tinderbox-builds-dir %(branch)s-%(target)s -b %(branch)s-%(target)s -p b2g -i %(buildid)s --revision %(revision)s --release-to-tinderbox-dated-builds --release-to-latest --release-to-dated",
         },
     },
     "gittool_share_base": "/builds/git-shared/git",
     "gittool_base_mirror_urls": [],
-    "hgtool_share_base": "/builds/hg-shared",
-    "hgtool_base_bundle_urls": ["https://ftp-ssl.mozilla.org/pub/mozilla.org/firefox/bundles"],
+    "vcs_share_base": "/builds/hg-shared",
     "exes": {
         "tooltool.py": "/tools/tooltool.py",
     },
     "manifest": {
         "upload_remote_host": "stage.mozilla.org",
         "upload_remote_basepath": "/pub/mozilla.org/b2g/manifests/nightly/%(version)s",
         "depend_upload_remote_basepath": "/pub/mozilla.org/b2g/manifests/depend/%(branch)s/%(platform)s/%(buildid)s",
         "ssh_key": os.path.expanduser("~/.ssh/b2gbld_dsa"),
--- a/testing/mozharness/configs/b2g/releng-try.py
+++ b/testing/mozharness/configs/b2g/releng-try.py
@@ -17,18 +17,17 @@ config = {
             "ssh_user": "b2gtry",
             "upload_remote_host": "pvtbuilds2.dmz.scl3.mozilla.com",
             "upload_remote_path": "/pub/mozilla.org/b2g/try-builds/%(user)s-%(revision)s/%(branch)s-%(target)s",
             "upload_dep_target_exclusions": [],
         },
     },
     "gittool_share_base": "/builds/git-shared/git",
     "gittool_base_mirror_urls": [],
-    "hgtool_share_base": "/builds/hg-shared",
-    "hgtool_base_bundle_urls": ["https://ftp-ssl.mozilla.org/pub/mozilla.org/firefox/bundles"],
+    "vcs_share_base": "/builds/hg-shared",
     "sendchange_masters": ["buildbot-master81.build.mozilla.org:9301"],
     "exes": {
         "tooltool.py": "/tools/tooltool.py",
         "buildbot": "/tools/buildbot/bin/buildbot",
     },
     "env": {
         "CCACHE_DIR": "/builds/ccache",
         "CCACHE_COMPRESS": "1",
--- a/testing/mozharness/scripts/b2g_build.py
+++ b/testing/mozharness/scripts/b2g_build.py
@@ -125,28 +125,28 @@ class B2GBuild(LocalesMixin, PurgeMixin,
         }],
     ]
 
     def __init__(self, require_config_file=False, config={},
                  all_actions=all_actions,
                  default_actions=default_actions):
         # Default configuration
         default_config = {
-            'default_vcs': 'hgtool',
+            'default_vcs': 'hg',
             'ccache': True,
             'locales_dir': 'gecko/b2g/locales',
             'l10n_dir': 'gecko-l10n',
             'ignore_locales': ['en-US', 'multi'],
             'locales_file': 'gecko/b2g/locales/all-locales',
             'mozilla_dir': 'build/gecko',
             'objdir': 'build/objdir-gecko',
             'merge_locales': True,
             'compare_locales_repo': 'https://hg.mozilla.org/build/compare-locales',
             'compare_locales_rev': 'RELEASE_AUTOMATION',
-            'compare_locales_vcs': 'hgtool',
+            'compare_locales_vcs': 'hg',
             'repo_remote_mappings': {},
             'influx_credentials_file': 'oauth.txt',
             'balrog_credentials_file': 'oauth.txt',
             'build_resources_path': '%(abs_obj_dir)s/.mozbuild/build_resources.json',
             'virtualenv_modules': [
                 'requests==2.8.1',
             ],
             'virtualenv_path': 'venv',
--- a/toolkit/components/telemetry/docs/core-ping.rst
+++ b/toolkit/components/telemetry/docs/core-ping.rst
@@ -20,17 +20,17 @@ Submission will be per the Edge server s
 * ``appBuildID`` is the build number
 
 Note: Counts below (e.g. search & usage times) are “since the last
 ping”, not total for the whole application lifetime.
 
 Structure::
 
     {
-      "v": 5, // ping format version
+      "v": 6, // ping format version
       "clientId": <string>, // client id, e.g.
                             // "c641eacf-c30c-4171-b403-f077724e848a"
       "seq": <positive integer>, // running ping counter, e.g. 3
       "locale": <string>, // application locale, e.g. "en-US"
       "os": <string>, // OS name.
       "osversion": <string>, // OS version.
       "device": <string>, // Build.MANUFACTURER + " - " + Build.MODEL
                           // where manufacturer is truncated to 12 characters
@@ -40,16 +40,19 @@ Structure::
                                     // UNIX epoch.
       "defaultSearch": <string>, // Identifier of the default search engine,
                                  // e.g. "yahoo".
       "distributionId": <string>, // Distribution identifier (optional)
       "created": <string>, // date the ping was created
                            // in local time, "yyyy-mm-dd"
       "tz": <integer>, // timezone offset (in minutes) of the
                        // device when the ping was created
+      "searches": <object>, // Optional, object of search use counts in the
+                            // format: { "engine.source": <pos integer> }
+                            // e.g.: { "yahoo.searchbar": 3, "other.searchbar": 1 }
       "experiments": [<string>, …], // Optional, array of identifiers
                                     // for the active experiments
     }
 
 Field details
 -------------
 
 device
@@ -118,16 +121,17 @@ HTTP "Date" header
 This header is used to track the submission date of the core ping in the format
 specified by
 `rfc 2616 sec 14.18 <https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18>`_,
 et al (e.g. "Tue, 01 Feb 2011 14:00:00 GMT").
 
 
 Version history
 ---------------
+* v6: added ``searches``
 * v5: added ``created`` & ``tz``
 * v4: ``profileDate`` will return package install time when times.json is not available
 * v3: added ``defaultSearch``
 * v2: added ``distributionId``
 * v1: initial version
 
 Notes
 ~~~~~
--- a/toolkit/content/tests/browser/browser.ini
+++ b/toolkit/content/tests/browser/browser.ini
@@ -7,17 +7,16 @@ support-files =
 [browser_bug295977_autoscroll_overflow.js]
 [browser_bug451286.js]
 [browser_bug594509.js]
 [browser_bug982298.js]
 [browser_bug1198465.js]
 [browser_contentTitle.js]
 [browser_default_image_filename.js]
 [browser_f7_caret_browsing.js]
-skip-if = e10s
 [browser_findbar.js]
 [browser_label_textlink.js]
 [browser_isSynthetic.js]
 support-files =
   empty.png
 [browser_keyevents_during_autoscrolling.js]
 [browser_save_resend_postdata.js]
 support-files =
--- a/toolkit/content/tests/browser/browser_f7_caret_browsing.js
+++ b/toolkit/content/tests/browser/browser_f7_caret_browsing.js
@@ -1,14 +1,8 @@
-XPCOMUtils.defineLazyModuleGetter(this, "Promise",
-  "resource://gre/modules/Promise.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "Task",
-  "resource://gre/modules/Task.jsm");
-
-var gTab = null;
 var gListener = null;
 const kURL = "data:text/html;charset=utf-8,Caret browsing is fun.<input id='in'>";
 
 const kPrefShortcutEnabled = "accessibility.browsewithcaret_shortcut.enabled";
 const kPrefWarnOnEnable    = "accessibility.warn_on_browsewithcaret";
 const kPrefCaretBrowsingOn = "accessibility.browsewithcaret";
 
 var oldPrefs = {};
@@ -16,264 +10,218 @@ for (let pref of [kPrefShortcutEnabled, 
   oldPrefs[pref] = Services.prefs.getBoolPref(pref);
 }
 
 Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
 Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
 Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
 
 registerCleanupFunction(function() {
-  if (gTab)
-    gBrowser.removeTab(gTab);
-  if (gListener)
-    Services.wm.removeListener(gListener);
-
   for (let pref of [kPrefShortcutEnabled, kPrefWarnOnEnable, kPrefCaretBrowsingOn]) {
     Services.prefs.setBoolPref(pref, oldPrefs[pref]);
   }
 });
 
-function waitForCondition(aConditionFn, aMaxTries=50, aCheckInterval=100) {
-  function tryNow() {
-    tries++;
-    if (aConditionFn()) {
-      deferred.resolve();
-    } else if (tries < aMaxTries) {
-      tryAgain();
-    } else {
-      deferred.reject("Condition timed out: " + aConditionFn.toSource());
+// NB: not using BrowserTestUtils.domWindowOpened here because there's no way to
+// undo waiting for a window open. If we don't want the window to be opened, and
+// wait for it to verify that it indeed does not open, we need to be able to
+// then "stop" waiting so that when we next *do* want it to open, our "old"
+// listener doesn't fire and do things we don't want (like close the window...).
+let gCaretPromptOpeningObserver;
+function promiseCaretPromptOpened() {
+  return new Promise(resolve => {
+    function observer(subject, topic, data) {
+      if (topic == "domwindowopened") {
+        Services.ww.unregisterNotification(observer);
+        let win = subject.QueryInterface(Ci.nsIDOMWindow);
+        BrowserTestUtils.waitForEvent(win, "load", false, e => e.target.location.href != "about:blank").then(() => resolve(win));
+        gCaretPromptOpeningObserver = null;
+      }
     }
-  }
-  function tryAgain() {
-    setTimeout(tryNow, aCheckInterval);
-  }
-  let deferred = Promise.defer();
-  let tries = 0;
-  tryAgain();
-  return deferred.promise;
-}
-
-function promiseWaitForDialogUnload(dialog) {
-  let deferred = Promise.defer();
-  dialog.addEventListener("unload", function listener() {
-    dialog.removeEventListener("unload", listener, false);
-    deferred.resolve();
-  }, false);
-  return deferred.promise;
-}
-
-function promiseWaitForFocusEvent(el) {
-  if (el.ownerDocument.activeElement == el) {
-    return true;
-  }
-  let deferred = Promise.defer();
-  el.addEventListener("focus", function listener() {
-    el.removeEventListener("focus", listener, false);
-    deferred.resolve();
-  }, false);
-  return deferred.promise;
-}
-
-function promiseTestPageLoad() {
-  let deferred = Promise.defer();
-  info("Waiting for test page to load.");
-
-  gTab = gBrowser.selectedTab = gBrowser.addTab(kURL);
-  let browser = gBrowser.selectedBrowser;
-  browser.addEventListener("load", function listener() {
-    if (browser.currentURI.spec == "about:blank")
-      return;
-    info("Page loaded: " + browser.currentURI.spec);
-    browser.removeEventListener("load", listener, true);
-
-    deferred.resolve();
-  }, true);
-
-  return deferred.promise;
-}
-
-function promiseCaretPromptOpened() {
-  let deferred = Promise.defer();
-  if (gListener) {
-    console.trace();
-    ok(false, "Should not be waiting for another prompt right now.");
-    return false;
-  }
-  info("Waiting for caret prompt to open");
-  gListener = {
-    onOpenWindow: function(win) {
-      let window = win.QueryInterface(Ci.nsIInterfaceRequestor)
-                      .getInterface(Ci.nsIDOMWindow);
-      window.addEventListener("load", function listener() {
-        window.removeEventListener("load", listener);
-        if (window.location.href == "chrome://global/content/commonDialog.xul") {
-          info("Caret prompt opened, removing listener and focusing");
-          Services.wm.removeListener(gListener);
-          gListener = null;
-          deferred.resolve(window);
-        }
-      });
-    },
-    onCloseWindow: function() {},
-  };
-  Services.wm.addListener(gListener);
-  return deferred.promise;
+    Services.ww.registerNotification(observer);
+    gCaretPromptOpeningObserver = observer;
+  });
 }
 
 function hitF7(async = true) {
-  let f7 = () => EventUtils.sendKey("F7", window.content);
+  let f7 = () => EventUtils.sendKey("F7");
   // Need to not stop execution inside this task:
   if (async) {
     executeSoon(f7);
   } else {
     f7();
   }
 }
 
 function syncToggleCaretNoDialog(expected) {
   let openedDialog = false;
   promiseCaretPromptOpened().then(function(win) {
     openedDialog = true;
     win.close(); // This will eventually return focus here and allow the test to continue...
   });
   // Cause the dialog to appear sync, if it still does.
   hitF7(false);
-  if (gListener) {
-    Services.wm.removeListener(gListener);
-    gListener = null;
-  }
+
   let expectedStr = expected ? "on." : "off.";
   ok(!openedDialog, "Shouldn't open a dialog to turn caret browsing " + expectedStr);
+  // Need to clean up if the dialog wasn't opened, so the observer doesn't get
+  // re-triggered later on causing "issues".
+  if (!openedDialog) {
+    Services.ww.unregisterNotification(gCaretPromptOpeningObserver);
+    gCaretPromptOpeningObserver = null;
+  }
   let prefVal = Services.prefs.getBoolPref(kPrefCaretBrowsingOn);
   is(prefVal, expected, "Caret browsing should now be " + expectedStr);
 }
 
+function waitForFocusOnInput(browser)
+{
+  return ContentTask.spawn(browser, null, function* () {
+    let textEl = content.document.getElementById("in");
+    return ContentTaskUtils.waitForCondition(() => {
+      return content.document.activeElement == textEl;
+    }, "Input should get focused.");
+  });
+}
+
+function focusInput(browser)
+{
+  return ContentTask.spawn(browser, null, function* () {
+    let textEl = content.document.getElementById("in");
+    textEl.focus();
+  });
+}
+
 add_task(function* checkTogglingCaretBrowsing() {
-  yield promiseTestPageLoad();
-  let textEl = window.content.document.getElementById("in");
-  textEl.focus();
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, kURL);
+  yield focusInput(tab.linkedBrowser);
 
   let promiseGotKey = promiseCaretPromptOpened();
   hitF7();
   let prompt = yield promiseGotKey;
   let doc = prompt.document;
   is(doc.documentElement.defaultButton, "cancel", "No button should be the default");
   ok(!doc.getElementById("checkbox").checked, "Checkbox shouldn't be checked by default.");
-  let promiseDialogUnloaded = promiseWaitForDialogUnload(prompt);
+  let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");
+
   doc.documentElement.cancelDialog();
   yield promiseDialogUnloaded;
-  yield waitForCondition(() => textEl.ownerDocument.activeElement == textEl);
+  info("Dialog unloaded");
+  yield waitForFocusOnInput(tab.linkedBrowser);
   ok(!Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should still be off after cancelling the dialog.");
 
   promiseGotKey = promiseCaretPromptOpened();
   hitF7();
   prompt = yield promiseGotKey;
 
   doc = prompt.document;
   is(doc.documentElement.defaultButton, "cancel", "No button should be the default");
   ok(!doc.getElementById("checkbox").checked, "Checkbox shouldn't be checked by default.");
-  promiseDialogUnloaded = promiseWaitForDialogUnload(prompt);
+  promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");
+
   doc.documentElement.acceptDialog();
   yield promiseDialogUnloaded;
-  yield waitForCondition(() => textEl.ownerDocument.activeElement == textEl);
+  info("Dialog unloaded");
+  yield waitForFocusOnInput(tab.linkedBrowser);
   ok(Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should be on after accepting the dialog.");
 
   syncToggleCaretNoDialog(false);
 
   promiseGotKey = promiseCaretPromptOpened();
   hitF7();
   prompt = yield promiseGotKey;
   doc = prompt.document;
 
   is(doc.documentElement.defaultButton, "cancel", "No button should be the default");
   ok(!doc.getElementById("checkbox").checked, "Checkbox shouldn't be checked by default.");
 
-  promiseDialogUnloaded = promiseWaitForDialogUnload(prompt);
+  promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");
   doc.documentElement.cancelDialog();
   yield promiseDialogUnloaded;
-  yield waitForCondition(() => textEl.ownerDocument.activeElement == textEl);
+  info("Dialog unloaded");
+  yield waitForFocusOnInput(tab.linkedBrowser);
 
   ok(!Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should still be off after cancelling the dialog.");
 
   Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
   Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
   Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
 
-  gBrowser.removeTab(gTab);
-  gTab = null;
+  yield BrowserTestUtils.removeTab(tab);
 });
 
 add_task(function* toggleCheckboxNoCaretBrowsing() {
-  yield promiseTestPageLoad();
-  let textEl = window.content.document.getElementById("in");
-  textEl.focus();
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, kURL);
+  yield focusInput(tab.linkedBrowser);
 
   let promiseGotKey = promiseCaretPromptOpened();
   hitF7();
   let prompt = yield promiseGotKey;
   let doc = prompt.document;
   is(doc.documentElement.defaultButton, "cancel", "No button should be the default");
   let checkbox = doc.getElementById("checkbox");
   ok(!checkbox.checked, "Checkbox shouldn't be checked by default.");
 
   // Check the box:
   checkbox.click();
-  let promiseDialogUnloaded = promiseWaitForDialogUnload(prompt);
+
+  let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");
+
   // Say no:
   doc.documentElement.getButton("cancel").click();
+
   yield promiseDialogUnloaded;
-  yield waitForCondition(() => textEl.ownerDocument.activeElement == textEl);
+  info("Dialog unloaded");
+  yield waitForFocusOnInput(tab.linkedBrowser);
   ok(!Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should still be off.");
-
   ok(!Services.prefs.getBoolPref(kPrefShortcutEnabled), "Shortcut should now be disabled.");
 
   syncToggleCaretNoDialog(false);
   ok(!Services.prefs.getBoolPref(kPrefShortcutEnabled), "Shortcut should still be disabled.");
 
   Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
   Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
   Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
 
-  gBrowser.removeTab(gTab);
-  gTab = null;
+  yield BrowserTestUtils.removeTab(tab);
 });
 
 
 add_task(function* toggleCheckboxWantCaretBrowsing() {
-  yield promiseTestPageLoad();
-  let textEl = window.content.document.getElementById("in");
-  textEl.focus();
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, kURL);
+  yield focusInput(tab.linkedBrowser);
 
   let promiseGotKey = promiseCaretPromptOpened();
   hitF7();
   let prompt = yield promiseGotKey;
   let doc = prompt.document;
   is(doc.documentElement.defaultButton, "cancel", "No button should be the default");
   let checkbox = doc.getElementById("checkbox");
   ok(!checkbox.checked, "Checkbox shouldn't be checked by default.");
 
   // Check the box:
   checkbox.click();
-  let promiseDialogUnloaded = promiseWaitForDialogUnload(prompt);
+
+  let promiseDialogUnloaded = BrowserTestUtils.waitForEvent(prompt, "unload");
+
   // Say yes:
   doc.documentElement.acceptDialog();
   yield promiseDialogUnloaded;
-  yield waitForCondition(() => textEl.ownerDocument.activeElement == textEl);
+  info("Dialog unloaded");
+  yield waitForFocusOnInput(tab.linkedBrowser);
   ok(Services.prefs.getBoolPref(kPrefCaretBrowsingOn), "Caret browsing should now be on.");
   ok(Services.prefs.getBoolPref(kPrefShortcutEnabled), "Shortcut should still be enabled.");
   ok(!Services.prefs.getBoolPref(kPrefWarnOnEnable), "Should no longer warn when enabling.");
 
-
   syncToggleCaretNoDialog(false);
   syncToggleCaretNoDialog(true);
   syncToggleCaretNoDialog(false);
 
   Services.prefs.setBoolPref(kPrefShortcutEnabled, true);
   Services.prefs.setBoolPref(kPrefWarnOnEnable, true);
   Services.prefs.setBoolPref(kPrefCaretBrowsingOn, false);
 
-  gBrowser.removeTab(gTab);
-  gTab = null;
+  yield BrowserTestUtils.removeTab(tab);
 });