Merge m-c to inbound. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 19 May 2016 21:43:31 -0400
changeset 298198 f70b8561b4796217c3328dfc97b61e8ae934c1dd
parent 298197 f186693ee23aa8444edc705ebce11667a8a0c1c1 (current diff)
parent 298133 c67dc1f9fab86d4f2cf3224307809c44fe3ce820 (diff)
child 298199 c403ac05b8f42e44a5f7a0c98c2bf190b28706b1
push id30273
push userkwierso@gmail.com
push dateFri, 20 May 2016 21:08:12 +0000
treeherdermozilla-central@c403ac05b8f4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone49.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 m-c to inbound. a=merge CLOSED TREE
--- 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/configs/multi_locale/ash_android-x86.json
+++ b/testing/mozharness/configs/multi_locale/ash_android-x86.json
@@ -16,15 +16,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/l10n-central",
     "hg_l10n_tag": "default",
     "l10n_dir": "l10n-central",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-x86/nightly"
 }
--- a/testing/mozharness/configs/multi_locale/ash_android.json
+++ b/testing/mozharness/configs/multi_locale/ash_android.json
@@ -16,14 +16,15 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/l10n-central",
     "hg_l10n_tag": "default",
     "l10n_dir": "l10n-central",
     "merge_locales": true,
     "mozilla_dir": "build"
 }
--- a/testing/mozharness/configs/multi_locale/mozilla-aurora_android-armv6.json
+++ b/testing/mozharness/configs/multi_locale/mozilla-aurora_android-armv6.json
@@ -16,15 +16,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-aurora",
     "hg_l10n_tag": "default",
     "l10n_dir": "mozilla-aurora",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-armv6/nightly"
 }
--- a/testing/mozharness/configs/multi_locale/mozilla-aurora_android-x86.json
+++ b/testing/mozharness/configs/multi_locale/mozilla-aurora_android-x86.json
@@ -16,15 +16,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-aurora",
     "hg_l10n_tag": "default",
     "l10n_dir": "mozilla-aurora",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-x86/nightly"
 }
--- a/testing/mozharness/configs/multi_locale/mozilla-aurora_android.json
+++ b/testing/mozharness/configs/multi_locale/mozilla-aurora_android.json
@@ -16,14 +16,15 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-aurora",
     "hg_l10n_tag": "default",
     "l10n_dir": "mozilla-aurora",
     "merge_locales": true,
     "mozilla_dir": "build"
 }
--- a/testing/mozharness/configs/multi_locale/mozilla-beta_android-armv6.json
+++ b/testing/mozharness/configs/multi_locale/mozilla-beta_android-armv6.json
@@ -16,15 +16,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-beta",
     "hg_l10n_tag": "default",
     "l10n_dir": "mozilla-beta",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-armv6/nightly"
 }
--- a/testing/mozharness/configs/multi_locale/mozilla-beta_android-x86.json
+++ b/testing/mozharness/configs/multi_locale/mozilla-beta_android-x86.json
@@ -16,15 +16,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-beta",
     "hg_l10n_tag": "default",
     "l10n_dir": "mozilla-beta",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-x86/nightly"
 }
--- a/testing/mozharness/configs/multi_locale/mozilla-beta_android.json
+++ b/testing/mozharness/configs/multi_locale/mozilla-beta_android.json
@@ -16,14 +16,15 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-beta",
     "hg_l10n_tag": "default",
     "l10n_dir": "mozilla-beta",
     "merge_locales": true,
     "mozilla_dir": "build"
 }
--- a/testing/mozharness/configs/multi_locale/mozilla-central_android-armv6.json
+++ b/testing/mozharness/configs/multi_locale/mozilla-central_android-armv6.json
@@ -16,15 +16,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/l10n-central",
     "hg_l10n_tag": "default",
     "l10n_dir": "l10n-central",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-armv6/nightly"
 }
--- a/testing/mozharness/configs/multi_locale/mozilla-central_android-x86.json
+++ b/testing/mozharness/configs/multi_locale/mozilla-central_android-x86.json
@@ -16,15 +16,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/l10n-central",
     "hg_l10n_tag": "default",
     "l10n_dir": "l10n-central",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-x86/nightly"
 }
--- a/testing/mozharness/configs/multi_locale/mozilla-central_android.json
+++ b/testing/mozharness/configs/multi_locale/mozilla-central_android.json
@@ -16,14 +16,15 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/l10n-central",
     "hg_l10n_tag": "default",
     "l10n_dir": "l10n-central",
     "merge_locales": true,
     "mozilla_dir": "build"
 }
--- a/testing/mozharness/configs/multi_locale/mozilla-release_android-armv6.json
+++ b/testing/mozharness/configs/multi_locale/mozilla-release_android-armv6.json
@@ -16,15 +16,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-release",
     "hg_l10n_tag": "default",
     "l10n_dir": "mozilla-release",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-armv6/nightly"
 }
--- a/testing/mozharness/configs/multi_locale/mozilla-release_android-x86.json
+++ b/testing/mozharness/configs/multi_locale/mozilla-release_android-x86.json
@@ -16,15 +16,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-release",
     "hg_l10n_tag": "default",
     "l10n_dir": "mozilla-release",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-x86/nightly"
 }
--- a/testing/mozharness/configs/multi_locale/mozilla-release_android.json
+++ b/testing/mozharness/configs/multi_locale/mozilla-release_android.json
@@ -16,14 +16,15 @@
     },{
         "repo": "https://hg.mozilla.org/build/tools",
         "tag": "default",
         "dest": "tools"
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-release",
     "hg_l10n_tag": "default",
     "l10n_dir": "mozilla-release",
     "merge_locales": true,
     "mozilla_dir": "build"
 }
--- a/testing/mozharness/configs/multi_locale/release_mozilla-beta_android-armv6.json
+++ b/testing/mozharness/configs/multi_locale/release_mozilla-beta_android-armv6.json
@@ -18,15 +18,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/build/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-beta",
     "required_config_vars": ["tag_override"],
     "l10n_dir": "mozilla-beta",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-armv6/release"
 }
--- a/testing/mozharness/configs/multi_locale/release_mozilla-beta_android-x86.json
+++ b/testing/mozharness/configs/multi_locale/release_mozilla-beta_android-x86.json
@@ -18,15 +18,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/build/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-beta",
     "required_config_vars": ["tag_override"],
     "l10n_dir": "mozilla-beta",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-x86/release"
 }
--- a/testing/mozharness/configs/multi_locale/release_mozilla-beta_android.json
+++ b/testing/mozharness/configs/multi_locale/release_mozilla-beta_android.json
@@ -18,14 +18,15 @@
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/build/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-beta",
     "required_config_vars": ["tag_override"],
     "l10n_dir": "mozilla-beta",
     "merge_locales": true,
     "mozilla_dir": "build"
 }
--- a/testing/mozharness/configs/multi_locale/release_mozilla-esr_android-armv6.json
+++ b/testing/mozharness/configs/multi_locale/release_mozilla-esr_android-armv6.json
@@ -18,15 +18,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/build/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-release",
     "required_config_vars": ["tag_override"],
     "l10n_dir": "mozilla-esr31",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-armv6/release"
 }
--- a/testing/mozharness/configs/multi_locale/release_mozilla-release_android-armv6.json
+++ b/testing/mozharness/configs/multi_locale/release_mozilla-release_android-armv6.json
@@ -18,15 +18,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/build/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-release",
     "required_config_vars": ["tag_override"],
     "l10n_dir": "mozilla-release",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-armv6/release"
 }
--- a/testing/mozharness/configs/multi_locale/release_mozilla-release_android-x86.json
+++ b/testing/mozharness/configs/multi_locale/release_mozilla-release_android-x86.json
@@ -18,15 +18,16 @@
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/build/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-release",
     "required_config_vars": ["tag_override"],
     "l10n_dir": "mozilla-release",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-x86/release"
 }
--- a/testing/mozharness/configs/multi_locale/release_mozilla-release_android.json
+++ b/testing/mozharness/configs/multi_locale/release_mozilla-release_android.json
@@ -18,14 +18,15 @@
     },{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/build/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/releases/l10n/mozilla-release",
     "required_config_vars": ["tag_override"],
     "l10n_dir": "mozilla-release",
     "merge_locales": true,
     "mozilla_dir": "build"
 }
--- a/testing/mozharness/configs/multi_locale/staging_release_mozilla-beta_android-armv6.json
+++ b/testing/mozharness/configs/multi_locale/staging_release_mozilla-beta_android-armv6.json
@@ -18,15 +18,16 @@
     },{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/%(user_repo_override)s",
     "required_config_vars": ["tag_override", "user_repo_override"],
     "l10n_dir": "mozilla-beta",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-armv6/release"
 }
--- a/testing/mozharness/configs/multi_locale/staging_release_mozilla-beta_android-x86.json
+++ b/testing/mozharness/configs/multi_locale/staging_release_mozilla-beta_android-x86.json
@@ -18,15 +18,16 @@
     },{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/%(user_repo_override)s",
     "required_config_vars": ["tag_override", "user_repo_override"],
     "l10n_dir": "mozilla-beta",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-x86/release"
 }
--- a/testing/mozharness/configs/multi_locale/staging_release_mozilla-beta_android.json
+++ b/testing/mozharness/configs/multi_locale/staging_release_mozilla-beta_android.json
@@ -18,14 +18,15 @@
     },{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/%(user_repo_override)s",
     "required_config_vars": ["tag_override", "user_repo_override"],
     "l10n_dir": "mozilla-beta",
     "merge_locales": true,
     "mozilla_dir": "build"
 }
--- a/testing/mozharness/configs/multi_locale/staging_release_mozilla-esr_android-armv6.json
+++ b/testing/mozharness/configs/multi_locale/staging_release_mozilla-esr_android-armv6.json
@@ -18,15 +18,16 @@
     },{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/users/stage-ffxbld",
     "required_config_vars": ["tag_override", "user_repo_override"],
     "l10n_dir": "mozilla-esr31",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-armv6/release"
 }
--- a/testing/mozharness/configs/multi_locale/staging_release_mozilla-release_android-armv6.json
+++ b/testing/mozharness/configs/multi_locale/staging_release_mozilla-release_android-armv6.json
@@ -18,15 +18,16 @@
     },{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/%(user_repo_override)s",
     "required_config_vars": ["tag_override", "user_repo_override"],
     "l10n_dir": "mozilla-release",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-armv6/release"
 }
--- a/testing/mozharness/configs/multi_locale/staging_release_mozilla-release_android-x86.json
+++ b/testing/mozharness/configs/multi_locale/staging_release_mozilla-release_android-x86.json
@@ -18,15 +18,16 @@
     },{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/%(user_repo_override)s",
     "required_config_vars": ["tag_override", "user_repo_override"],
     "l10n_dir": "mozilla-release",
     "merge_locales": true,
     "mozilla_dir": "build",
     "mozconfig": "build/mobile/android/config/mozconfigs/android-x86/release"
 }
--- a/testing/mozharness/configs/multi_locale/staging_release_mozilla-release_android.json
+++ b/testing/mozharness/configs/multi_locale/staging_release_mozilla-release_android.json
@@ -18,14 +18,15 @@
     },{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/%(user_repo_override)s/buildbot-configs",
         "dest": "build/configs"
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "hg_l10n_base": "https://hg.mozilla.org/%(user_repo_override)s",
     "required_config_vars": ["tag_override", "user_repo_override"],
     "l10n_dir": "mozilla-release",
     "merge_locales": true,
     "mozilla_dir": "build"
 }
--- a/testing/mozharness/configs/multi_locale/standalone_mozilla-central.py
+++ b/testing/mozharness/configs/multi_locale/standalone_mozilla-central.py
@@ -23,16 +23,17 @@ config = {
     "locales_file": "%s/%s/locales/maemo-locales" % (BUILD_DIR, ANDROID_DIR),
     "locales_dir": "%s/locales" % ANDROID_DIR,
     "ignore_locales": ["en-US", "multi"],
     "repos": [{
         "repo": "https://hg.mozilla.org/%s" % REPO_PATH,
         "tag": "default",
         "dest": BUILD_DIR,
     }],
+    "vcs_share_base": "/builds/hg-shared",
     "l10n_repos": [{
         "repo": "https://hg.mozilla.org/build/compare-locales",
         "tag": "RELEASE_AUTOMATION"
     }],
     "hg_l10n_base": "https://hg.mozilla.org/%s" % L10N_REPO_PATH,
     "hg_l10n_tag": "default",
     "l10n_dir": "l10n",
     "merge_locales": True,
--- 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);
 });