merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 16 Jun 2015 15:52:21 +0200
changeset 279728 95b628befd287933e2098988e49c564a17c999dc
parent 279700 64bbc886ac8cb89fdb8ec5c0a339b1db93fd5734 (current diff)
parent 279727 87c9b69a0ec085e4bb78560c80c195112a803aa7 (diff)
child 279839 abaec2f7ed2c10724f6b1d51a7d6c4a397a3daf8
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-beta@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone41.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
merge fx-team to mozilla-central a=merge
browser/themes/linux/webRTC-shareDevice-16.png
browser/themes/linux/webRTC-shareDevice-64.png
browser/themes/linux/webRTC-shareMicrophone-16.png
browser/themes/linux/webRTC-shareMicrophone-64.png
browser/themes/osx/webRTC-shareDevice-16.png
browser/themes/osx/webRTC-shareDevice-16@2x.png
browser/themes/osx/webRTC-shareDevice-64.png
browser/themes/osx/webRTC-shareDevice-64@2x.png
browser/themes/osx/webRTC-shareMicrophone-16.png
browser/themes/osx/webRTC-shareMicrophone-16@2x.png
browser/themes/osx/webRTC-shareMicrophone-64.png
browser/themes/osx/webRTC-shareMicrophone-64@2x.png
browser/themes/windows/webRTC-shareDevice-16.png
browser/themes/windows/webRTC-shareDevice-64.png
browser/themes/windows/webRTC-shareMicrophone-16.png
browser/themes/windows/webRTC-shareMicrophone-64.png
mobile/android/base/fennec-ids-generator.py
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1373,16 +1373,17 @@ pref("devtools.appmanager.manifestEditor
 
 // Enable DevTools WebIDE by default
 pref("devtools.webide.enabled", true);
 
 // Toolbox preferences
 pref("devtools.toolbox.footer.height", 250);
 pref("devtools.toolbox.sidebar.width", 500);
 pref("devtools.toolbox.host", "bottom");
+pref("devtools.toolbox.previousHost", "side");
 pref("devtools.toolbox.selectedTool", "webconsole");
 pref("devtools.toolbox.toolbarSpec", '["splitconsole", "paintflashing toggle","tilt toggle","scratchpad","resize toggle","eyedropper","screenshot --fullpage", "rulers"]');
 pref("devtools.toolbox.sideEnabled", true);
 pref("devtools.toolbox.zoomValue", "1");
 pref("devtools.toolbox.splitconsoleEnabled", false);
 pref("devtools.toolbox.splitconsoleHeight", 100);
 
 // Toolbox Button preferences
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -1236,24 +1236,19 @@
                          oncommand="DeveloperToolbar.hide();"
                          tooltiptext="&devToolbarCloseButton.tooltiptext;"/>
 #endif
    </toolbar>
   </vbox>
 
   <svg:svg height="0">
 #include tab-shape.inc.svg
-#if defined(XP_UNIX) && !defined(XP_MACOSX)
-    <svg:clipPath id="urlbar-clip-path" clipPathUnits="userSpaceOnUse">
-      <svg:path d="m 1,-5 l 0,50 l 10000,0 l 0,-50 z"/>
-    </svg:clipPath>
-#endif
     <svg:clipPath id="urlbar-back-button-clip-path" clipPathUnits="userSpaceOnUse">
 #ifndef XP_MACOSX
-      <svg:path d="m 1,-5 l 0,7.8 c 2.5,3.2 4,6.2 4,10.2 c 0,4 -1.5,7 -4,10 l 0,22 l 10000,0 l 0,-50 l -10000,0 z"/>
+      <svg:path d="m 1,-5 l 0,7.8 c 2.5,3.2 4,6.2 4,10.2 c 0,4 -1.5,7 -4,10 l 0,22l10000,0 l 0,-50 l -10000,0 z"/>
 #else
       <svg:path d="M -11,-5 a 16 16 0 0 1 0,34 l 10000,0 l 0,-34 l -10000,0 z"/>
 #endif
     </svg:clipPath>
   </svg:svg>
 
 </vbox>
 # <iframe id="tab-view"> is dynamically appended as the 2nd child of #tab-view-deck.
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -97,22 +97,25 @@ let handleContentContextMenu = function 
   let charSet = doc.characterSet;
   let baseURI = doc.baseURI;
   let referrer = doc.referrer;
   let referrerPolicy = doc.referrerPolicy;
   let frameOuterWindowID = doc.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
                                           .getInterface(Ci.nsIDOMWindowUtils)
                                           .outerWindowID;
 
+  let disableSetDesktopBg = null;
   // Media related cache info parent needs for saving
   let contentType = null;
   let contentDisposition = null;
   if (event.target.nodeType == Ci.nsIDOMNode.ELEMENT_NODE &&
       event.target instanceof Ci.nsIImageLoadingContent &&
       event.target.currentURI) {
+    disableSetDesktopBg = disableSetDesktopBackground(event.target);
+
     try {
       let imageCache = 
         Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools)
                                         .getImgCacheForDocument(doc);
       let props =
         imageCache.findEntryProperties(event.target.currentURI);
       try {
         contentType = props.get("type", Ci.nsISupportsCString).data;
@@ -143,17 +146,17 @@ let handleContentContextMenu = function 
     event.target.ownerDocument.defaultView.updateCommands("contentcontextmenu");
 
     let customMenuItems = PageMenuChild.build(event.target);
     let principal = doc.nodePrincipal;
     sendSyncMessage("contextmenu",
                     { editFlags, spellInfo, customMenuItems, addonInfo,
                       principal, docLocation, charSet, baseURI, referrer,
                       referrerPolicy, contentType, contentDisposition,
-                      frameOuterWindowID, selectionInfo },
+                      frameOuterWindowID, selectionInfo, disableSetDesktopBg },
                     { event, popupNode: event.target });
   }
   else {
     // Break out to the parent window and pass the add-on info along
     let browser = docShell.chromeEventHandler;
     let mainWin = browser.ownerDocument.defaultView;
     mainWin.gContextMenuContentData = {
       isRemote: false,
@@ -164,16 +167,17 @@ let handleContentContextMenu = function 
       documentURIObject: doc.documentURIObject,
       docLocation: docLocation,
       charSet: charSet,
       referrer: referrer,
       referrerPolicy: referrerPolicy,
       contentType: contentType,
       contentDisposition: contentDisposition,
       selectionInfo: selectionInfo,
+      disableSetDesktopBackground: disableSetDesktopBg,
     };
   }
 }
 
 Cc["@mozilla.org/eventlistenerservice;1"]
   .getService(Ci.nsIEventListenerService)
   .addSystemEventListener(global, "contextmenu", handleContentContextMenu, false);
 
@@ -748,8 +752,57 @@ addMessageListener("ContextMenu:SearchFi
   else {
     let separator = spec.includes("?") ? "&" : "?";
     spec += separator + formData.join("&");
   }
 
   sendAsyncMessage("ContextMenu:SearchFieldBookmarkData:Result",
                    { spec, title, description, postData, charset });
 });
+
+function disableSetDesktopBackground(aTarget) {
+  // Disable the Set as Desktop Background menu item if we're still trying
+  // to load the image or the load failed.
+  if (!(aTarget instanceof Ci.nsIImageLoadingContent))
+    return true;
+
+  if (("complete" in aTarget) && !aTarget.complete)
+    return true;
+
+  if (aTarget.currentURI.schemeIs("javascript"))
+    return true;
+
+  let request = aTarget.QueryInterface(Ci.nsIImageLoadingContent)
+                       .getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+  if (!request)
+    return true;
+
+  return false;
+}
+
+addMessageListener("ContextMenu:SetAsDesktopBackground", (message) => {
+  let target = message.objects.target;
+
+  // Paranoia: check disableSetDesktopBackground again, in case the
+  // image changed since the context menu was initiated.
+  let disable = disableSetDesktopBackground(target);
+
+  if (!disable) {
+    try {
+      BrowserUtils.urlSecurityCheck(target.currentURI.spec, target.ownerDocument.nodePrincipal);
+      let canvas = content.document.createElement("canvas");
+      canvas.width = target.naturalWidth;
+      canvas.height = target.naturalHeight;
+      let ctx = canvas.getContext("2d");
+      ctx.drawImage(target, 0, 0);
+      let dataUrl = canvas.toDataURL();
+      sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result",
+                       { dataUrl });
+    }
+    catch (e) {
+      Cu.reportError(e);
+      disable = true;
+    }
+  }
+
+  if (disable)
+    sendAsyncMessage("ContextMenu:SetAsDesktopBackground:Result", { disable });
+});
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -236,17 +236,17 @@ nsContextMenu.prototype = {
     if (shell)
       haveSetDesktopBackground = shell.canSetDesktopBackground;
 #endif
     this.showItem("context-setDesktopBackground",
                   haveSetDesktopBackground && this.onLoadedImage);
 
     if (haveSetDesktopBackground && this.onLoadedImage) {
       document.getElementById("context-setDesktopBackground")
-              .disabled = this.disableSetDesktopBackground();
+              .disabled = gContextMenuContentData.disableSetDesktopBackground;
     }
 
     // Reload image depends on an image that's not fully loaded
     this.showItem("context-reloadimage", (this.onImage && !this.onCompletedImage));
 
     // View image depends on having an image that's not standalone
     // (or is in a frame), or a canvas.
     this.showItem("context-viewimage", (this.onImage &&
@@ -1129,70 +1129,59 @@ nsContextMenu.prototype = {
   viewBGImage: function(e) {
     urlSecurityCheck(this.bgImageURL,
                      this.browser.contentPrincipal,
                      Ci.nsIScriptSecurityManager.DISALLOW_SCRIPT);
     openUILink(this.bgImageURL, e, { disallowInheritPrincipal: true,
                                      referrerURI: gContextMenuContentData.documentURIObject });
   },
 
-  disableSetDesktopBackground: function() {
-    // Disable the Set as Desktop Background menu item if we're still trying
-    // to load the image or the load failed.
-    if (!(this.target instanceof Ci.nsIImageLoadingContent))
-      return true;
+  setDesktopBackground: function() {
+    let mm = this.browser.messageManager;
 
-    if (("complete" in this.target) && !this.target.complete)
-      return true;
-
-    if (this.target.currentURI.schemeIs("javascript"))
-      return true;
+    mm.sendAsyncMessage("ContextMenu:SetAsDesktopBackground", null,
+                        { target: this.target });
 
-    var request = this.target
-                      .QueryInterface(Ci.nsIImageLoadingContent)
-                      .getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
-    if (!request)
-      return true;
+    let onMessage = (message) => {
+      mm.removeMessageListener("ContextMenu:SetAsDesktopBackground:Result",
+                               onMessage);
 
-    return false;
-  },
+      if (message.data.disable)
+        return;
 
-  setDesktopBackground: function() {
-    // Paranoia: check disableSetDesktopBackground again, in case the
-    // image changed since the context menu was initiated.
-    if (this.disableSetDesktopBackground())
-      return;
+      let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');
+      image.src = message.data.dataUrl;
 
-    var doc = this.target.ownerDocument;
-    urlSecurityCheck(this.target.currentURI.spec, this.principal);
-
-    // Confirm since it's annoying if you hit this accidentally.
-    const kDesktopBackgroundURL =
-                  "chrome://browser/content/setDesktopBackground.xul";
+      // Confirm since it's annoying if you hit this accidentally.
+      const kDesktopBackgroundURL =
+                    "chrome://browser/content/setDesktopBackground.xul";
 #ifdef XP_MACOSX
-    // On Mac, the Set Desktop Background window is not modal.
-    // Don't open more than one Set Desktop Background window.
-    var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
-                       .getService(Components.interfaces.nsIWindowMediator);
-    var dbWin = wm.getMostRecentWindow("Shell:SetDesktopBackground");
-    if (dbWin) {
-      dbWin.gSetBackground.init(this.target);
-      dbWin.focus();
-    }
-    else {
+      // On Mac, the Set Desktop Background window is not modal.
+      // Don't open more than one Set Desktop Background window.
+      const wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+                 getService(Ci.nsIWindowMediator);
+      let dbWin = wm.getMostRecentWindow("Shell:SetDesktopBackground");
+      if (dbWin) {
+        dbWin.gSetBackground.init(image);
+        dbWin.focus();
+      }
+      else {
+        openDialog(kDesktopBackgroundURL, "",
+                   "centerscreen,chrome,dialog=no,dependent,resizable=no",
+                   image);
+      }
+#else
+      // On non-Mac platforms, the Set Wallpaper dialog is modal.
       openDialog(kDesktopBackgroundURL, "",
-                 "centerscreen,chrome,dialog=no,dependent,resizable=no",
-                 this.target);
-    }
-#else
-    // On non-Mac platforms, the Set Wallpaper dialog is modal.
-    openDialog(kDesktopBackgroundURL, "",
-               "centerscreen,chrome,dialog,modal,dependent",
-               this.target);
+                 "centerscreen,chrome,dialog,modal,dependent",
+                 image);
 #endif
+    };
+
+    mm.addMessageListener("ContextMenu:SetAsDesktopBackground:Result", onMessage);
   },
 
   // Save URL of clicked-on frame.
   saveFrame: function () {
     saveDocument(this.target.ownerDocument);
   },
 
   // Helper function to wait for appropriate MIME-type headers and
@@ -1293,25 +1282,27 @@ nsContextMenu.prototype = {
     function timerCallback() {}
     timerCallback.prototype = {
       notify: function sLA_timer_notify(aTimer) {
         channel.cancel(NS_ERROR_SAVE_LINK_AS_TIMEOUT);
         return;
       }
     }
 
-    // set up a channel to do the saving
+    // setting up a new channel for 'right click - save link as ...'
+    // which should be treated the same way as a toplevel load, hence
+    // we use TYPE_DOCUMENT, see also bug: 1136055
     var ioService = Cc["@mozilla.org/network/io-service;1"].
                     getService(Ci.nsIIOService);
     var channel = ioService.newChannelFromURI2(makeURI(linkURL),
                                                null, // aLoadingNode
                                                this.principal, // aLoadingPrincipal
                                                null, // aTriggeringPrincipal
                                                Ci.nsILoadInfo.SEC_NORMAL,
-                                               Ci.nsIContentPolicy.TYPE_OTHER);
+                                               Ci.nsIContentPolicy.TYPE_DOCUMENT);
     if (linkDownload)
       channel.contentDispositionFilename = linkDownload;
     if (channel instanceof Ci.nsIPrivateBrowsingChannel) {
       let docIsPrivate = PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser);
       channel.setPrivate(docIsPrivate);
     }
     channel.notificationCallbacks = new callbacks();
 
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -3885,16 +3885,17 @@
                                           docLocation: aMessage.data.docLocation,
                                           charSet: aMessage.data.charSet,
                                           referrer: aMessage.data.referrer,
                                           referrerPolicy: aMessage.data.referrerPolicy,
                                           contentType: aMessage.data.contentType,
                                           contentDisposition: aMessage.data.contentDisposition,
                                           frameOuterWindowID: aMessage.data.frameOuterWindowID,
                                           selectionInfo: aMessage.data.selectionInfo,
+                                          disableSetDesktopBackground: aMessage.data.disableSetDesktopBg,
                                         };
               let popup = browser.ownerDocument.getElementById("contentAreaContextMenu");
               let event = gContextMenuContentData.event;
               popup.openPopupAtScreen(event.screenX, event.screenY, true);
               break;
             }
             case "DOMWebNotificationClicked": {
               let tab = this.getTabForBrowser(browser);
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -132,17 +132,17 @@ skip-if = e10s # Bug 1093153 - no about:
 [browser_autocomplete_a11y_label.js]
 skip-if = e10s # Bug 1101993 - times out for unknown reasons when run in the dir (works on its own)
 [browser_autocomplete_enter_race.js]
 [browser_autocomplete_no_title.js]
 [browser_autocomplete_autoselect.js]
 [browser_autocomplete_oldschool_wrap.js]
 [browser_autocomplete_tag_star_visibility.js]
 [browser_backButtonFitts.js]
-skip-if = os != "win" # The Fitts Law back button is only supported on Windows (bug 571454)
+skip-if = os == "mac" # The Fitt's Law back button is not supported on OS X
 [browser_beforeunload_duplicate_dialogs.js]
 skip-if = e10s # bug 967873 means permitUnload doesn't work in e10s mode
 [browser_blob-channelname.js]
 [browser_bookmark_titles.js]
 skip-if = buildapp == 'mulet' || toolkit == "windows" # Disabled on Windows due to frequent failures (bugs 825739, 841341)
 [browser_bug304198.js]
 [browser_bug321000.js]
 skip-if = true # browser_bug321000.js is disabled because newline handling is shaky (bug 592528)
--- a/browser/base/content/test/general/browser_readerMode_hidden_nodes.js
+++ b/browser/base/content/test/general/browser_readerMode_hidden_nodes.js
@@ -31,15 +31,24 @@ add_task(function* test_reader_button() 
   TEST_PREFS.forEach(([name, value]) => {
     Services.prefs.setBoolPref(name, value);
   });
 
   let tab = gBrowser.selectedTab = gBrowser.addTab();
   is_element_hidden(readerButton, "Reader mode button is not present on a new tab");
   // Point tab to a test page that is not reader-able due to hidden nodes.
   let url = TEST_PATH + "readerModeArticleHiddenNodes.html";
-  yield promiseTabLoadEvent(tab, url);
-  yield ContentTask.spawn(tab.linkedBrowser, "", function() {
-    return ContentTaskUtils.waitForEvent(content, "MozAfterPaint");
+  let paintPromise = ContentTask.spawn(tab.linkedBrowser, "", function() {
+    return new Promise(resolve => {
+      addEventListener("DOMContentLoaded", function onDCL() {
+        removeEventListener("DOMContentLoaded", onDCL);
+        addEventListener("MozAfterPaint", function onPaint() {
+          removeEventListener("MozAfterPaint", onPaint);
+          resolve();
+        });
+      });
+    });
   });
+  tab.linkedBrowser.loadURI(url);
+  yield paintPromise;
 
   is_element_hidden(readerButton, "Reader mode button is still not present on tab with unreadable content.");
 });
--- a/browser/base/content/test/general/healthreport_testRemoteCommands.html
+++ b/browser/base/content/test/general/healthreport_testRemoteCommands.html
@@ -1,267 +1,272 @@
-<html>
-  <head>
-    <meta charset="utf-8">
-<script type="application/javascript;version=1.7"
-            src="healthreport_pingData.js">
-</script>
-<script type="application/javascript;version=1.7">
-
-function init() {
-  window.addEventListener("message", function process(e) {
-    // The init function of abouthealth.js schedules an initial payload event,
-    // which will be sent after the payload data has been collected. This extra
-    // event can cause unexpected successes/failures in this test, so we wait
-    // for the extra event to arrive here before progressing with the actual
-    // test.
-    if (e.data.type == "payload") {
-      window.removeEventListener("message", process, false);
-
-      window.addEventListener("message", doTest, false);
-      doTest();
-    }
-  }, false);
-}
-
-function checkSubmissionValue(payload, expectedValue) {
-  return payload.enabled == expectedValue;
-}
-
-function validatePayload(payload) {
-  payload = JSON.parse(payload);
-
-  // xxxmpc - this is some pretty low-bar validation, but we have plenty of tests of that API elsewhere
-  if (!payload.thisPingDate)
-    return false;
-
-  return true;
-}
-
-function isArray(arg) {
-  return Object.prototype.toString.call(arg) === '[object Array]';
-}
-
-function writeDiagnostic(text) {
-  let node = document.createTextNode(text);
-  let br = document.createElement("br");
-  document.body.appendChild(node);
-  document.body.appendChild(br);
-}
-
-function validateCurrentTelemetryEnvironment(data) {
-  // Simple check for now: check that the received object has the expected
-  // top-level properties.
-  const expectedKeys = ["profile", "settings", "system", "build", "partner", "addons"];
-  return expectedKeys.every(key => (key in data));
-}
-
-function validateCurrentTelemetryPingData(ping) {
-  // Simple check for now: check that the received object has the expected
-  // top-level properties and that the type and reason match.
-  const expectedKeys = ["environment", "clientId", "payload", "application",
-                        "version", "type", "id"];
-  return expectedKeys.every(key => (key in ping)) &&
-         (ping.type == "main") &&
-         ("info" in ping.payload) &&
-         ("reason" in ping.payload.info) &&
-         (ping.payload.info.reason == "gather-subsession-payload");
-}
-
-function validateTelemetryPingList(list) {
-  if (!isArray(list)) {
-    console.log("Telemetry ping list is not an array.");
-    return false;
-  }
-
-  if (list.length != TEST_PINGS.length) {
-    console.log("Telemetry ping length is not correct.");
-    return false;
-  }
-
-  let valid = true;
-  for (let i=0; i<list.length; ++i) {
-    let received = list[i];
-    let expected = TEST_PINGS[i];
-    if (received.type != expected.type ||
-        received.timestampCreated != expected.date.getTime()) {
-      writeDiagnostic("Telemetry ping " + i + " does not match.");
-      writeDiagnostic("Expected: " + JSON.stringify(expected));
-      writeDiagnostic("Received: " + JSON.stringify(received));
-      valid = false;
-    } else {
-      writeDiagnostic("Telemetry ping " + i + " matches.");
-    }
-  }
-
-  return true;
-}
-
-function validateTelemetryPingData(expected, received) {
-  const receivedDate = new Date(received.creationDate);
-  if (received.id != expected.id ||
-      received.type != expected.type ||
-      receivedDate.getTime() != expected.date.getTime()) {
-    writeDiagnostic("Telemetry ping data for " + expected.id + " doesn't match.");
-    writeDiagnostic("Expected: " + JSON.stringify(expected));
-    writeDiagnostic("Received: " + JSON.stringify(received));
-    return false;
-  }
-
-  writeDiagnostic("Telemetry ping data for " + expected.id + " matched.");
-  return true;
-}
-
-var tests = [
-{
-  info: "Checking initial value is enabled",
-  event: "RequestCurrentPrefs",
-  payloadType: "prefs",
-  validateResponse: function(payload) {
-    return checkSubmissionValue(payload, true);
-  },
-},
-{
-  info: "Verifying disabling works",
-  event: "DisableDataSubmission",
-  payloadType: "prefs",
-  validateResponse: function(payload) {
-    return checkSubmissionValue(payload, false);
-  },
-},
-{
-  info: "Verifying we're still disabled",
-  event: "RequestCurrentPrefs",
-  payloadType: "prefs",
-  validateResponse: function(payload) {
-    return checkSubmissionValue(payload, false);
-  },
-},
-{
-  info: "Verifying we can get a payload while submission is disabled",
-  event: "RequestCurrentPayload",
-  payloadType: "payload",
-  validateResponse: function(payload) {
-    return validatePayload(payload);
-  },
-},
-{
-  info: "Verifying enabling works",
-  event: "EnableDataSubmission",
-  payloadType: "prefs",
-  validateResponse: function(payload) {
-    return checkSubmissionValue(payload, true);
-  },
-},
-{
-  info: "Verifying we're still re-enabled",
-  event: "RequestCurrentPrefs",
-  payloadType: "prefs",
-  validateResponse: function(payload) {
-    return checkSubmissionValue(payload, true);
-  },
-},
-{
-  info: "Verifying we can get a payload after re-enabling",
-  event: "RequestCurrentPayload",
-  payloadType: "payload",
-  validateResponse: function(payload) {
-    return validatePayload(payload);
-  },
-},
-{
-  info: "Verifying that we can get the current Telemetry environment data",
-  event: "RequestCurrentEnvironment",
-  payloadType: "telemetry-current-environment-data",
-  validateResponse: function(payload) {
-    return validateCurrentTelemetryEnvironment(payload);
-  },
-},
-{
-  info: "Verifying that we can get the current Telemetry ping data",
-  event: "RequestCurrentPingData",
-  payloadType: "telemetry-current-ping-data",
-  validateResponse: function(payload) {
-    return validateCurrentTelemetryPingData(payload);
-  },
-},
-{
-  info: "Verifying that we get the proper Telemetry ping list",
-  event: "RequestTelemetryPingList",
-  payloadType: "telemetry-ping-list",
-  validateResponse: function(payload) {
-    // Validate the ping list
-    if (!validateTelemetryPingList(payload)) {
-      return false;
-    }
-
-    // Now that we received the ping ids, set up additional test tasks
-    // that check loading the individual pings.
-    for (let i=0; i<TEST_PINGS.length; ++i) {
-      TEST_PINGS[i].id = payload[i].id;
-      tests.push({
-        info: "Verifying that we can get the proper Telemetry ping data #" + (i + 1),
-        event: "RequestTelemetryPingData",
-        eventData: { id: TEST_PINGS[i].id },
-        payloadType: "telemetry-ping-data",
-        validateResponse: function(payload) {
-          return validateTelemetryPingData(TEST_PINGS[i], payload.pingData);
-        },
-      });
-    }
-
-    return true;
-  },
-},
-];
-
-var currentTest = -1;
-function doTest(evt) {
-  if (evt) {
-    if (currentTest < 0 || !evt.data.content)
-      return; // not yet testing
-
-    var test = tests[currentTest];
-    if (evt.data.type != test.payloadType)
-      return; // skip unrequested events
-
-    var error = JSON.stringify(evt.data.content);
-    var pass = false;
-    try {
-      pass = test.validateResponse(evt.data.content)
-    } catch (e) {}
-    reportResult(test.info, pass, error);
-  }
-  // start the next test if there are any left
-  if (tests[++currentTest])
-    sendToBrowser(tests[currentTest].event, tests[currentTest].eventData);
-  else
-    reportFinished();
-}
-
-function reportResult(info, pass, error) {
-  var data = {type: "testResult", info: info, pass: pass, error: error};
-  var event = new CustomEvent("FirefoxHealthReportTestResponse", {detail: {data: data}, bubbles: true});
-  document.dispatchEvent(event);
-}
-
-function reportFinished(cmd) {
-  var data = {type: "testsComplete", count: tests.length};
-  var event = new CustomEvent("FirefoxHealthReportTestResponse", {detail: {data: data}, bubbles: true});
-  document.dispatchEvent(event);
-}
-
-function sendToBrowser(type, eventData) {
-  eventData = eventData || {};
-  let detail = {command: type};
-  for (let key of Object.keys(eventData)) {
-    detail[key] = eventData[key];
-  }
-
-  var event = new CustomEvent("RemoteHealthReportCommand", {detail: detail, bubbles: true});
-  document.dispatchEvent(event);
-}
-
-</script>
-  </head>
-  <body onload="init()">
-  </body>
-</html>
+<html>
+  <head>
+    <meta charset="utf-8">
+<script type="application/javascript;version=1.7"
+            src="healthreport_pingData.js">
+</script>
+<script type="application/javascript;version=1.7">
+
+function init() {
+  window.addEventListener("message", function process(e) {
+    // The init function of abouthealth.js schedules an initial payload event,
+    // which will be sent after the payload data has been collected. This extra
+    // event can cause unexpected successes/failures in this test, so we wait
+    // for the extra event to arrive here before progressing with the actual
+    // test.
+    if (e.data.type == "payload") {
+      window.removeEventListener("message", process, false);
+
+      window.addEventListener("message", doTest, false);
+      doTest();
+    }
+  }, false);
+}
+
+function checkSubmissionValue(payload, expectedValue) {
+  return payload.enabled == expectedValue;
+}
+
+function validatePayload(payload) {
+  payload = JSON.parse(payload);
+
+  // xxxmpc - this is some pretty low-bar validation, but we have plenty of tests of that API elsewhere
+  if (!payload.thisPingDate)
+    return false;
+
+  return true;
+}
+
+function isArray(arg) {
+  return Object.prototype.toString.call(arg) === '[object Array]';
+}
+
+function writeDiagnostic(text) {
+  let node = document.createTextNode(text);
+  let br = document.createElement("br");
+  document.body.appendChild(node);
+  document.body.appendChild(br);
+}
+
+function validateCurrentTelemetryEnvironment(data) {
+  // Simple check for now: check that the received object has the expected
+  // top-level properties.
+  const expectedKeys = ["profile", "settings", "system", "build", "partner", "addons"];
+  return expectedKeys.every(key => (key in data));
+}
+
+function validateCurrentTelemetryPingData(ping) {
+  // Simple check for now: check that the received object has the expected
+  // top-level properties and that the type and reason match.
+  const expectedKeys = ["environment", "clientId", "payload", "application",
+                        "version", "type", "id"];
+  return expectedKeys.every(key => (key in ping)) &&
+         (ping.type == "main") &&
+         ("info" in ping.payload) &&
+         ("reason" in ping.payload.info) &&
+         (ping.payload.info.reason == "gather-subsession-payload");
+}
+
+function validateTelemetryPingList(list) {
+  if (!isArray(list)) {
+    console.log("Telemetry ping list is not an array.");
+    return false;
+  }
+
+  // Telemetry may generate other pings (e.g. "deletion" pings), so filter those
+  // out.
+  const TEST_TYPES_REGEX = /^test-telemetryArchive/;
+  list = list.filter(p => TEST_TYPES_REGEX.test(p.type));
+
+  if (list.length != TEST_PINGS.length) {
+    console.log("Telemetry ping length is not correct.");
+    return false;
+  }
+
+  let valid = true;
+  for (let i=0; i<list.length; ++i) {
+    let received = list[i];
+    let expected = TEST_PINGS[i];
+    if (received.type != expected.type ||
+        received.timestampCreated != expected.date.getTime()) {
+      writeDiagnostic("Telemetry ping " + i + " does not match.");
+      writeDiagnostic("Expected: " + JSON.stringify(expected));
+      writeDiagnostic("Received: " + JSON.stringify(received));
+      valid = false;
+    } else {
+      writeDiagnostic("Telemetry ping " + i + " matches.");
+    }
+  }
+
+  return true;
+}
+
+function validateTelemetryPingData(expected, received) {
+  const receivedDate = new Date(received.creationDate);
+  if (received.id != expected.id ||
+      received.type != expected.type ||
+      receivedDate.getTime() != expected.date.getTime()) {
+    writeDiagnostic("Telemetry ping data for " + expected.id + " doesn't match.");
+    writeDiagnostic("Expected: " + JSON.stringify(expected));
+    writeDiagnostic("Received: " + JSON.stringify(received));
+    return false;
+  }
+
+  writeDiagnostic("Telemetry ping data for " + expected.id + " matched.");
+  return true;
+}
+
+var tests = [
+{
+  info: "Checking initial value is enabled",
+  event: "RequestCurrentPrefs",
+  payloadType: "prefs",
+  validateResponse: function(payload) {
+    return checkSubmissionValue(payload, true);
+  },
+},
+{
+  info: "Verifying disabling works",
+  event: "DisableDataSubmission",
+  payloadType: "prefs",
+  validateResponse: function(payload) {
+    return checkSubmissionValue(payload, false);
+  },
+},
+{
+  info: "Verifying we're still disabled",
+  event: "RequestCurrentPrefs",
+  payloadType: "prefs",
+  validateResponse: function(payload) {
+    return checkSubmissionValue(payload, false);
+  },
+},
+{
+  info: "Verifying we can get a payload while submission is disabled",
+  event: "RequestCurrentPayload",
+  payloadType: "payload",
+  validateResponse: function(payload) {
+    return validatePayload(payload);
+  },
+},
+{
+  info: "Verifying enabling works",
+  event: "EnableDataSubmission",
+  payloadType: "prefs",
+  validateResponse: function(payload) {
+    return checkSubmissionValue(payload, true);
+  },
+},
+{
+  info: "Verifying we're still re-enabled",
+  event: "RequestCurrentPrefs",
+  payloadType: "prefs",
+  validateResponse: function(payload) {
+    return checkSubmissionValue(payload, true);
+  },
+},
+{
+  info: "Verifying we can get a payload after re-enabling",
+  event: "RequestCurrentPayload",
+  payloadType: "payload",
+  validateResponse: function(payload) {
+    return validatePayload(payload);
+  },
+},
+{
+  info: "Verifying that we can get the current Telemetry environment data",
+  event: "RequestCurrentEnvironment",
+  payloadType: "telemetry-current-environment-data",
+  validateResponse: function(payload) {
+    return validateCurrentTelemetryEnvironment(payload);
+  },
+},
+{
+  info: "Verifying that we can get the current Telemetry ping data",
+  event: "RequestCurrentPingData",
+  payloadType: "telemetry-current-ping-data",
+  validateResponse: function(payload) {
+    return validateCurrentTelemetryPingData(payload);
+  },
+},
+{
+  info: "Verifying that we get the proper Telemetry ping list",
+  event: "RequestTelemetryPingList",
+  payloadType: "telemetry-ping-list",
+  validateResponse: function(payload) {
+    // Validate the ping list
+    if (!validateTelemetryPingList(payload)) {
+      return false;
+    }
+
+    // Now that we received the ping ids, set up additional test tasks
+    // that check loading the individual pings.
+    for (let i=0; i<TEST_PINGS.length; ++i) {
+      TEST_PINGS[i].id = payload[i].id;
+      tests.push({
+        info: "Verifying that we can get the proper Telemetry ping data #" + (i + 1),
+        event: "RequestTelemetryPingData",
+        eventData: { id: TEST_PINGS[i].id },
+        payloadType: "telemetry-ping-data",
+        validateResponse: function(payload) {
+          return validateTelemetryPingData(TEST_PINGS[i], payload.pingData);
+        },
+      });
+    }
+
+    return true;
+  },
+},
+];
+
+var currentTest = -1;
+function doTest(evt) {
+  if (evt) {
+    if (currentTest < 0 || !evt.data.content)
+      return; // not yet testing
+
+    var test = tests[currentTest];
+    if (evt.data.type != test.payloadType)
+      return; // skip unrequested events
+
+    var error = JSON.stringify(evt.data.content);
+    var pass = false;
+    try {
+      pass = test.validateResponse(evt.data.content)
+    } catch (e) {}
+    reportResult(test.info, pass, error);
+  }
+  // start the next test if there are any left
+  if (tests[++currentTest])
+    sendToBrowser(tests[currentTest].event, tests[currentTest].eventData);
+  else
+    reportFinished();
+}
+
+function reportResult(info, pass, error) {
+  var data = {type: "testResult", info: info, pass: pass, error: error};
+  var event = new CustomEvent("FirefoxHealthReportTestResponse", {detail: {data: data}, bubbles: true});
+  document.dispatchEvent(event);
+}
+
+function reportFinished(cmd) {
+  var data = {type: "testsComplete", count: tests.length};
+  var event = new CustomEvent("FirefoxHealthReportTestResponse", {detail: {data: data}, bubbles: true});
+  document.dispatchEvent(event);
+}
+
+function sendToBrowser(type, eventData) {
+  eventData = eventData || {};
+  let detail = {command: type};
+  for (let key of Object.keys(eventData)) {
+    detail[key] = eventData[key];
+  }
+
+  var event = new CustomEvent("RemoteHealthReportCommand", {detail: detail, bubbles: true});
+  document.dispatchEvent(event);
+}
+
+</script>
+  </head>
+  <body onload="init()">
+  </body>
+</html>
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -3066,16 +3066,27 @@ file, You can obtain one at http://mozil
 
           // Only handle supported notification panels.
           if (!this._notificationType) {
             return;
           }
 
           let viewsLeft = this._viewsLeft;
           if (viewsLeft) {
+            let notification = this._panel.firstElementChild.notification;
+            if (this._notificationType == "passwords" && notification && notification.options &&
+                notification.options.origin) {
+              let fxAOrigin = new URL(Services.prefs.getCharPref("identity.fxaccounts.remote.signup.uri")).origin
+              if (notification.options.origin == fxAOrigin) {
+                // Somewhat gross hack - we don't want to show the sync promo while
+                // the user may be logging into Sync.
+                return;
+              }
+            }
+
             if (Services.prefs.prefHasUserValue("services.sync.username") &&
                this._notificationType != "addons-sync-disabled") {
               // If the user has already setup Sync, don't show the notification.
               this._viewsLeft = 0;
               // Be sure to hide the panel, in case it was visible and the user
               // decided to setup Sync after noticing it.
               viewsLeft = 0;
               // The panel is still hidden, just bail out.
--- a/browser/components/uitour/UITour.jsm
+++ b/browser/components/uitour/UITour.jsm
@@ -7,16 +7,17 @@
 this.EXPORTED_SYMBOLS = ["UITour", "UITourMetricsProvider"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/TelemetryController.jsm");
 
 Cu.importGlobalProperties(["URL"]);
 
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
   "resource://gre/modules/LightweightThemeManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ResetProfile",
   "resource://gre/modules/ResetProfile.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
@@ -1973,16 +1974,26 @@ this.UITour.init();
  */
 const DAILY_DISCRETE_TEXT_FIELD = Metrics.Storage.FIELD_DAILY_DISCRETE_TEXT;
 
 /**
  * Public API to be called by the UITour code
  */
 const UITourHealthReport = {
   recordTreatmentTag: function(tag, value) {
+  TelemetryController.submitExternalPing("uitour-tag",
+    {
+      version: 1,
+      tagName: tag,
+      tagValue: value,
+    },
+    {
+      addClientId: true,
+      addEnvironment: true,
+    });
 #ifdef MOZ_SERVICES_HEALTHREPORT
     Task.spawn(function*() {
       let reporter = Cc["@mozilla.org/datareporting/service;1"]
                        .getService()
                        .wrappedJSObject
                        .healthReporter;
 
       // This can happen if the FHR component of the data reporting service is
--- a/browser/components/uitour/test/browser_UITour.js
+++ b/browser/components/uitour/test/browser_UITour.js
@@ -3,16 +3,17 @@
 
 "use strict";
 
 let gTestTab;
 let gContentAPI;
 let gContentWindow;
 
 Components.utils.import("resource:///modules/UITour.jsm");
+Components.utils.import("resource://testing-common/TelemetryArchiveTesting.jsm", this);
 
 function test() {
   UITourTest();
 }
 
 let tests = [
   function test_untrusted_host(done) {
     loadUITourTestPage(function() {
@@ -395,23 +396,34 @@ let tests = [
           Services.obs.removeObserver(observe, "browser-search-engine-modified");
           Services.search.defaultEngine = defaultEngine;
         });
 
         gContentAPI.setDefaultSearchEngine(someOtherEngineID);
       });
     });
   },
-  function test_treatment_tag(done) {
+  taskify(function* test_treatment_tag(done) {
+    let ac = new TelemetryArchiveTesting.Checker();
+    yield ac.promiseInit();
     gContentAPI.setTreatmentTag("foobar", "baz");
     gContentAPI.getTreatmentTag("foobar", (data) => {
       is(data.value, "baz", "set and retrieved treatmentTag");
-      done();
+      ac.promiseFindPing("uitour-tag", [
+        [["payload", "tagName"], "foobar"],
+        [["payload", "tagValue"], "baz"],
+      ]).then((found) => {
+        ok(found, "Telemetry ping submitted for setTreatmentTag");
+        done();
+      }, (err) => {
+        ok(false, "Exeption finding uitour telemetry ping: " + err);
+        done();
+      });
     });
-  },
+  }),
 
   // Make sure this test is last in the file so the appMenu gets left open and done will confirm it got tore down.
   taskify(function* cleanupMenus() {
     let shownPromise = promisePanelShown(window);
     gContentAPI.showMenu("appMenu");
     yield shownPromise;
   }),
 ];
--- a/browser/devtools/framework/test/browser.ini
+++ b/browser/devtools/framework/test/browser.ini
@@ -17,16 +17,17 @@ support-files =
   serviceworker.js
 
 [browser_devtools_api.js]
 [browser_devtools_api_destroy.js]
 [browser_dynamic_tool_enabling.js]
 [browser_ignore_toolbox_network_requests.js]
 [browser_keybindings_01.js]
 [browser_keybindings_02.js]
+[browser_keybindings_03.js]
 [browser_new_activation_workflow.js]
 [browser_target_events.js]
 [browser_target_remote.js]
 [browser_target_support.js]
 [browser_two_tabs.js]
 [browser_toolbox_dynamic_registration.js]
 [browser_toolbox_getpanelwhenready.js]
 [browser_toolbox_highlight.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/framework/test/browser_keybindings_03.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the toolbox 'switch to previous host' feature works.
+// Pressing ctrl/cmd+shift+d should switch to the last used host.
+
+const URL = "data:text/html;charset=utf8,test page for toolbox switching";
+
+add_task(function*() {
+  info("Create a test tab and open the toolbox");
+  let tab = yield addTab(URL);
+  let target = TargetFactory.forTab(tab);
+  let toolbox = yield gDevTools.showToolbox(target, "webconsole");
+
+  let keyElement = toolbox.doc.getElementById("toolbox-toggle-host-key");
+
+  let {SIDE, BOTTOM, WINDOW} = devtools.Toolbox.HostType;
+  checkHostType(toolbox, BOTTOM, SIDE);
+
+  info ("Switching from bottom to side");
+  synthesizeKeyElement(keyElement);
+  yield toolbox.once("host-changed");
+  checkHostType(toolbox, SIDE, BOTTOM);
+
+  info ("Switching from side to bottom");
+  synthesizeKeyElement(keyElement);
+  yield toolbox.once("host-changed");
+  checkHostType(toolbox, BOTTOM, SIDE);
+
+  info ("Switching to window");
+  yield toolbox.switchHost(WINDOW);
+  checkHostType(toolbox, WINDOW, BOTTOM);
+
+  info ("Switching from window to bottom");
+  synthesizeKeyElement(keyElement);
+  yield toolbox.once("host-changed");
+  checkHostType(toolbox, BOTTOM, WINDOW);
+
+  yield toolbox.destroy();
+  gBrowser.removeCurrentTab();
+});
--- a/browser/devtools/framework/test/browser_toolbox_hosts.js
+++ b/browser/devtools/framework/test/browser_toolbox_hosts.js
@@ -1,130 +1,137 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-let temp = {}
-Cu.import("resource:///modules/devtools/gDevTools.jsm", temp);
-let DevTools = temp.DevTools;
+"use strict";
 
-Cu.import("resource://gre/modules/devtools/Loader.jsm", temp);
-let devtools = temp.devtools;
-
-let Toolbox = devtools.Toolbox;
-
+let {SIDE, BOTTOM, WINDOW} = devtools.Toolbox.HostType;
 let toolbox, target;
 
-function test()
-{
-  gBrowser.selectedTab = gBrowser.addTab();
-  target = TargetFactory.forTab(gBrowser.selectedTab);
+const URL = "data:text/html;charset=utf8,test for opening toolbox in different hosts";
+
+add_task(function* runTest() {
+  info("Create a test tab and open the toolbox");
+  let tab = yield addTab(URL);
+  target = TargetFactory.forTab(tab);
+  toolbox = yield gDevTools.showToolbox(target, "webconsole");
 
-  gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
-    gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
-    gDevTools.showToolbox(target)
-             .then(testBottomHost, console.error)
-             .then(null, console.error);
-  }, true);
+  yield testBottomHost();
+  yield testSidebarHost();
+  yield testWindowHost();
+  yield testToolSelect();
+  yield testDestroy();
+  yield testRememberHost();
+  yield testPreviousHost();
 
-  content.location = "data:text/html,test for opening toolbox in different hosts";
-}
+  yield toolbox.destroy();
 
-function testBottomHost(aToolbox)
-{
-  toolbox = aToolbox;
+  toolbox = target = null;
+  gBrowser.removeCurrentTab();
+});
 
-  checkHostType(Toolbox.HostType.BOTTOM);
+function* testBottomHost() {
+  checkHostType(toolbox, BOTTOM);
 
   // test UI presence
   let nbox = gBrowser.getNotificationBox();
   let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
   ok(iframe, "toolbox bottom iframe exists");
 
   checkToolboxLoaded(iframe);
-
-  toolbox.switchHost(Toolbox.HostType.SIDE).then(testSidebarHost);
 }
 
-function testSidebarHost()
-{
-  checkHostType(Toolbox.HostType.SIDE);
+function* testSidebarHost() {
+  yield toolbox.switchHost(SIDE);
+  checkHostType(toolbox, SIDE);
 
   // test UI presence
   let nbox = gBrowser.getNotificationBox();
   let bottom = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-bottom-iframe");
   ok(!bottom, "toolbox bottom iframe doesn't exist");
 
   let iframe = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
   ok(iframe, "toolbox side iframe exists");
 
   checkToolboxLoaded(iframe);
-
-  toolbox.switchHost(Toolbox.HostType.WINDOW).then(testWindowHost);
 }
 
-function testWindowHost()
-{
-  checkHostType(Toolbox.HostType.WINDOW);
+function* testWindowHost() {
+  yield toolbox.switchHost(WINDOW);
+  checkHostType(toolbox, WINDOW);
 
   let nbox = gBrowser.getNotificationBox();
   let sidebar = document.getAnonymousElementByAttribute(nbox, "class", "devtools-toolbox-side-iframe");
   ok(!sidebar, "toolbox sidebar iframe doesn't exist");
 
   let win = Services.wm.getMostRecentWindow("devtools:toolbox");
   ok(win, "toolbox separate window exists");
 
   let iframe = win.document.getElementById("toolbox-iframe");
   checkToolboxLoaded(iframe);
-
-  testToolSelect();
 }
 
-function testToolSelect()
-{
+function* testToolSelect() {
   // make sure we can load a tool after switching hosts
-  toolbox.selectTool("inspector").then(testDestroy);
+  yield toolbox.selectTool("inspector");
 }
 
-function testDestroy()
-{
-  toolbox.destroy().then(function() {
-    target = TargetFactory.forTab(gBrowser.selectedTab);
-    gDevTools.showToolbox(target).then(testRememberHost);
-  });
+function* testDestroy() {
+  yield toolbox.destroy();
+  target = TargetFactory.forTab(gBrowser.selectedTab);
+  toolbox = yield gDevTools.showToolbox(target);
 }
 
-function testRememberHost(aToolbox)
-{
-  toolbox = aToolbox;
+function* testRememberHost() {
   // last host was the window - make sure it's the same when re-opening
-  is(toolbox.hostType, Toolbox.HostType.WINDOW, "host remembered");
+  is(toolbox.hostType, WINDOW, "host remembered");
 
   let win = Services.wm.getMostRecentWindow("devtools:toolbox");
   ok(win, "toolbox separate window exists");
-
-  cleanup();
 }
 
-function checkHostType(hostType)
-{
-  is(toolbox.hostType, hostType, "host type is " + hostType);
+function* testPreviousHost() {
+  // last host was the window - make sure it's the same when re-opening
+  is(toolbox.hostType, WINDOW, "host remembered");
+
+  info("Switching to side");
+  yield toolbox.switchHost(SIDE);
+  checkHostType(toolbox, SIDE, WINDOW);
+
+  info("Switching to bottom");
+  yield toolbox.switchHost(BOTTOM);
+  checkHostType(toolbox, BOTTOM, SIDE);
+
+  info("Switching from bottom to side");
+  yield toolbox.switchToPreviousHost();
+  checkHostType(toolbox, SIDE, BOTTOM);
+
+  info("Switching from side to bottom");
+  yield toolbox.switchToPreviousHost();
+  checkHostType(toolbox, BOTTOM, SIDE);
 
-  let pref = Services.prefs.getCharPref("devtools.toolbox.host");
-  is(pref, hostType, "host pref is " + hostType);
+  info("Switching to window");
+  yield toolbox.switchHost(WINDOW);
+  checkHostType(toolbox, WINDOW, BOTTOM);
+
+  info("Switching from window to bottom");
+  yield toolbox.switchToPreviousHost();
+  checkHostType(toolbox, BOTTOM, WINDOW);
+
+  info("Forcing the previous host to match the current (bottom)")
+  Services.prefs.setCharPref("devtools.toolbox.previousHost", BOTTOM);
+
+  info("Switching from bottom to side (since previous=current=bottom");
+  yield toolbox.switchToPreviousHost();
+  checkHostType(toolbox, SIDE, BOTTOM);
+
+  info("Forcing the previous host to match the current (side)")
+  Services.prefs.setCharPref("devtools.toolbox.previousHost", SIDE);
+  info("Switching from side to bottom (since previous=current=side");
+  yield toolbox.switchToPreviousHost();
+  checkHostType(toolbox, BOTTOM, SIDE);
 }
 
-function checkToolboxLoaded(iframe)
-{
+function checkToolboxLoaded(iframe) {
   let tabs = iframe.contentDocument.getElementById("toolbox-tabs");
   ok(tabs, "toolbox UI has been loaded into iframe");
 }
-
-function cleanup()
-{
-  Services.prefs.setCharPref("devtools.toolbox.host", Toolbox.HostType.BOTTOM);
-
-  toolbox.destroy().then(function() {
-    DevTools = Toolbox = toolbox = target = null;
-    gBrowser.removeCurrentTab();
-    finish();
-  });
- }
--- a/browser/devtools/framework/test/browser_toolbox_window_reload_target.js
+++ b/browser/devtools/framework/test/browser_toolbox_window_reload_target.js
@@ -63,35 +63,27 @@ function testAllTheTools(docked, callbac
             testAllTheTools(docked, callback, toolNum+1);
           });
         });
       });
     });
   });
 }
 
-function synthesizeKeyForToolbox(keyId) {
-  let el = toolbox.doc.getElementById(keyId);
-  let key = el.getAttribute("key") || el.getAttribute("keycode");
-  let mod = {};
-  el.getAttribute("modifiers").split(" ").forEach((m) => mod[m+"Key"] = true);
-  info("Synthesizing: key="+key+", mod="+JSON.stringify(mod));
-  EventUtils.synthesizeKey(key, mod, toolbox.doc.defaultView);
-}
-
 function testReload(key, docked, toolID, callback) {
   let complete = () => {
     gBrowser.selectedBrowser.messageManager.removeMessageListener("devtools:test:load", complete);
     return callback();
   };
   gBrowser.selectedBrowser.messageManager.addMessageListener("devtools:test:load", complete);
 
   description = docked+" devtools with tool "+toolID+", key #" + key;
   info("Testing reload in "+description);
-  synthesizeKeyForToolbox(key);
+  let el = toolbox.doc.getElementById(key);
+  synthesizeKeyElement(el);
   reloadsSent++;
 }
 
 function finishUp() {
   toolbox.destroy().then(() => {
     gBrowser.removeCurrentTab();
 
     target = toolbox = description = reloadsSent = toolIDs = null;
--- a/browser/devtools/framework/test/head.js
+++ b/browser/devtools/framework/test/head.js
@@ -104,8 +104,41 @@ function executeInContent(name, data={},
 
   mm.sendAsyncMessage(name, data, objects);
   if (expectResponse) {
     return waitForContentMessage(name);
   } else {
     return promise.resolve();
   }
 }
+
+/**
+ * Synthesize a keypress from a <key> element, taking into account
+ * any modifiers.
+ * @param {Element} el the <key> element to synthesize
+ */
+function synthesizeKeyElement(el) {
+  let key = el.getAttribute("key") || el.getAttribute("keycode");
+  let mod = {};
+  el.getAttribute("modifiers").split(" ").forEach((m) => mod[m+"Key"] = true);
+  info(`Synthesizing: key=${key}, mod=${JSON.stringify(mod)}`);
+  EventUtils.synthesizeKey(key, mod, el.ownerDocument.defaultView);
+}
+
+/* Check the toolbox host type and prefs to make sure they match the
+ * expected values
+ * @param {Toolbox}
+ * @param {HostType} hostType
+ *        One of {SIDE, BOTTOM, WINDOW} from devtools.Toolbox.HostType
+ * @param {HostType} Optional previousHostType
+ *        The host that will be switched to when calling switchToPreviousHost
+ */
+function checkHostType(toolbox, hostType, previousHostType) {
+  is(toolbox.hostType, hostType, "host type is " + hostType);
+
+  let pref = Services.prefs.getCharPref("devtools.toolbox.host");
+  is(pref, hostType, "host pref is " + hostType);
+
+  if (previousHostType) {
+    is (Services.prefs.getCharPref("devtools.toolbox.previousHost"),
+      previousHostType, "The previous host is correct");
+  }
+}
--- a/browser/devtools/framework/test/shared-head.js
+++ b/browser/devtools/framework/test/shared-head.js
@@ -25,19 +25,21 @@ function getFrameScript() {
   mm.loadFrameScript(frameURL, false);
   SimpleTest.registerCleanupFunction(() => {
     mm = null;
   });
   return mm;
 }
 
 gDevTools.testing = true;
-SimpleTest.registerCleanupFunction(() => {
+registerCleanupFunction(() => {
   gDevTools.testing = false;
   Services.prefs.clearUserPref("devtools.dump.emit");
+  Services.prefs.clearUserPref("devtools.toolbox.host");
+  Services.prefs.clearUserPref("devtools.toolbox.previousHost");
 });
 
 registerCleanupFunction(function cleanup() {
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
 });
 
--- a/browser/devtools/framework/toolbox.js
+++ b/browser/devtools/framework/toolbox.js
@@ -183,17 +183,18 @@ Toolbox.HostType = {
 };
 
 Toolbox.prototype = {
   _URL: "chrome://browser/content/devtools/framework/toolbox.xul",
 
   _prefs: {
     LAST_HOST: "devtools.toolbox.host",
     LAST_TOOL: "devtools.toolbox.selectedTool",
-    SIDE_ENABLED: "devtools.toolbox.sideEnabled"
+    SIDE_ENABLED: "devtools.toolbox.sideEnabled",
+    PREVIOUS_HOST: "devtools.toolbox.previousHost"
   },
 
   currentToolId: null,
 
   /**
    * Returns a *copy* of the _toolPanels collection.
    *
    * @return {Map} panels
@@ -492,22 +493,26 @@ Toolbox.prototype = {
         this.reloadTarget(force);
       }, true);
     });
   },
 
   _addHostListeners: function() {
     let nextKey = this.doc.getElementById("toolbox-next-tool-key");
     nextKey.addEventListener("command", this.selectNextTool.bind(this), true);
+
     let prevKey = this.doc.getElementById("toolbox-previous-tool-key");
     prevKey.addEventListener("command", this.selectPreviousTool.bind(this), true);
 
     let minimizeKey = this.doc.getElementById("toolbox-minimize-key");
     minimizeKey.addEventListener("command", this._toggleMinimizeMode, true);
 
+    let toggleKey = this.doc.getElementById("toolbox-toggle-host-key");
+    toggleKey.addEventListener("command", this.switchToPreviousHost.bind(this), true);
+
     // Split console uses keypress instead of command so the event can be
     // cancelled with stopPropagation on the keypress, and not preventDefault.
     this.doc.addEventListener("keypress", this._splitConsoleOnKeypress, false);
 
     this.doc.addEventListener("focus", this._onFocus, true);
   },
 
   _saveSplitConsoleHeight: function() {
@@ -1575,16 +1580,36 @@ Toolbox.prototype = {
 
     // clean up the toolbox if its window is closed
     let newHost = new Hosts[hostType](this.target.tab, options);
     newHost.on("window-closed", this.destroy);
     return newHost;
   },
 
   /**
+   * Switch to the last used host for the toolbox UI.
+   * This is determined by the devtools.toolbox.previousHost pref.
+   */
+  switchToPreviousHost: function() {
+    let hostType = Services.prefs.getCharPref(this._prefs.PREVIOUS_HOST);
+
+    // Handle the case where the previous host happens to match the current
+    // host. If so, switch to bottom if it's not already used, and side if not.
+    if (hostType === this._host.type) {
+      if (hostType === Toolbox.HostType.BOTTOM) {
+        hostType = Toolbox.HostType.SIDE;
+      } else {
+        hostType = Toolbox.HostType.BOTTOM;
+      }
+    }
+
+    return this.switchHost(hostType);
+  },
+
+  /**
    * Switch to a new host for the toolbox UI. E.g. bottom, sidebar, window,
    * and focus the window when done.
    *
    * @param {string} hostType
    *        The host type of the new host object
    */
   switchHost: function(hostType) {
     if (hostType == this._host.type || !this._target.isLocalTab) {
@@ -1602,20 +1627,22 @@ Toolbox.prototype = {
       // See bug 1022726, most probably because of swapFrameLoaders we need to
       // first focus the window here, and then once again further below to make
       // sure focus actually happens.
       this.frame.contentWindow.focus();
 
       this._host.off("window-closed", this.destroy);
       this.destroyHost();
 
+      let prevHostType = this._host.type;
       this._host = newHost;
 
       if (this.hostType != Toolbox.HostType.CUSTOM) {
         Services.prefs.setCharPref(this._prefs.LAST_HOST, this._host.type);
+        Services.prefs.setCharPref(this._prefs.PREVIOUS_HOST, prevHostType);
       }
 
       this._buildDockButtons();
       this._addKeysToWindow();
 
       // Focus the contentWindow to make sure keyboard shortcuts work straight
       // away.
       this.frame.contentWindow.focus();
--- a/browser/devtools/framework/toolbox.xul
+++ b/browser/devtools/framework/toolbox.xul
@@ -69,16 +69,20 @@
     <key id="toolbox-force-reload-key2"
          keycode="VK_F5"
          oncommand="void(0);"
          modifiers="accel"/>
     <key id="toolbox-minimize-key"
          key="&toolboxToggleMinimize.key;"
          oncommand="void(0);"
          modifiers="shift, accel"/>
+    <key id="toolbox-toggle-host-key"
+         key="&toolboxToggle.key;"
+         oncommand="void(0);"
+         modifiers="accel shift"/>
   </keyset>
 
   <popupset>
     <menupopup id="toolbox-textbox-context-popup">
       <menuitem id="cMenu_undo"/>
       <menuseparator/>
       <menuitem id="cMenu_cut"/>
       <menuitem id="cMenu_copy"/>
--- a/browser/devtools/scratchpad/scratchpad.js
+++ b/browser/devtools/scratchpad/scratchpad.js
@@ -259,28 +259,43 @@ var Scratchpad = {
 
   /**
    * Check or uncheck view menu items according to stored preferences.
    */
   _updateViewMenuItems: function SP_updateViewMenuItems() {
     this._updateViewMenuItem(SHOW_LINE_NUMBERS, "sp-menu-line-numbers");
     this._updateViewMenuItem(WRAP_TEXT, "sp-menu-word-wrap");
     this._updateViewMenuItem(SHOW_TRAILING_SPACE, "sp-menu-highlight-trailing-space");
+    this._updateViewFontMenuItem(MINIMUM_FONT_SIZE, "sp-cmd-smaller-font");
+    this._updateViewFontMenuItem(MAXIMUM_FONT_SIZE, "sp-cmd-larger-font");
   },
 
+  /**
+   * Check or uncheck view menu item according to stored preferences.
+   */
   _updateViewMenuItem: function SP_updateViewMenuItem(preferenceName, menuId) {
     let checked = Services.prefs.getBoolPref(preferenceName);
     if (checked) {
         document.getElementById(menuId).setAttribute('checked', true);
     } else {
         document.getElementById(menuId).removeAttribute('checked');
     }
   },
 
   /**
+   * Disable view menu item if the stored font size is equals to the given one.
+   */
+  _updateViewFontMenuItem: function SP_updateViewFontMenuItem(fontSize, commandId) {
+    let prefFontSize = Services.prefs.getIntPref(EDITOR_FONT_SIZE);
+    if (prefFontSize === fontSize) {
+      document.getElementById(commandId).setAttribute('disabled', true);
+    }
+  },
+
+  /**
    * The script execution context. This tells Scratchpad in which context the
    * script shall execute.
    *
    * Possible values:
    *   - SCRATCHPAD_CONTEXT_CONTENT to execute code in the context of the current
    *   tab content window object.
    *   - SCRATCHPAD_CONTEXT_BROWSER to execute code in the context of the
    *   currently active chrome window object.
@@ -1934,40 +1949,55 @@ var Scratchpad = {
   increaseFontSize: function SP_increaseFontSize()
   {
     let size = this.editor.getFontSize();
 
     if (size < MAXIMUM_FONT_SIZE) {
       let newFontSize = size + 1;
       this.editor.setFontSize(newFontSize);
       Services.prefs.setIntPref(EDITOR_FONT_SIZE, newFontSize);
+
+      if (newFontSize === MAXIMUM_FONT_SIZE) {
+        document.getElementById("sp-cmd-larger-font").setAttribute('disabled', true);
+      }
+
+      document.getElementById("sp-cmd-smaller-font").removeAttribute('disabled');
     }
   },
 
   /**
    * Decrease the editor's font size by 1 px.
    */
   decreaseFontSize: function SP_decreaseFontSize()
   {
     let size = this.editor.getFontSize();
 
     if (size > MINIMUM_FONT_SIZE) {
       let newFontSize = size - 1;
       this.editor.setFontSize(newFontSize);
       Services.prefs.setIntPref(EDITOR_FONT_SIZE, newFontSize);
+
+      if (newFontSize === MINIMUM_FONT_SIZE) {
+        document.getElementById("sp-cmd-smaller-font").setAttribute('disabled', true);
+      }
     }
+
+    document.getElementById("sp-cmd-larger-font").removeAttribute('disabled');
   },
 
   /**
    * Restore the editor's original font size.
    */
   normalFontSize: function SP_normalFontSize()
   {
     this.editor.setFontSize(NORMAL_FONT_SIZE);
     Services.prefs.setIntPref(EDITOR_FONT_SIZE, NORMAL_FONT_SIZE);
+
+    document.getElementById("sp-cmd-larger-font").removeAttribute('disabled');
+    document.getElementById("sp-cmd-smaller-font").removeAttribute('disabled');
   },
 
   _observers: [],
 
   /**
    * Add an observer for Scratchpad events.
    *
    * The observer implements IScratchpadObserver := {
--- a/browser/devtools/scratchpad/test/browser.ini
+++ b/browser/devtools/scratchpad/test/browser.ini
@@ -39,8 +39,9 @@ support-files = NS_ERROR_ILLEGAL_INPUT.t
 [browser_scratchpad_pprint-02.js]
 [browser_scratchpad_pprint.js]
 [browser_scratchpad_pprint_error_goto_line.js]
 [browser_scratchpad_restore.js]
 [browser_scratchpad_tab_switch.js]
 [browser_scratchpad_ui.js]
 [browser_scratchpad_close_toolbox.js]
 [browser_scratchpad_remember_view_options.js]
+[browser_scratchpad_disable_view_menu_items.js]
new file mode 100644
--- /dev/null
+++ b/browser/devtools/scratchpad/test/browser_scratchpad_disable_view_menu_items.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test if the view menu items "Larger Font" and "Smaller Font" are disabled
+// when the font size reaches the maximum/minimum values.
+
+let {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
+
+function test() {
+  const options = {
+    tabContent: 'test if view menu items "Larger Font" and "Smaller Font" are enabled/disabled.'
+  };
+  openTabAndScratchpad(options)
+    .then(Task.async(runTests))
+    .then(finish, console.error);
+}
+
+function* runTests([win, sp]) {
+  yield testMaximumFontSize(win, sp);
+
+  yield testMinimumFontSize(win, sp);
+}
+
+const MAXIMUM_FONT_SIZE = 96;
+const MINIMUM_FONT_SIZE = 6;
+const NORMAL_FONT_SIZE = 12;
+
+let testMaximumFontSize = Task.async(function* (win, sp) {
+  let doc = win.document;
+
+  Services.prefs.clearUserPref('devtools.scratchpad.editorFontSize');
+
+  let menu = doc.getElementById('sp-menu-larger-font');
+
+  for (let i = NORMAL_FONT_SIZE; i <= MAXIMUM_FONT_SIZE; i++) {
+    menu.doCommand();
+  }
+
+  let cmd = doc.getElementById('sp-cmd-larger-font');
+  ok(cmd.getAttribute('disabled') === 'true', 'Command "sp-cmd-larger-font" is disabled.');
+
+  menu = doc.getElementById('sp-menu-smaller-font');
+  menu.doCommand();
+
+  ok(cmd.hasAttribute('disabled') === false, 'Command "sp-cmd-larger-font" is enabled.');
+});
+
+let testMinimumFontSize = Task.async(function* (win, sp) {
+  let doc = win.document;
+
+  let menu = doc.getElementById('sp-menu-smaller-font');
+
+  for (let i = MAXIMUM_FONT_SIZE; i >= MINIMUM_FONT_SIZE; i--) {
+    menu.doCommand();
+  }
+
+  let cmd = doc.getElementById('sp-cmd-smaller-font');
+  ok(cmd.getAttribute('disabled') === 'true', 'Command "sp-cmd-smaller-font" is disabled.');
+
+  menu = doc.getElementById('sp-menu-larger-font');
+  menu.doCommand();
+
+  ok(cmd.hasAttribute('disabled') === false, 'Command "sp-cmd-smaller-font" is enabled.');
+
+  Services.prefs.clearUserPref('devtools.scratchpad.editorFontSize');
+});
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
+++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd
@@ -19,16 +19,17 @@
 <!ENTITY toolboxZoomIn.key2            "="> <!-- + is above this key on many keyboards -->
 <!ENTITY toolboxZoomOut.key            "-">
 <!ENTITY toolboxZoomReset.key          "0">
 
 <!ENTITY toolboxReload.key             "r">
 <!-- This key is used with the accel+shift modifiers to minimize the toolbox -->
 <!ENTITY toolboxToggleMinimize.key     "U">
 
+<!ENTITY toolboxToggle.key             "d">
 <!-- LOCALIZATION NOTE (toolboxFramesButton): This is the label for
   -  the iframes menu list that appears only when the document has some.
   -  It allows you to switch the context of the whole toolbox. -->
 <!ENTITY toolboxFramesTooltip          "Select an iframe as the currently targeted document">
 
 <!-- LOCALIZATION NOTE (browserToolboxErrorMessage): This is the label
   -  shown next to error details when the Browser Toolbox is unable to open. -->
 <!ENTITY browserToolboxErrorMessage          "Error opening Browser Toolbox:">
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -675,27 +675,27 @@ toolbarbutton[constrain-size="true"][cui
 
 :-moz-any(#TabsToolbar, #nav-bar) .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
   -moz-margin-start: -4px;
   margin-top: 3px;
   margin-bottom: 3px;
 }
 
 #back-button {
-  margin-top: 3px;
-  margin-bottom: 3px;
-  -moz-margin-start: 5px;
-  padding: 0;
+  padding-top: 3px;
+  padding-bottom: 3px;
+  -moz-padding-start: 5px;
+  -moz-padding-end: 0;
   position: relative;
   z-index: 1;
-  border-radius: 10000px;
+  border-radius: 0 10000px 10000px 0;
 }
 
-#back-button:not(:-moz-lwtheme) {
-  background-color: -moz-dialog;
+#back-button:-moz-locale-dir(rtl) {
+  border-radius: 10000px 0 0 10000px;
 }
 
 #back-button > menupopup {
   margin-top: -1px;
 }
 
 #back-button > .toolbarbutton-icon {
   border-radius: 10000px;
@@ -889,27 +889,23 @@ toolbarbutton[constrain-size="true"][cui
 }
 
 @conditionalForwardWithUrlbar@ > #urlbar:-moz-locale-dir(rtl) {
   border-top-right-radius: 0;
   border-bottom-right-radius: 0;
 }
 
 @conditionalForwardWithUrlbar@ {
-  clip-path: url("chrome://browser/content/browser.xul#urlbar-clip-path");
+  clip-path: url("chrome://browser/content/browser.xul#urlbar-back-button-clip-path");
   -moz-margin-start: -5px;
 }
 
-@conditionalForwardWithUrlbar@:-moz-lwtheme {
-  clip-path: url("chrome://browser/content/browser.xul#urlbar-back-button-clip-path");
-}
-
 @conditionalForwardWithUrlbar@:-moz-locale-dir(rtl),
 @conditionalForwardWithUrlbar@ > #urlbar:-moz-locale-dir(rtl) {
-  /* Let clip-path clip the urlbar-wrapper's right side for RTL. */
+  /* let urlbar-back-button-clip-path clip the urlbar's right side for RTL */
   transform: scaleX(-1);
 }
 
 @conditionalForwardWithUrlbar@:-moz-locale-dir(rtl) {
   -moz-box-direction: reverse;
 }
 
 #urlbar-icons {
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -112,25 +112,34 @@ browser.jar:
   skin/classic/browser/tab-crashed.svg                      (../shared/incontent-icons/tab-crashed.svg)
   skin/classic/browser/welcome-back.svg                     (../shared/incontent-icons/welcome-back.svg)
   skin/classic/browser/reader-tour.png                      (../shared/reader/reader-tour.png)
   skin/classic/browser/reader-tour@2x.png                   (../shared/reader/reader-tour@2x.png)
   skin/classic/browser/readerMode.svg                       (../shared/reader/readerMode.svg)
   skin/classic/browser/readinglist/icons.svg          (../shared/readinglist/icons.svg)
   skin/classic/browser/readinglist/readinglist-icon.svg (../shared/readinglist/readinglist-icon.svg)
 * skin/classic/browser/readinglist/sidebar.css        (readinglist/sidebar.css)
-  skin/classic/browser/webRTC-shareDevice-16.png
-  skin/classic/browser/webRTC-shareDevice-64.png
+  skin/classic/browser/webRTC-shareDevice-16.png      (../shared/webrtc/webRTC-shareDevice-16.png)
+  skin/classic/browser/webRTC-shareDevice-16@2x.png   (../shared/webrtc/webRTC-shareDevice-16@2x.png)
+  skin/classic/browser/webRTC-shareDevice-64.png      (../shared/webrtc/webRTC-shareDevice-64.png)
+  skin/classic/browser/webRTC-shareDevice-64@2x.png   (../shared/webrtc/webRTC-shareDevice-64@2x.png)
   skin/classic/browser/webRTC-sharingDevice-16.png    (../shared/webrtc/webRTC-sharingDevice-16.png)
-  skin/classic/browser/webRTC-shareMicrophone-16.png
-  skin/classic/browser/webRTC-shareMicrophone-64.png
+  skin/classic/browser/webRTC-sharingDevice-16@2x.png (../shared/webrtc/webRTC-sharingDevice-16@2x.png)
+  skin/classic/browser/webRTC-shareMicrophone-16.png  (../shared/webrtc/webRTC-shareMicrophone-16.png)
+  skin/classic/browser/webRTC-shareMicrophone-16@2x.png (../shared/webrtc/webRTC-shareMicrophone-16@2x.png)
+  skin/classic/browser/webRTC-shareMicrophone-64.png  (../shared/webrtc/webRTC-shareMicrophone-64.png)
+  skin/classic/browser/webRTC-shareMicrophone-64@2x.png (../shared/webrtc/webRTC-shareMicrophone-64@2x.png)
   skin/classic/browser/webRTC-sharingMicrophone-16.png (../shared/webrtc/webRTC-sharingMicrophone-16.png)
+  skin/classic/browser/webRTC-sharingMicrophone-16@2x.png (../shared/webrtc/webRTC-sharingMicrophone-16@2x.png)
   skin/classic/browser/webRTC-shareScreen-16.png      (../shared/webrtc/webRTC-shareScreen-16.png)
+  skin/classic/browser/webRTC-shareScreen-16@2x.png   (../shared/webrtc/webRTC-shareScreen-16@2x.png)
   skin/classic/browser/webRTC-shareScreen-64.png      (../shared/webrtc/webRTC-shareScreen-64.png)
+  skin/classic/browser/webRTC-shareScreen-64@2x.png   (../shared/webrtc/webRTC-shareScreen-64@2x.png)
   skin/classic/browser/webRTC-sharingScreen-16.png    (../shared/webrtc/webRTC-sharingScreen-16.png)
+  skin/classic/browser/webRTC-sharingScreen-16@2x.png (../shared/webrtc/webRTC-sharingScreen-16@2x.png)
   skin/classic/browser/webRTC-indicator.css           (../shared/webrtc/indicator.css)
   skin/classic/browser/webRTC-camera-white-16.png     (../shared/webrtc/camera-white-16.png)
   skin/classic/browser/webRTC-microphone-white-16.png (../shared/webrtc/microphone-white-16.png)
   skin/classic/browser/webRTC-screen-white-16.png     (../shared/webrtc/screen-white-16.png)
   skin/classic/browser/loop/menuPanel.png             (loop/menuPanel.png)
   skin/classic/browser/loop/menuPanel@2x.png          (loop/menuPanel@2x.png)
   skin/classic/browser/loop/toolbar.png               (loop/toolbar.png)
   skin/classic/browser/loop/toolbar@2x.png            (loop/toolbar@2x.png)
deleted file mode 100644
index 8bc5b3acaed5f864dd06c36ae9350dd15524df9a..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index d125789fbce71eff4c4d5af890dd5ed6a862c50e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index ac67cdbed3a9b7e43456b1ba6b2dc7d8501366f2..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 6bb5a8880c93a61e6b95807209c509bd8648c04a..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -150,26 +150,26 @@ browser.jar:
   skin/classic/browser/tab-crashed.svg                (../shared/incontent-icons/tab-crashed.svg)
   skin/classic/browser/welcome-back.svg               (../shared/incontent-icons/welcome-back.svg)
   skin/classic/browser/reader-tour.png                (../shared/reader/reader-tour.png)
   skin/classic/browser/reader-tour@2x.png             (../shared/reader/reader-tour@2x.png)
   skin/classic/browser/readerMode.svg                 (../shared/reader/readerMode.svg)
   skin/classic/browser/readinglist/icons.svg          (../shared/readinglist/icons.svg)
   skin/classic/browser/readinglist/readinglist-icon.svg (../shared/readinglist/readinglist-icon.svg)
 * skin/classic/browser/readinglist/sidebar.css        (readinglist/sidebar.css)
-  skin/classic/browser/webRTC-shareDevice-16.png
-  skin/classic/browser/webRTC-shareDevice-16@2x.png
-  skin/classic/browser/webRTC-shareDevice-64.png
-  skin/classic/browser/webRTC-shareDevice-64@2x.png
+  skin/classic/browser/webRTC-shareDevice-16.png      (../shared/webrtc/webRTC-shareDevice-16.png)
+  skin/classic/browser/webRTC-shareDevice-16@2x.png   (../shared/webrtc/webRTC-shareDevice-16@2x.png)
+  skin/classic/browser/webRTC-shareDevice-64.png      (../shared/webrtc/webRTC-shareDevice-64.png)
+  skin/classic/browser/webRTC-shareDevice-64@2x.png   (../shared/webrtc/webRTC-shareDevice-64@2x.png)
   skin/classic/browser/webRTC-sharingDevice-16.png    (../shared/webrtc/webRTC-sharingDevice-16.png)
   skin/classic/browser/webRTC-sharingDevice-16@2x.png (../shared/webrtc/webRTC-sharingDevice-16@2x.png)
-  skin/classic/browser/webRTC-shareMicrophone-16.png
-  skin/classic/browser/webRTC-shareMicrophone-16@2x.png
-  skin/classic/browser/webRTC-shareMicrophone-64.png
-  skin/classic/browser/webRTC-shareMicrophone-64@2x.png
+  skin/classic/browser/webRTC-shareMicrophone-16.png  (../shared/webrtc/webRTC-shareMicrophone-16.png)
+  skin/classic/browser/webRTC-shareMicrophone-16@2x.png (../shared/webrtc/webRTC-shareMicrophone-16@2x.png)
+  skin/classic/browser/webRTC-shareMicrophone-64.png  (../shared/webrtc/webRTC-shareMicrophone-64.png)
+  skin/classic/browser/webRTC-shareMicrophone-64@2x.png (../shared/webrtc/webRTC-shareMicrophone-64@2x.png)
   skin/classic/browser/webRTC-sharingMicrophone-16.png (../shared/webrtc/webRTC-sharingMicrophone-16.png)
   skin/classic/browser/webRTC-sharingMicrophone-16@2x.png (../shared/webrtc/webRTC-sharingMicrophone-16@2x.png)
   skin/classic/browser/webRTC-shareScreen-16.png      (../shared/webrtc/webRTC-shareScreen-16.png)
   skin/classic/browser/webRTC-shareScreen-16@2x.png   (../shared/webrtc/webRTC-shareScreen-16@2x.png)
   skin/classic/browser/webRTC-shareScreen-64.png      (../shared/webrtc/webRTC-shareScreen-64.png)
   skin/classic/browser/webRTC-shareScreen-64@2x.png   (../shared/webrtc/webRTC-shareScreen-64@2x.png)
   skin/classic/browser/webRTC-sharingScreen-16.png    (../shared/webrtc/webRTC-sharingScreen-16.png)
   skin/classic/browser/webRTC-sharingScreen-16@2x.png (../shared/webrtc/webRTC-sharingScreen-16@2x.png)
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -313,16 +313,61 @@
 }
 
 /* HiDPI notification icons */
 @media (min-resolution: 1.1dppx) {
   #notification-popup-box {
     border-image: url("chrome://browser/skin/urlbar-arrow@2x.png") 0 16 0 0 fill;
   }
 
+  .webRTC-shareDevices-notification-icon,
+  #webRTC-shareDevices-notification-icon {
+    list-style-image: url(chrome://browser/skin/webRTC-shareDevice-16@2x.png);
+  }
+
+  .webRTC-sharingDevices-notification-icon,
+  #webRTC-sharingDevices-notification-icon {
+    list-style-image: url(chrome://browser/skin/webRTC-sharingDevice-16@2x.png);
+  }
+
+  .webRTC-shareMicrophone-notification-icon,
+  #webRTC-shareMicrophone-notification-icon {
+    list-style-image: url(chrome://browser/skin/webRTC-shareMicrophone-16@2x.png);
+  }
+
+  .webRTC-sharingMicrophone-notification-icon,
+  #webRTC-sharingMicrophone-notification-icon {
+    list-style-image: url(chrome://browser/skin/webRTC-sharingMicrophone-16@2x.png);
+  }
+
+  .webRTC-shareScreen-notification-icon,
+  #webRTC-shareScreen-notification-icon {
+    list-style-image: url(chrome://browser/skin/webRTC-shareScreen-16@2x.png);
+  }
+
+  .webRTC-sharingScreen-notification-icon,
+  #webRTC-sharingScreen-notification-icon {
+    list-style-image: url(chrome://browser/skin/webRTC-sharingScreen-16@2x.png);
+  }
+
+  .popup-notification-icon[popupid="webRTC-sharingDevices"],
+  .popup-notification-icon[popupid="webRTC-shareDevices"] {
+    list-style-image: url(chrome://browser/skin/webRTC-shareDevice-64@2x.png);
+  }
+
+  .popup-notification-icon[popupid="webRTC-sharingMicrophone"],
+  .popup-notification-icon[popupid="webRTC-shareMicrophone"] {
+    list-style-image: url(chrome://browser/skin/webRTC-shareMicrophone-64@2x.png);
+  }
+
+  .popup-notification-icon[popupid="webRTC-sharingScreen"],
+  .popup-notification-icon[popupid="webRTC-shareScreen"] {
+    list-style-image: url(chrome://browser/skin/webRTC-shareScreen-64@2x.png);
+  }
+
 %ifdef XP_MACOSX
 /* OSX only until we have icons for Windows and Linux */
   .default-notification-icon,
   #default-notification-icon {
     list-style-image: url(chrome://global/skin/icons/information-32.png);
   }
 
   .geo-notification-icon,
@@ -376,46 +421,16 @@
   #bad-content-blocked-notification-icon {
     list-style-image: url(chrome://browser/skin/bad-content-blocked-16@2x.png);
   }
 
   #bad-content-unblocked-notification-icon {
     list-style-image: url(chrome://browser/skin/bad-content-unblocked-16@2x.png);
   }
 
-  .webRTC-shareDevices-notification-icon,
-  #webRTC-shareDevices-notification-icon {
-    list-style-image: url(chrome://browser/skin/webRTC-shareDevice-16@2x.png);
-  }
-
-  .webRTC-sharingDevices-notification-icon,
-  #webRTC-sharingDevices-notification-icon {
-    list-style-image: url(chrome://browser/skin/webRTC-sharingDevice-16@2x.png);
-  }
-
-  .webRTC-shareMicrophone-notification-icon,
-  #webRTC-shareMicrophone-notification-icon {
-    list-style-image: url(chrome://browser/skin/webRTC-shareMicrophone-16@2x.png);
-  }
-
-  .webRTC-sharingMicrophone-notification-icon,
-  #webRTC-sharingMicrophone-notification-icon {
-    list-style-image: url(chrome://browser/skin/webRTC-sharingMicrophone-16@2x.png);
-  }
-
-  .webRTC-shareScreen-notification-icon,
-  #webRTC-shareScreen-notification-icon {
-    list-style-image: url(chrome://browser/skin/webRTC-shareScreen-16@2x.png);
-  }
-
-  .webRTC-sharingScreen-notification-icon,
-  #webRTC-sharingScreen-notification-icon {
-    list-style-image: url(chrome://browser/skin/webRTC-sharingScreen-16@2x.png);
-  }
-
   .web-notifications-notification-icon,
   #web-notifications-notification-icon {
     list-style-image: url(chrome://browser/skin/notification-16@2x.png);
   }
 
   .pointerLock-notification-icon,
   #pointerLock-notification-icon {
     list-style-image: url(chrome://browser/skin/pointerLock-16@2x.png);
@@ -453,31 +468,16 @@
   .popup-notification-icon[popupid="bad-content"][trackingblockdisabled] {
     list-style-image: url(chrome://browser/skin/bad-content-unblocked-64@2x.png);
   }
 
   .popup-notification-icon[popupid="pointerLock"] {
     list-style-image: url(chrome://browser/skin/pointerLock-64@2x.png);
   }
 
-  .popup-notification-icon[popupid="webRTC-sharingDevices"],
-  .popup-notification-icon[popupid="webRTC-shareDevices"] {
-    list-style-image: url(chrome://browser/skin/webRTC-shareDevice-64@2x.png);
-  }
-
-  .popup-notification-icon[popupid="webRTC-sharingMicrophone"],
-  .popup-notification-icon[popupid="webRTC-shareMicrophone"] {
-    list-style-image: url(chrome://browser/skin/webRTC-shareMicrophone-64@2x.png);
-  }
-
-  .popup-notification-icon[popupid="webRTC-sharingScreen"],
-  .popup-notification-icon[popupid="webRTC-shareScreen"] {
-    list-style-image: url(chrome://browser/skin/webRTC-shareScreen-64@2x.png);
-  }
-
   .popup-notification-icon[popupid="servicesInstall"] {
     list-style-image: url(chrome://browser/skin/social/services-64@2x.png);
   }
 
   #servicesInstall-notification-icon {
     list-style-image: url(chrome://browser/skin/social/services-16@2x.png);
   }
 %endif
rename from browser/themes/osx/webRTC-shareDevice-16.png
rename to browser/themes/shared/webrtc/webRTC-shareDevice-16.png
rename from browser/themes/osx/webRTC-shareDevice-16@2x.png
rename to browser/themes/shared/webrtc/webRTC-shareDevice-16@2x.png
rename from browser/themes/osx/webRTC-shareDevice-64.png
rename to browser/themes/shared/webrtc/webRTC-shareDevice-64.png
rename from browser/themes/osx/webRTC-shareDevice-64@2x.png
rename to browser/themes/shared/webrtc/webRTC-shareDevice-64@2x.png
rename from browser/themes/osx/webRTC-shareMicrophone-16.png
rename to browser/themes/shared/webrtc/webRTC-shareMicrophone-16.png
rename from browser/themes/osx/webRTC-shareMicrophone-16@2x.png
rename to browser/themes/shared/webrtc/webRTC-shareMicrophone-16@2x.png
rename from browser/themes/osx/webRTC-shareMicrophone-64.png
rename to browser/themes/shared/webrtc/webRTC-shareMicrophone-64.png
rename from browser/themes/osx/webRTC-shareMicrophone-64@2x.png
rename to browser/themes/shared/webrtc/webRTC-shareMicrophone-64@2x.png
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -150,25 +150,34 @@ browser.jar:
         skin/classic/browser/reader-tour@2x.png                      (../shared/reader/reader-tour@2x.png)
         skin/classic/browser/readerMode.svg                          (../shared/reader/readerMode.svg)
         skin/classic/browser/readinglist/icons.svg                   (../shared/readinglist/icons.svg)
         skin/classic/browser/readinglist/readinglist-icon.svg        (../shared/readinglist/readinglist-icon.svg)
 *       skin/classic/browser/readinglist/sidebar.css                 (readinglist/sidebar.css)
         skin/classic/browser/notification-pluginNormal.png           (../shared/plugins/notification-pluginNormal.png)
         skin/classic/browser/notification-pluginAlert.png            (../shared/plugins/notification-pluginAlert.png)
         skin/classic/browser/notification-pluginBlocked.png          (../shared/plugins/notification-pluginBlocked.png)
-        skin/classic/browser/webRTC-shareDevice-16.png
-        skin/classic/browser/webRTC-shareDevice-64.png
+        skin/classic/browser/webRTC-shareDevice-16.png               (../shared/webrtc/webRTC-shareDevice-16.png)
+        skin/classic/browser/webRTC-shareDevice-16@2x.png            (../shared/webrtc/webRTC-shareDevice-16@2x.png)
+        skin/classic/browser/webRTC-shareDevice-64.png               (../shared/webrtc/webRTC-shareDevice-64.png)
+        skin/classic/browser/webRTC-shareDevice-64@2x.png            (../shared/webrtc/webRTC-shareDevice-64@2x.png)
         skin/classic/browser/webRTC-sharingDevice-16.png             (../shared/webrtc/webRTC-sharingDevice-16.png)
-        skin/classic/browser/webRTC-shareMicrophone-16.png
-        skin/classic/browser/webRTC-shareMicrophone-64.png
+        skin/classic/browser/webRTC-sharingDevice-16@2x.png          (../shared/webrtc/webRTC-sharingDevice-16@2x.png)
+        skin/classic/browser/webRTC-shareMicrophone-16.png           (../shared/webrtc/webRTC-shareMicrophone-16.png)
+        skin/classic/browser/webRTC-shareMicrophone-16@2x.png        (../shared/webrtc/webRTC-shareMicrophone-16@2x.png)
+        skin/classic/browser/webRTC-shareMicrophone-64.png           (../shared/webrtc/webRTC-shareMicrophone-64.png)
+        skin/classic/browser/webRTC-shareMicrophone-64@2x.png        (../shared/webrtc/webRTC-shareMicrophone-64@2x.png)
         skin/classic/browser/webRTC-sharingMicrophone-16.png         (../shared/webrtc/webRTC-sharingMicrophone-16.png)
+        skin/classic/browser/webRTC-sharingMicrophone-16@2x.png      (../shared/webrtc/webRTC-sharingMicrophone-16@2x.png)
         skin/classic/browser/webRTC-shareScreen-16.png               (../shared/webrtc/webRTC-shareScreen-16.png)
+        skin/classic/browser/webRTC-shareScreen-16@2x.png            (../shared/webrtc/webRTC-shareScreen-16@2x.png)
         skin/classic/browser/webRTC-shareScreen-64.png               (../shared/webrtc/webRTC-shareScreen-64.png)
+        skin/classic/browser/webRTC-shareScreen-64@2x.png            (../shared/webrtc/webRTC-shareScreen-64@2x.png)
         skin/classic/browser/webRTC-sharingScreen-16.png             (../shared/webrtc/webRTC-sharingScreen-16.png)
+        skin/classic/browser/webRTC-sharingScreen-16@2x.png          (../shared/webrtc/webRTC-sharingScreen-16@2x.png)
         skin/classic/browser/webRTC-indicator.css                    (../shared/webrtc/indicator.css)
         skin/classic/browser/webRTC-camera-white-16.png              (../shared/webrtc/camera-white-16.png)
         skin/classic/browser/webRTC-microphone-white-16.png          (../shared/webrtc/microphone-white-16.png)
         skin/classic/browser/webRTC-screen-white-16.png              (../shared/webrtc/screen-white-16.png)
         skin/classic/browser/loop/menuPanel.png                      (loop/menuPanel.png)
         skin/classic/browser/loop/menuPanel@2x.png                   (loop/menuPanel@2x.png)
         skin/classic/browser/loop/menuPanel-aero.png                 (loop/menuPanel-aero.png)
         skin/classic/browser/loop/menuPanel-aero@2x.png              (loop/menuPanel-aero@2x.png)
deleted file mode 100644
index df01b33515889863c9ef99d0daf650b13a104f2d..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 0982941669352ea9335b10a43b240e176daec8f9..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index f50726fd673b579c8e21b6174d06e4b0f81f949c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 73e299b93c6ebb396584f40cd5817d9dda7756dc..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/layout/base/PositionedEventTargeting.cpp
+++ b/layout/base/PositionedEventTargeting.cpp
@@ -382,21 +382,24 @@ GetClosest(nsIFrame* aRoot, const nsPoin
       bestTarget = f;
     }
   }
   return bestTarget;
 }
 
 /*
  * Return always true when touch cluster detection is OFF.
- * When cluster detection is ON, return true if the text inside
- * the frame is readable (by human eyes):
- *   in this case, the frame is really clickable.
- * Frames with a too small size will return false:
- *   in this case, the frame is considered not clickable.
+ * When cluster detection is ON, return true:
+ *   if the text inside the frame is readable (by human eyes)
+ *   or
+ *   if the structure is too complex to determine the size.
+ * In both cases, the frame is considered as clickable.
+ *
+ * Frames with a too small size will return false.
+ * In this case, the frame is considered not clickable.
  */
 static bool
 IsElementClickableAndReadable(nsIFrame* aFrame, WidgetGUIEvent* aEvent, const EventRadiusPrefs* aPrefs)
 {
   if (aPrefs->mTouchClusterDetectionDisabled) {
     return true;
   }
 
@@ -408,21 +411,47 @@ IsElementClickableAndReadable(nsIFrame* 
   nsSize frameSize = aFrame->GetSize();
   nsPresContext* pc = aFrame->PresContext();
   nsIPresShell* presShell = pc->PresShell();
   float cumulativeResolution = presShell->GetCumulativeResolution();
   if ((pc->AppUnitsToGfxUnits(frameSize.height) * cumulativeResolution) < limitReadableSize ||
       (pc->AppUnitsToGfxUnits(frameSize.width) * cumulativeResolution) < limitReadableSize) {
     return false;
   }
-  nsRefPtr<nsFontMetrics> fm;
-  nsLayoutUtils::GetFontMetricsForFrame(aFrame, getter_AddRefs(fm),
-    nsLayoutUtils::FontSizeInflationFor(aFrame));
-  if (fm) {
-    if ((fm->EmHeight() > 0) && // See bug 1171731
+  // We want to detect small clickable text elements using the font size.
+  // Two common cases are supported for now:
+  //    1. text node
+  //    2. any element with only one child of type text node
+  // All the other cases are currently ignored.
+  nsIContent *content = aFrame->GetContent();
+  bool testFontSize = false;
+  if (content) {
+    nsINodeList* childNodes = content->ChildNodes();
+    uint32_t childNodeCount = childNodes->Length();
+    if ((content->IsNodeOfType(nsINode::eTEXT)) ||
+      // click occurs on the text inside <a></a> or other clickable tags with text inside
+
+      (childNodeCount == 1 && childNodes->Item(0) &&
+        childNodes->Item(0)->IsNodeOfType(nsINode::eTEXT))) {
+      // The click occurs on an element with only one text node child. In this case, the font size
+      // can be tested.
+      // The number of child nodes is tested to avoid the following cases (See bug 1172488):
+      //   Some jscript libraries transform text elements into Canvas elements but keep the text nodes
+      //   with a very small size (1px) to handle the selection of text.
+      //   With such libraries, the font size of the text elements is not relevant to detect small elements.
+
+      testFontSize = true;
+    }
+  }
+
+  if (testFontSize) {
+    nsRefPtr<nsFontMetrics> fm;
+    nsLayoutUtils::GetFontMetricsForFrame(aFrame, getter_AddRefs(fm),
+      nsLayoutUtils::FontSizeInflationFor(aFrame));
+    if (fm && fm->EmHeight() > 0 && // See bug 1171731
         (pc->AppUnitsToGfxUnits(fm->EmHeight()) * cumulativeResolution) < limitReadableSize) {
       return false;
     }
   }
 
   return true;
 }
 
--- a/mobile/android/base/GeckoAppShell.java
+++ b/mobile/android/base/GeckoAppShell.java
@@ -95,16 +95,17 @@ import android.net.Uri;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.Message;
 import android.os.MessageQueue;
 import android.os.SystemClock;
 import android.os.Vibrator;
+import android.provider.Browser;
 import android.provider.Settings;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
 import android.util.Base64;
 import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.ContextThemeWrapper;
 import android.view.HapticFeedbackConstants;
@@ -955,33 +956,47 @@ public class GeckoAppShell
         Uri uri = aURL.indexOf(':') >= 0 ? Uri.parse(aURL) : new Uri.Builder().scheme(aURL).build();
 
         Intent intent = getOpenURIIntent(getContext(), uri.toString(), "",
             TextUtils.isEmpty(aAction) ? Intent.ACTION_VIEW : aAction, "");
 
         return getHandlersForIntent(intent);
     }
 
+    static List<ResolveInfo> queryIntentActivities(Intent intent) {
+        final PackageManager pm = getContext().getPackageManager();
+
+        // Exclude any non-exported activities: we can't open them even if we want to!
+        // Bug 1031569 has some details.
+        final ArrayList<ResolveInfo> list = new ArrayList<>();
+        for (ResolveInfo ri: pm.queryIntentActivities(intent, 0)) {
+            if (ri.activityInfo.exported) {
+                list.add(ri);
+            }
+        }
+
+        return list;
+    }
+
     static boolean hasHandlersForIntent(Intent intent) {
         try {
-            PackageManager pm = getContext().getPackageManager();
-            List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
-            return !list.isEmpty();
+            return !queryIntentActivities(intent).isEmpty();
         } catch (Exception ex) {
             Log.e(LOGTAG, "Exception in GeckoAppShell.hasHandlersForIntent");
             return false;
         }
     }
 
     static String[] getHandlersForIntent(Intent intent) {
+        final PackageManager pm = getContext().getPackageManager();
         try {
-            PackageManager pm = getContext().getPackageManager();
-            List<ResolveInfo> list = pm.queryIntentActivities(intent, 0);
+            final List<ResolveInfo> list = queryIntentActivities(intent);
+
             int numAttr = 4;
-            String[] ret = new String[list.size() * numAttr];
+            final String[] ret = new String[list.size() * numAttr];
             for (int i = 0; i < list.size(); i++) {
                 ResolveInfo resolveInfo = list.get(i);
                 ret[i * numAttr] = resolveInfo.loadLabel(pm).toString();
                 if (resolveInfo.isDefault)
                     ret[i * numAttr + 1] = "default";
                 else
                     ret[i * numAttr + 1] = "";
                 ret[i * numAttr + 2] = resolveInfo.activityInfo.applicationInfo.packageName;
@@ -1092,16 +1107,20 @@ public class GeckoAppShell
             }
         }
 
         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
         try {
             context.startActivity(intent);
             return true;
         } catch (ActivityNotFoundException e) {
+            Log.w(LOGTAG, "Activity not found.", e);
+            return false;
+        } catch (SecurityException e) {
+            Log.w(LOGTAG, "Forbidden to launch activity.", e);
             return false;
         }
     }
 
     /**
      * Return a <code>Uri</code> instance which is equivalent to <code>u</code>,
      * but with a guaranteed-lowercase scheme as if the API level 16 method
      * <code>u.normalizeScheme</code> had been called.
@@ -1168,16 +1187,36 @@ public class GeckoAppShell
      *         produced.
      */
     static Intent getOpenURIIntent(final Context context,
                                    final String targetURI,
                                    final String mimeType,
                                    final String action,
                                    final String title) {
 
+        // The resultant chooser can return non-exported activities in 4.1 and earlier.
+        // https://code.google.com/p/android/issues/detail?id=29535
+        final Intent intent = getOpenURIIntentInner(context, targetURI, mimeType, action, title);
+
+        if (intent != null) {
+            // Only handle applications which can accept arbitrary data from a browser.
+            intent.addCategory(Intent.CATEGORY_BROWSABLE);
+
+            // Some applications use this field to return to the same browser after processing the
+            // Intent. While there is some danger (e.g. denial of service), other major browsers already
+            // use it and so it's the norm.
+            intent.putExtra(Browser.EXTRA_APPLICATION_ID, GeckoApp.class.getPackage().getName());
+        }
+
+        return intent;
+    }
+
+    private static Intent getOpenURIIntentInner(final Context context,  final String targetURI,
+            final String mimeType, final String action, final String title) {
+
         if (action.equalsIgnoreCase(Intent.ACTION_SEND)) {
             Intent shareIntent = getShareIntent(context, targetURI, mimeType, title);
             return Intent.createChooser(shareIntent,
                                         context.getResources().getString(R.string.share_title)); 
         }
 
         final Uri uri = normalizeUriScheme(targetURI.indexOf(':') >= 0 ? Uri.parse(targetURI) : new Uri.Builder().scheme(targetURI).build());
         if (!TextUtils.isEmpty(mimeType)) {
@@ -1195,19 +1234,16 @@ public class GeckoAppShell
             final Intent intent;
             try {
                 intent = Intent.parseUri(targetURI, Intent.URI_INTENT_SCHEME);
             } catch (final URISyntaxException e) {
                 Log.e(LOGTAG, "Unable to parse URI - " + e);
                 return null;
             }
 
-            // Only handle applications which can accept arbitrary data from a browser.
-            intent.addCategory(Intent.CATEGORY_BROWSABLE);
-
             // Prevent site from explicitly opening our internal activities, which can leak data.
             intent.setComponent(null);
             nullIntentSelector(intent);
 
             return intent;
         }
 
         // Compute our most likely intent, then check to see if there are any
--- a/mobile/android/base/IntentHelper.java
+++ b/mobile/android/base/IntentHelper.java
@@ -97,17 +97,23 @@ public final class IntentHelper implemen
     private void openForResult(final JSONObject message) throws JSONException {
         Intent intent = GeckoAppShell.getOpenURIIntent(activity,
                                                        message.optString("url"),
                                                        message.optString("mime"),
                                                        message.optString("action"),
                                                        message.optString("title"));
         intent.setClassName(message.optString("packageName"), message.optString("className"));
         intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        ActivityHandlerHelper.startIntentForActivity(activity, intent, new ResultHandler(message));
+
+        final ResultHandler handler = new ResultHandler(message);
+        try {
+            ActivityHandlerHelper.startIntentForActivity(activity, intent, handler);
+        } catch (SecurityException e) {
+            Log.w(LOGTAG, "Forbidden to launch activity.", e);
+        }
     }
 
     private void openWebActivity(JSONObject message) throws JSONException {
         final Intent intent = WebActivityMapper.getIntentForWebActivity(message.getJSONObject("activity"));
         ActivityHandlerHelper.startIntentForActivity(activity, intent, new ResultHandler(message));
     }
 
     private static class ResultHandler implements ActivityResultHandler {
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -39,17 +39,16 @@ GARBAGE += \
   AndroidManifest.xml  \
   WebappManifestFragment.xml.frag \
   classes.dex  \
   gecko.ap_  \
   res/values/strings.xml \
   res/raw/browsersearch.json \
   res/raw/suggestedsites.json \
   .aapt.deps \
-  fennec_ids.txt \
   javah.out \
   jni-stubs.inc \
   GeneratedJNIWrappers.cpp \
   GeneratedJNIWrappers.h \
   $(NULL)
 
 GARBAGE_DIRS += classes db jars res sync services generated
 
@@ -231,16 +230,17 @@ ANNOTATION_PROCESSOR_JAR_FILES := $(DEPT
 
 GeneratedJNIWrappers.cpp: $(ANNOTATION_PROCESSOR_JAR_FILES)
 GeneratedJNIWrappers.cpp: $(ALL_JARS)
 	$(JAVA) -classpath gecko-mozglue.jar:$(JAVA_BOOTCLASSPATH):$(JAVA_CLASSPATH):$(ANNOTATION_PROCESSOR_JAR_FILES) org.mozilla.gecko.annotationProcessors.AnnotationProcessor $(ALL_JARS)
 
 manifest := \
   AndroidManifest.xml.in \
   WebappManifestFragment.xml.frag.in \
+  fennec_ids.txt.in \
   $(NULL)
 
 PP_TARGETS += manifest
 
 # Certain source files need to be preprocessed.  This special rule
 # generates these files into generated/org/mozilla/gecko for
 # consumption by the build system and IDEs.
 
@@ -444,19 +444,16 @@ endef
 # toolkit/mozapps/installer/packager.mk.
 
 # .aapt.deps: $(all_resources)
 $(eval $(call aapt_command,.aapt.deps,$(all_resources),gecko.ap_,generated/,./))
 
 # .aapt.nodeps: $(CURDIR)/AndroidManifest.xml FORCE
 $(eval $(call aapt_command,.aapt.nodeps,$(CURDIR)/AndroidManifest.xml FORCE,gecko-nodeps.ap_,gecko-nodeps/,gecko-nodeps/))
 
-fennec_ids.txt: generated/org/mozilla/gecko/R.java fennec-ids-generator.py
-	$(PYTHON) $(topsrcdir)/mobile/android/base/fennec-ids-generator.py -i $< -o $@
-
 # Override the Java settings with some specific android settings
 include $(topsrcdir)/config/android-common.mk
 
 update-generated-wrappers:
 	@mv $(topsrcdir)/widget/android/GeneratedJNIWrappers.cpp $(topsrcdir)/widget/android/GeneratedJNIWrappers.cpp.old
 	@mv $(topsrcdir)/widget/android/GeneratedJNIWrappers.h $(topsrcdir)/widget/android/GeneratedJNIWrappers.h.old
 	@echo old GeneratedJNIWrappers.cpp/h moved to GeneratedJNIWrappers.cpp/h.old
 	@cp $(CURDIR)/jni-stubs.inc $(topsrcdir)/mozglue/android
@@ -483,17 +480,17 @@ update-generated-wrappers:
 
 # Targets built very early during a Gradle build.
 gradle-targets: .aapt.deps
 
 gradle-omnijar: $(abspath $(DIST)/fennec/$(OMNIJAR_NAME))
 
 .PHONY: gradle-targets gradle-omnijar
 
-libs:: geckoview_resources.zip classes.dex jni-stubs.inc GeneratedJNIWrappers.cpp fennec_ids.txt
+libs:: geckoview_resources.zip classes.dex jni-stubs.inc GeneratedJNIWrappers.cpp $(CURDIR)/fennec_ids.txt
 	$(INSTALL) geckoview_resources.zip $(FINAL_TARGET)
 	$(INSTALL) classes.dex $(FINAL_TARGET)
 	@(diff jni-stubs.inc $(topsrcdir)/mozglue/android/jni-stubs.inc >/dev/null && diff GeneratedJNIWrappers.cpp $(topsrcdir)/widget/android/GeneratedJNIWrappers.cpp >/dev/null) || \
 	 (echo '*****************************************************' && \
 	  echo '***   Error: The generated JNI code has changed   ***' && \
 	  echo '* To update generated code in the tree, please run  *' && \
 	  echo && \
 	  echo '  make -C $(CURDIR) update-generated-wrappers' && \
--- a/mobile/android/base/ZoomedView.java
+++ b/mobile/android/base/ZoomedView.java
@@ -30,16 +30,20 @@ import android.graphics.Point;
 import android.graphics.PointF;
 import android.graphics.RectF;
 import android.graphics.Shader;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewTreeObserver;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.OvershootInterpolator;
+import android.view.animation.ScaleAnimation;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import java.nio.ByteBuffer;
 import java.text.DecimalFormat;
 
@@ -47,45 +51,55 @@ public class ZoomedView extends FrameLay
         LayerView.ZoomedViewListener, GeckoEventListener {
     private static final String LOGTAG = "Gecko" + ZoomedView.class.getSimpleName();
 
     private static final float[] ZOOM_FACTORS_LIST = {2.0f, 3.0f, 1.5f};
     private static final int W_CAPTURED_VIEW_IN_PERCENT = 50;
     private static final int H_CAPTURED_VIEW_IN_PERCENT = 50;
     private static final int MINIMUM_DELAY_BETWEEN_TWO_RENDER_CALLS_NS = 1000000;
     private static final int DELAY_BEFORE_NEXT_RENDER_REQUEST_MS = 2000;
+    private static final int OPENING_ANIMATION_DURATION_MS = 250;
+    private static final int CLOSING_ANIMATION_DURATION_MS = 150;
+    private static final float OVERSHOOT_INTERPOLATOR_TENSION = 1.5f;
 
     private float zoomFactor;
     private int currentZoomFactorIndex;
     private ImageView zoomedImageView;
     private LayerView layerView;
     private int viewWidth;
     private int viewHeight; // Only the zoomed view height, no toolbar, no shadow ...
     private int viewContainerWidth;
     private int viewContainerHeight; // Zoomed view height with toolbar and other elements like shadow, ...
     private int containterSize; // shadow, margin, ...
     private Point lastPosition;
     private boolean shouldSetVisibleOnUpdate;
     private PointF returnValue;
+    private final PointF animationStart;
     private ImageView closeButton;
     private TextView changeZoomFactorButton;
     private boolean toolbarOnTop;
     private float offsetDueToToolBarPosition;
     private int toolbarHeight;
     private int cornerRadius;
 
     private boolean stopUpdateView;
 
     private int lastOrientation;
 
     private ByteBuffer buffer;
     private Runnable requestRenderRunnable;
     private long startTimeReRender;
     private long lastStartTimeReRender;
 
+    private ZoomedViewTouchListener touchListener;
+
+    private enum StartPointUpdate {
+        GECKO_POSITION, CENTER, NO_CHANGE
+    }
+
     private class RoundedBitmapDrawable extends BitmapDrawable {
         private Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
         final float cornerRadius;
         final boolean squareOnTopOfDrawable;
 
         RoundedBitmapDrawable(Resources res, Bitmap bitmap, boolean squareOnTop, int radius) {
             super(res, bitmap);
             squareOnTopOfDrawable = squareOnTop;
@@ -177,17 +191,17 @@ public class ZoomedView extends FrameLay
                 // When the user just touches the screen ACTION_MOVE can be detected for a very small delta on position.
                 // In this case, the move is ignored if the delta is lower than 1 unit.
                 return false;
             }
 
             float newLeftMargin = params.leftMargin + event.getRawX() - originRawX;
             float newTopMargin = params.topMargin + event.getRawY() - originRawY;
             ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
-            ZoomedView.this.moveZoomedView(metrics, newLeftMargin, newTopMargin);
+            ZoomedView.this.moveZoomedView(metrics, newLeftMargin, newTopMargin, StartPointUpdate.CENTER);
             originRawX = event.getRawX();
             originRawY = event.getRawY();
             return true;
         }
     }
 
     public ZoomedView(Context context) {
         this(context, null, 0);
@@ -195,24 +209,26 @@ public class ZoomedView extends FrameLay
 
     public ZoomedView(Context context, AttributeSet attrs) {
         this(context, attrs, 0);
     }
 
     public ZoomedView(Context context, AttributeSet attrs, int defStyle) {
         super(context, attrs, defStyle);
         returnValue = new PointF();
+        animationStart = new PointF();
         currentZoomFactorIndex = 0;
         zoomFactor = ZOOM_FACTORS_LIST[currentZoomFactorIndex];
         requestRenderRunnable = new Runnable() {
             @Override
             public void run() {
                 requestZoomedViewRender();
             }
         };
+        touchListener = new ZoomedViewTouchListener();
         EventDispatcher.getInstance().registerGeckoThreadListener(this,
                 "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange");
     }
 
     void destroy() {
         ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
         EventDispatcher.getInstance().unregisterGeckoThreadListener(this,
                 "Gesture:clusteredLinksClicked", "Window:Resize", "Content:LocationChange");
@@ -220,40 +236,51 @@ public class ZoomedView extends FrameLay
 
     // This method (onFinishInflate) is called only when the zoomed view class is used inside
     // an xml structure <org.mozilla.gecko.ZoomedView ...
     // It won't be called if the class is used from java code like "new  ZoomedView(context);"
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
         closeButton = (ImageView) findViewById(R.id.dialog_close);
-        closeButton.setOnClickListener(new View.OnClickListener() {
-            public void onClick(View view) {
-                stopZoomDisplay();
-            }
-        });
+        changeZoomFactorButton = (TextView) findViewById(R.id.change_zoom_factor);
+        zoomedImageView = (ImageView) findViewById(R.id.zoomed_image_view);
 
-        changeZoomFactorButton = (TextView) findViewById(R.id.change_zoom_factor);
-        changeZoomFactorButton.setOnClickListener(new View.OnClickListener() {
-            public void onClick(View view) {
-                changeZoomFactor();
-            }
-        });
         setTextInZoomFactorButton(ZOOM_FACTORS_LIST[0]);
 
-        zoomedImageView = (ImageView) findViewById(R.id.zoomed_image_view);
-        this.setOnTouchListener(new ZoomedViewTouchListener());
-
         toolbarHeight = getResources().getDimensionPixelSize(R.dimen.zoomed_view_toolbar_height);
         containterSize = getResources().getDimensionPixelSize(R.dimen.drawable_dropshadow_size);
         cornerRadius = getResources().getDimensionPixelSize(R.dimen.button_corner_radius);
 
         moveToolbar(true);
     }
 
+    private void setListeners() {
+        closeButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View view) {
+                stopZoomDisplay(true);
+            }
+        });
+
+        changeZoomFactorButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View view) {
+                changeZoomFactor();
+            }
+        });
+
+        setOnTouchListener(touchListener);
+    }
+
+    private void removeListeners() {
+        closeButton.setOnClickListener(null);
+
+        changeZoomFactorButton.setOnClickListener(null);
+
+        setOnTouchListener(null);
+    }
     /*
      * Convert a click from ZoomedView. Return the position of the click in the
      * LayerView
      */
     private PointF getUnzoomedPositionFromPointInZoomedView(float x, float y) {
         if (toolbarOnTop && y > toolbarHeight) {
            y = y - toolbarHeight;
         }
@@ -318,17 +345,18 @@ public class ZoomedView extends FrameLay
         returnValue.y = (int) ((((y + offsetDueToToolBarPosition - (viewHeight / (2 * zoomFactor)))) /
                         ((parentHeight - offset.y + offsetDueToToolBarPosition - (viewHeight / zoomFactor)) /
                         (parentHeight - offset.y - viewContainerHeight)))
                 + offset.y);
 
         return returnValue;
     }
 
-    private void moveZoomedView(ImmutableViewportMetrics metrics, float newLeftMargin, float newTopMargin) {
+    private void moveZoomedView(ImmutableViewportMetrics metrics, float newLeftMargin, float newTopMargin,
+            StartPointUpdate animateStartPoint) {
         final float parentWidth = metrics.getWidth();
         final float parentHeight = metrics.getHeight();
         RelativeLayout.LayoutParams newLayoutParams = (RelativeLayout.LayoutParams) getLayoutParams();
         newLayoutParams.leftMargin = (int) newLeftMargin;
         newLayoutParams.topMargin = (int) newTopMargin;
         int topMarginMin;
         int leftMarginMin;
         PointF offset = metrics.getMarginOffset();
@@ -348,16 +376,32 @@ public class ZoomedView extends FrameLay
         }
 
         if (newLayoutParams.topMargin < topMarginMin + 1) {
             moveToolbar(false);
         } else if (newLayoutParams.topMargin + viewContainerHeight > parentHeight - 1) {
             moveToolbar(true);
         }
 
+        if (animateStartPoint == StartPointUpdate.GECKO_POSITION) {
+            // Before this point, the animationStart point is relative to the layerView.
+            // The value is initialized in startZoomDisplay using the click point position coming from Gecko.
+            // The position of the zoomed view is now calculated, so the position of the animation
+            // can now be correctly set relative to the zoomed view
+            animationStart.x = animationStart.x - newLayoutParams.leftMargin;
+            animationStart.y = animationStart.y - newLayoutParams.topMargin;
+        } else if (animateStartPoint == StartPointUpdate.CENTER) {
+            // At this point, the animationStart point is no more valid probably because
+            // the zoomed view has been moved by the user.
+            // In this case, the animationStart point is set to the center point of the zoomed view.
+            PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(viewContainerWidth / 2, viewContainerHeight / 2);
+            animationStart.x = convertedPosition.x - newLayoutParams.leftMargin;
+            animationStart.y = convertedPosition.y - newLayoutParams.topMargin;
+        }
+
         setLayoutParams(newLayoutParams);
         PointF convertedPosition = getUnzoomedPositionFromPointInZoomedView(0, 0);
         lastPosition = PointUtils.round(convertedPosition);
         requestZoomedViewRender();
     }
 
     private void moveToolbar(boolean moveTop) {
         if (toolbarOnTop == moveTop) {
@@ -404,17 +448,17 @@ public class ZoomedView extends FrameLay
 
     private void refreshZoomedViewSize(ImmutableViewportMetrics viewport) {
         if (layerView == null) {
             return;
         }
 
         RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) getLayoutParams();
         setCapturedSize(viewport);
-        moveZoomedView(viewport, params.leftMargin, params.topMargin);
+        moveZoomedView(viewport, params.leftMargin, params.topMargin, StartPointUpdate.NO_CHANGE);
     }
 
     private void setCapturedSize(ImmutableViewportMetrics metrics) {
         float parentMinSize = Math.min(metrics.getWidth(), metrics.getHeight());
         viewWidth = (int) ((parentMinSize * W_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor);
         viewHeight = (int) ((parentMinSize * H_CAPTURED_VIEW_IN_PERCENT / (zoomFactor * 100.0)) * zoomFactor);
         viewContainerHeight = viewHeight + toolbarHeight +
                 2 * containterSize; // Top and bottom shadows
@@ -438,22 +482,31 @@ public class ZoomedView extends FrameLay
             layerView = aLayerView;
             layerView.addZoomedViewListener(this);
             layerView.setOnMetricsChangedZoomedViewportListener(this);
             ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
             setCapturedSize(metrics);
         }
         startTimeReRender = 0;
         shouldSetVisibleOnUpdate = true;
+
+        ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
+        PointF offset = metrics.getMarginOffset();
+        // At this point, the start point is relative to the layerView.
+        // Later, it will be converted relative to the zoomed view as soon as
+        // the position of the zoomed view will be calculated.
+        animationStart.x = (float) leftFromGecko * metrics.zoomFactor + offset.x;
+        animationStart.y = (float) topFromGecko * metrics.zoomFactor + offset.y;
+
         moveUsingGeckoPosition(leftFromGecko, topFromGecko);
     }
 
-    private void stopZoomDisplay() {
+    private void stopZoomDisplay(boolean withAnimation) {
         shouldSetVisibleOnUpdate = false;
-        this.setVisibility(View.GONE);
+        hideZoomedView(withAnimation);
         ThreadUtils.removeCallbacksFromUiThread(requestRenderRunnable);
         if (layerView != null) {
             layerView.setOnMetricsChangedZoomedViewportListener(null);
             layerView.removeZoomedViewListener(this);
             layerView = null;
         }
     }
 
@@ -489,34 +542,34 @@ public class ZoomedView extends FrameLay
                         LayerView geckoAppLayerView = GeckoAppShell.getLayerView();
                         if (geckoAppLayerView != null) {
                             startZoomDisplay(geckoAppLayerView, left, top);
                         }
                     } else if (event.equals("Window:Resize")) {
                         ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
                         refreshZoomedViewSize(metrics);
                     } else if (event.equals("Content:LocationChange")) {
-                        stopZoomDisplay();
+                        stopZoomDisplay(false);
                     }
                 } catch (JSONException e) {
                     Log.e(LOGTAG, "JSON exception", e);
                 }
             }
         });
     }
 
     private void moveUsingGeckoPosition(int leftFromGecko, int topFromGecko) {
         ImmutableViewportMetrics metrics = layerView.getViewportMetrics();
         final float parentHeight = metrics.getHeight();
         // moveToolbar is called before getZoomedViewTopLeftPositionFromTouchPosition in order to
         // correctly center vertically the zoomed area
         moveToolbar((topFromGecko * metrics.zoomFactor > parentHeight / 2));
         PointF convertedPosition = getZoomedViewTopLeftPositionFromTouchPosition((leftFromGecko * metrics.zoomFactor),
                 (topFromGecko * metrics.zoomFactor));
-        moveZoomedView(metrics, convertedPosition.x, convertedPosition.y);
+        moveZoomedView(metrics, convertedPosition.x, convertedPosition.y, StartPointUpdate.GECKO_POSITION);
     }
 
     @Override
     public void onMetricsChanged(final ImmutableViewportMetrics viewport) {
         // It can be called from a Gecko thread (forceViewportMetrics in GeckoLayerClient).
         // Post to UI Thread to avoid Exception:
         //    "Only the original thread that created a view hierarchy can touch its views."
         ThreadUtils.postToUiThread(new Runnable() {
@@ -543,23 +596,76 @@ public class ZoomedView extends FrameLay
                 Log.w(LOGTAG, iae.toString());
             }
             if (zoomedImageView != null) {
                 RoundedBitmapDrawable ob3 = new RoundedBitmapDrawable(getResources(), sb3, toolbarOnTop, cornerRadius);
                 zoomedImageView.setImageDrawable(ob3);
             }
         }
         if (shouldSetVisibleOnUpdate) {
-            this.setVisibility(View.VISIBLE);
-            shouldSetVisibleOnUpdate = false;
+            this.showZoomedView();
         }
         lastStartTimeReRender = startTimeReRender;
         startTimeReRender = 0;
     }
 
+    private void showZoomedView() {
+        // no animation if the zoomed view is already visible
+        if (getVisibility() != View.VISIBLE) {
+            final Animation anim = new ScaleAnimation(
+                    0f, 1f, // Start and end values for the X axis scaling
+                    0f, 1f, // Start and end values for the Y axis scaling
+                    Animation.ABSOLUTE, animationStart.x, // Pivot point of X scaling
+                    Animation.ABSOLUTE, animationStart.y); // Pivot point of Y scaling
+            anim.setFillAfter(true); // Needed to keep the result of the animation
+            anim.setDuration(OPENING_ANIMATION_DURATION_MS);
+            anim.setInterpolator(new OvershootInterpolator(OVERSHOOT_INTERPOLATOR_TENSION));
+            anim.setAnimationListener(new AnimationListener() {
+                public void onAnimationEnd(Animation animation) {
+                    setListeners();
+                }
+                public void onAnimationRepeat(Animation animation) {
+                }
+                public void onAnimationStart(Animation animation) {
+                    removeListeners();
+                }
+            });
+            setAnimation(anim);
+        }
+        setVisibility(View.VISIBLE);
+        shouldSetVisibleOnUpdate = false;
+    }
+
+    private void hideZoomedView(boolean withAnimation) {
+        if (withAnimation) {
+            final Animation anim = new ScaleAnimation(
+                1f, 0f, // Start and end values for the X axis scaling
+                1f, 0f, // Start and end values for the Y axis scaling
+                Animation.ABSOLUTE, animationStart.x, // Pivot point of X scaling
+                Animation.ABSOLUTE, animationStart.y); // Pivot point of Y scaling
+            anim.setFillAfter(true); // Needed to keep the result of the animation
+            anim.setDuration(CLOSING_ANIMATION_DURATION_MS);
+            anim.setAnimationListener(new AnimationListener() {
+                public void onAnimationEnd(Animation animation) {
+                }
+                public void onAnimationRepeat(Animation animation) {
+                }
+                public void onAnimationStart(Animation animation) {
+                    removeListeners();
+                }
+            });
+            setAnimation(anim);
+        } else {
+            removeListeners();
+            setAnimation(null);
+        }
+        setVisibility(View.GONE);
+        shouldSetVisibleOnUpdate = false;
+    }
+
     private void updateBufferSize() {
         int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2;
         int capacity = viewWidth * viewHeight * pixelSize;
         if (buffer == null || buffer.capacity() != capacity) {
             buffer = DirectBufferAllocator.free(buffer);
             buffer = DirectBufferAllocator.allocate(capacity);
         }
     }
deleted file mode 100644
--- a/mobile/android/base/fennec-ids-generator.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-import re
-import os
-import sys
-import optparse
-
-def getFile(filename):
-  fHandle = open(filename, 'r')
-  data = fHandle.read()
-  fHandle.close()
-  return data
-
-def findIDs(data):
-  start_function = False
-  reID = re.compile('.*public static final class id {.*')
-  reEnd = re.compile('.*}.*')
-  idlist = []
-
-  for line in data.split('\n'):
-    if reEnd.match(line):
-      start_function = False
-
-    if start_function:
-      id_value = line.split(' ')[-1]
-      idlist.append(id_value.split(';')[0].split('='))
-
-    if reID.match(line):
-      start_function = True
-
-  return idlist
-
-
-def printIDs(outputFile, idlist):
-  fOutput = open(outputFile, 'w')
-  for item in idlist:
-    fOutput.write("%s=%s\n" % (item[0], item[1]))
-  fOutput.close()
-
-def main(args=sys.argv[1:]):
-  parser = optparse.OptionParser()
-  parser.add_option('-o', '--output', dest='outputFile', default='',
-                    help="output file with the id=value pairs")
-  parser.add_option('-i', '--input', dest='inputFile', default='',
-                    help="filename of the input R.java file")
-  options, args = parser.parse_args(args)
-
-  if options.inputFile == '':
-    print "Error: please provide input file: -i <filename>"
-    sys.exit(1)
-
-  if options.outputFile == '':
-    print "Error: please provide output file: -o <filename>"
-    sys.exit(1)
-
-  data = getFile(os.path.abspath(options.inputFile));
-  idlist = findIDs(data)
-  printIDs(os.path.abspath(options.outputFile), idlist)
-
-if __name__ == "__main__":
-    main()
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/fennec_ids.txt.in
@@ -0,0 +1,2 @@
+#filter slashslash
+// fennec_ids.txt needs to exist (for automation) but contains no content.
--- a/mobile/android/chrome/content/aboutLogins.js
+++ b/mobile/android/chrome/content/aboutLogins.js
@@ -58,20 +58,16 @@ let Logins = {
 
   init: function () {
     window.addEventListener("popstate", this , false);
 
     Services.obs.addObserver(this, "passwordmgr-storage-changed", false);
 
     this._loadList(this._getLogins());
 
-    document.getElementById("copyusername-btn").addEventListener("click", this._copyUsername.bind(this), false);
-    document.getElementById("copypassword-btn").addEventListener("click", this._copyPassword.bind(this), false);
-    document.getElementById("details-header").addEventListener("click", this._openLink.bind(this), false);
-
     let filterInput = document.getElementById("filter-input");
     let filterContainer = document.getElementById("filter-input-container");
 
     filterInput.addEventListener("input", (event) => {
       // Stop any in-progress filter timer
       if (this._filterTimer) {
         clearTimeout(this._filterTimer);
         this._filterTimer = null;
@@ -120,52 +116,35 @@ let Logins = {
       let item = this._createItemForLogin(login);
       newList.appendChild(item);
     });
 
     list.parentNode.replaceChild(newList, list);
   },
 
   _showList: function () {
-    // Hide the detail page and show the list
-    let details = document.getElementById("login-details");
-    details.setAttribute("hidden", "true");
     let list = document.getElementById("logins-list");
     list.removeAttribute("hidden");
   },
 
-  _onPopState: function (event) {
-    // Called when back/forward is used to change the state of the page
-    if (event.state) {
-      // Show the detail page for an addon
-      this._showDetails(this._getElementForLogin(event.state.id));
-    } else {
-      // Clear any previous detail addon
-      let detailItem = document.querySelector("#login-details > .login-item");
-      detailItem.login = null;
-      this._showList();
-    }
-  },
-
   _onLoginClick: function (event) {
     let loginItem = event.currentTarget;
     let login = loginItem.login;
     if (!login) {
       debug("No login!");
       return;
     }
 
     let prompt = new Prompt({
       window: window,
     });
     let menuItems = [
       { label: gStringBundle.GetStringFromName("loginsMenu.showPassword") },
       { label: gStringBundle.GetStringFromName("loginsMenu.copyPassword") },
       { label: gStringBundle.GetStringFromName("loginsMenu.copyUsername") },
-      { label: gStringBundle.GetStringFromName("loginsMenu.details") },
       { label: gStringBundle.GetStringFromName("loginsMenu.delete") }
     ];
 
     prompt.setSingleChoiceItems(menuItems);
     prompt.show((data) => {
       // Switch on indices of buttons, as they were added when creating login item.
       switch (data.button) {
         case 0:
@@ -185,20 +164,16 @@ let Logins = {
           break;
         case 1:
           copyStringAndToast(login.password, gStringBundle.GetStringFromName("loginsDetails.passwordCopied"));
           break;
         case 2:
           copyStringAndToast(login.username, gStringBundle.GetStringFromName("loginsDetails.usernameCopied"));
           break;
         case 3:
-          this._showDetails(loginItem);
-          history.pushState({ id: login.guid }, document.title);
-          break;
-        case 4:
           let confirmPrompt = new Prompt({
             window: window,
             message: gStringBundle.GetStringFromName("loginsDialog.confirmDelete"),
             buttons: [
               gStringBundle.GetStringFromName("loginsDialog.confirm"),
               gStringBundle.GetStringFromName("loginsDialog.cancel") ]
           });
           confirmPrompt.show((data) => {
@@ -260,22 +235,16 @@ let Logins = {
     descPart.className = "username";
     inner.appendChild(descPart);
 
     loginItem.appendChild(inner);
     loginItem.login = login;
     return loginItem;
   },
 
-  _getElementForLogin: function (login) {
-    let list = document.getElementById("logins-list");
-    let element = list.querySelector("div[loginID=" + login.quote() + "]");
-    return element;
-  },
-
   handleEvent: function (event) {
     switch (event.type) {
       case "popstate": {
         this._onPopState(event);
         break;
       }
       case "click": {
         this._onLoginClick(event);
@@ -289,71 +258,16 @@ let Logins = {
       case "passwordmgr-storage-changed": {
         // Reload logins content.
         this._loadList(this._getLogins());
         break;
       }
     }
   },
 
-  _showDetails: function (listItem) {
-    let detailItem = document.querySelector("#login-details > .login-item");
-    let login = detailItem.login = listItem.login;
-    let favicon = detailItem.querySelector(".icon");
-    favicon.style["background-image"] = listItem.querySelector(".icon").style["background-image"];
-    favicon.style.visibility = "visible";
-    document.getElementById("details-header").setAttribute("link", login.hostname);
-
-    document.getElementById("detail-hostname").textContent = login.hostname;
-    document.getElementById("detail-realm").textContent = login.httpRealm;
-    document.getElementById("detail-username").textContent = login.username;
-
-    // Borrowed from desktop Firefox: http://mxr.mozilla.org/mozilla-central/source/browser/base/content/urlbarBindings.xml#204
-    let matchedURL = login.hostname.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/);
-
-    let userInputs = [];
-    if (matchedURL) {
-      let [, , domain] = matchedURL;
-      userInputs = domain.split(".").filter(part => part.length > 3);
-    }
-
-    let lastChanged = new Date(login.QueryInterface(Ci.nsILoginMetaInfo).timePasswordChanged);
-    let days = Math.round((Date.now() - lastChanged) / 1000 / 60 / 60/ 24);
-    document.getElementById("detail-age").textContent = gStringBundle.formatStringFromName("loginsDetails.age", [days], 1);
-
-    let list = document.getElementById("logins-list");
-    list.setAttribute("hidden", "true");
-
-    let loginDetails = document.getElementById("login-details");
-    loginDetails.removeAttribute("hidden");
-
-    // Password details page is loaded.
-    let loadEvent = document.createEvent("Events");
-    loadEvent.initEvent("PasswordsDetailsLoad", true, false);
-    window.dispatchEvent(loadEvent);
-  },
-
-  _copyUsername: function() {
-    let detailItem = document.querySelector("#login-details > .login-item");
-    let login = detailItem.login;
-    copyStringAndToast(login.username, gStringBundle.GetStringFromName("loginsDetails.usernameCopied"));
-  },
-
-  _copyPassword: function() {
-    let detailItem = document.querySelector("#login-details > .login-item");
-    let login = detailItem.login;
-    copyStringAndToast(login.password, gStringBundle.GetStringFromName("loginsDetails.passwordCopied"));
-  },
-
-  _openLink: function (clickEvent) {
-    let url = clickEvent.currentTarget.getAttribute("link");
-    let BrowserApp = gChromeWin.BrowserApp;
-    BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id });
-  },
-
   _filter: function(event) {
     let value = event.target.value.toLowerCase();
     let logins = this._logins.filter((login) => {
       if (login.hostname.toLowerCase().indexOf(value) != -1) {
         return true;
       }
       if (login.username &&
           login.username.toLowerCase().indexOf(value) != -1) {
--- a/mobile/android/chrome/content/aboutLogins.xhtml
+++ b/mobile/android/chrome/content/aboutLogins.xhtml
@@ -24,31 +24,14 @@
     <div id="logins-header" class="header">
       <div>&aboutLogins.title;</div>
       <ul class="toolbar-buttons">
         <li id="filter-button"></li>
       </ul>
     </div>
     <div id="logins-list" class="list" hidden="true">
     </div>
-    <div id="login-details" class="list" hidden="true">
-      <div class="login-item list-item">
-        <div class="icon"/>
-        <div id="details-header" class="inner">
-          <div class="details">
-            <div id="detail-hostname" class="hostname"></div>
-            <div id="detail-realm" class="realm"></div>
-          </div>
-          <div id="detail-username" class="username"></div>
-          <div id="detail-age"></div>
-        </div>
-        <div class="buttons">
-          <button id="copyusername-btn">&aboutLogins.copyUsername;</button>
-          <button id="copypassword-btn">&aboutLogins.copyPassword;</button>
-        </div>
-      </div>
-    </div>
     <div id="filter-input-container" hidden="true">
       <input id="filter-input" type="search"/>
       <div id="filter-clear"></div>
     </div>
   </body>
 </html>
--- a/mobile/android/locales/en-US/chrome/aboutLogins.dtd
+++ b/mobile/android/locales/en-US/chrome/aboutLogins.dtd
@@ -1,8 +1,5 @@
 <!-- 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/. -->
 
 <!ENTITY aboutLogins.title                       "Logins">
-
-<!ENTITY aboutLogins.copyUsername                "Copy Username">
-<!ENTITY aboutLogins.copyPassword                "Copy Password">
--- a/mobile/android/locales/en-US/chrome/aboutLogins.properties
+++ b/mobile/android/locales/en-US/chrome/aboutLogins.properties
@@ -1,16 +1,15 @@
 # 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/.
 
 loginsMenu.showPassword=Show password
 loginsMenu.copyPassword=Copy password
 loginsMenu.copyUsername=Copy username
-loginsMenu.details=Details
 loginsMenu.delete=Delete
 
 loginsDialog.confirmDelete=Delete this login?
 loginsDialog.copy=Copy
 loginsDialog.confirm=OK
 loginsDialog.cancel=Cancel
 
 loginsDetails.age=Age: %S days
--- a/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
+++ b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java
@@ -9,16 +9,17 @@ import java.net.URISyntaxException;
 import java.net.URL;
 
 import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.search.providers.SearchEngine;
 
+import android.annotation.SuppressLint;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.provider.Settings;
 import android.support.v4.app.Fragment;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.LayoutInflater;
@@ -39,16 +40,17 @@ public class PostSearchFragment extends 
     private SearchEngine engine;
 
     private ProgressBar progressBar;
     private WebView webview;
     private View errorView;
 
     private String resultsPageHost;
 
+    @SuppressLint("SetJavaScriptEnabled")
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
                              Bundle savedInstanceState) {
         View mainView = inflater.inflate(R.layout.search_fragment_post_search, container, false);
 
         progressBar = (ProgressBar) mainView.findViewById(R.id.progress_bar);
 
         webview = (WebView) mainView.findViewById(R.id.webview);
--- a/mobile/android/themes/core/aboutLogins.css
+++ b/mobile/android/themes/core/aboutLogins.css
@@ -4,24 +4,16 @@
 
 %filter substitution
 %include defines.inc
 
 .hidden {
   display: none;
 }
 
-.details {
-  width: 100%;
-}
-
-.details > div {
-  display: inline;
-}
-
 .username {
   width: 100%;
   overflow: hidden;
   white-space: nowrap;
   text-overflow: ellipsis;
 }
 
 .hostname {
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -648,24 +648,16 @@ var LoginManagerContent = {
       return;
 
     var hostname = LoginUtils._getPasswordOrigin(doc.documentURI);
     if (!hostname) {
       log("(form submission ignored -- invalid hostname)");
       return;
     }
 
-    // Somewhat gross hack - we don't want to show the "remember password"
-    // notification on about:accounts for Firefox.
-    let topWin = win.top;
-    if (/^about:accounts($|\?)/i.test(topWin.document.documentURI)) {
-      log("(form submission ignored -- about:accounts)");
-      return;
-    }
-
     let formSubmitURL = LoginUtils._getActionOrigin(form);
     let messageManager = messageManagerFromWindow(win);
 
     let recipesArray = messageManager.sendSyncMessage("RemoteLogins:findRecipes", {
       formOrigin: hostname,
     })[0];
     let recipes = new Set(recipesArray);
 
--- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
+++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
@@ -933,16 +933,17 @@ LoginManagerPrompter.prototype = {
       browser,
       "password",
       this._getLocalizedString(initialMsgNames.prompt, [displayHost]),
       "password-notification-icon",
       mainAction,
       secondaryActions,
       {
         timeout: Date.now() + 10000,
+        origin: login.hostname,
         persistWhileVisible: true,
         passwordNotificationType: type,
         eventCallback: function (topic) {
           switch (topic) {
             case "showing":
               currentNotification = this;
               chromeDoc.getElementById("password-notification-username")
                        .addEventListener("input", onInput);
--- a/toolkit/components/telemetry/TelemetryController.jsm
+++ b/toolkit/components/telemetry/TelemetryController.jsm
@@ -49,16 +49,17 @@ const PING_FORMAT_VERSION = 4;
 
 // Delay before intializing telemetry (ms)
 const TELEMETRY_DELAY = 60000;
 // Delay before initializing telemetry if we're testing (ms)
 const TELEMETRY_TEST_DELAY = 100;
 
 // Ping types.
 const PING_TYPE_MAIN = "main";
+const PING_TYPE_DELETION = "deletion";
 
 // Session ping reasons.
 const REASON_GATHER_PAYLOAD = "gather-payload";
 const REASON_GATHER_SUBSESSION_PAYLOAD = "gather-subsession-payload";
 
 XPCOMUtils.defineLazyModuleGetter(this, "ClientID",
                                   "resource://gre/modules/ClientID.jsm");
 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
@@ -138,16 +139,17 @@ this.TelemetryController = Object.freeze
   initLogging: function() {
     configureLogging();
   },
   /**
    * Used only for testing purposes.
    */
   reset: function() {
     Impl._clientID = null;
+    Impl._detachObservers();
     TelemetryStorage.reset();
     TelemetrySend.reset();
 
     return this.setup();
   },
   /**
    * Used only for testing purposes.
    */
@@ -648,16 +650,18 @@ let Impl = {
       this._sessionRecorder.onStartup();
     }
 
     if (!this.enableTelemetryRecording()) {
       this._log.config("setupChromeProcess - Telemetry recording is disabled, skipping Chrome process setup.");
       return Promise.resolve();
     }
 
+    this._attachObservers();
+
     // For very short session durations, we may never load the client
     // id from disk.
     // We try to cache it in prefs to avoid this, even though this may
     // lead to some stale client ids.
     this._clientID = Preferences.get(PREF_CACHED_CLIENTID, null);
 
     // Delay full telemetry initialization to give the browser time to
     // run various late initializers. Otherwise our gathered memory
@@ -712,25 +716,26 @@ let Impl = {
 
   // Do proper shutdown waiting and cleanup.
   _cleanupOnShutdown: Task.async(function*() {
     if (!this._initialized) {
       return;
     }
 
     Preferences.ignore(PREF_BRANCH_LOG, configureLogging);
+    this._detachObservers();
 
     // Now do an orderly shutdown.
     try {
+      // Stop any ping sending.
+      yield TelemetrySend.shutdown();
+
       // First wait for clients processing shutdown.
       yield this._shutdownBarrier.wait();
 
-      // Stop any ping sending.
-      yield TelemetrySend.shutdown();
-
       // ... and wait for any outstanding async ping activity.
       yield this._connectionsBarrier.wait();
 
       // Perform final shutdown operations.
       yield TelemetryStorage.shutdown();
     } finally {
       // Reset state.
       this._initialized = false;
@@ -800,16 +805,47 @@ let Impl = {
       initStarted: this._initStarted,
       haveDelayedInitTask: !!this._delayedInitTask,
       shutdownBarrier: this._shutdownBarrier.state,
       connectionsBarrier: this._connectionsBarrier.state,
     };
   },
 
   /**
+   * Called whenever the FHR Upload preference changes (e.g. when user disables FHR from
+   * the preferences panel), this triggers sending the deletion ping.
+   */
+  _onUploadPrefChange: function() {
+    const uploadEnabled = Preferences.get(PREF_FHR_UPLOAD_ENABLED, false);
+    if (uploadEnabled) {
+      // There's nothing we should do if we are enabling upload.
+      return;
+    }
+    // Send the deletion ping.
+    this._log.trace("_onUploadPrefChange - Sending deletion ping.");
+    this.submitExternalPing(PING_TYPE_DELETION, {}, { addClientId: true });
+  },
+
+  _attachObservers: function() {
+    if (IS_UNIFIED_TELEMETRY) {
+      // Watch the FHR upload setting to trigger deletion pings.
+      Preferences.observe(PREF_FHR_UPLOAD_ENABLED, this._onUploadPrefChange, this);
+    }
+  },
+
+  /**
+   * Remove the preference observer to avoid leaks.
+   */
+  _detachObservers: function() {
+    if (IS_UNIFIED_TELEMETRY) {
+      Preferences.ignore(PREF_FHR_UPLOAD_ENABLED, this._onUploadPrefChange, this);
+    }
+  },
+
+  /**
    * Allows waiting for TelemetryControllers delayed initialization to complete.
    * This will complete before TelemetryController is shutting down.
    * @return {Promise} Resolved when delayed TelemetryController initialization completed.
    */
   promiseInitialized: function() {
     return this._delayedInitTaskDeferred.promise;
   },
 
--- a/toolkit/components/telemetry/TelemetrySend.jsm
+++ b/toolkit/components/telemetry/TelemetrySend.jsm
@@ -49,16 +49,18 @@ const TOPIC_IDLE_DAILY = "idle-daily";
 const TOPIC_QUIT_APPLICATION = "quit-application";
 
 // Whether the FHR/Telemetry unification features are enabled.
 // Changing this pref requires a restart.
 const IS_UNIFIED_TELEMETRY = Preferences.get(PREF_UNIFIED, false);
 
 const PING_FORMAT_VERSION = 4;
 
+const PING_TYPE_DELETION = "deletion";
+
 // We try to spread "midnight" pings out over this interval.
 const MIDNIGHT_FUZZING_INTERVAL_MS = 60 * 60 * 1000;
 // We delay sending "midnight" pings on this client by this interval.
 const MIDNIGHT_FUZZING_DELAY_MS = Math.random() * MIDNIGHT_FUZZING_INTERVAL_MS;
 
 // Timeout after which we consider a ping submission failed.
 const PING_SUBMIT_TIMEOUT_MS = 2 * 60 * 1000;
 
@@ -88,16 +90,25 @@ let Policy = {
 /**
  * Determine if the ping has the new v4 ping format or the legacy v2 one or earlier.
  */
 function isV4PingFormat(aPing) {
   return ("id" in aPing) && ("application" in aPing) &&
          ("version" in aPing) && (aPing.version >= 2);
 }
 
+/**
+ * Check if the provided ping is a deletion ping.
+ * @param {Object} aPing The ping to check.
+ * @return {Boolean} True if the ping is a deletion ping, false otherwise.
+ */
+function isDeletionPing(aPing) {
+  return isV4PingFormat(aPing) && (aPing.type == PING_TYPE_DELETION);
+}
+
 function tomorrow(date) {
   let d = new Date(date);
   d.setDate(d.getDate() + 1);
   return d;
 }
 
 /**
  * @return {String} This returns a string with the gzip compressed data.
@@ -203,27 +214,34 @@ this.TelemetrySend = {
   },
 
   /**
    * Only used in tests.
    */
   setServer: function(server) {
     return TelemetrySendImpl.setServer(server);
   },
+
+  /**
+   * Only used in tests to wait on outgoing pending pings.
+   */
+  testWaitOnOutgoingPings: function() {
+    return TelemetrySendImpl.promisePendingPingActivity();
+  },
 };
 
 let TelemetrySendImpl = {
   _sendingEnabled: false,
   _logger: null,
   // Timer for scheduled ping sends.
   _pingSendTimer: null,
   // This tracks all pending ping requests to the server.
   _pendingPingRequests: new Map(),
-  // This is a private barrier blocked by pending async ping activity (sending & saving).
-  _connectionsBarrier: new AsyncShutdown.Barrier("TelemetrySend: Waiting for pending ping activity"),
+  // This tracks all the pending async ping activity.
+  _pendingPingActivity: new Set(),
   // This is true when running in the test infrastructure.
   _testMode: false,
 
   // Count of pending pings we discarded for age on startup.
   _discardedPingsCount: 0,
   // Count of pending pings we evicted for being over the limit on startup.
   _evictedPingsCount: 0,
   // Count of pending pings that were overdue.
@@ -258,23 +276,31 @@ let TelemetrySendImpl = {
     this._discardedPingsCount = 0;
     this._evictedPingsCount = 0;
 
     Services.obs.addObserver(this, TOPIC_IDLE_DAILY, false);
 
     this._server = Preferences.get(PREF_SERVER, undefined);
 
     // If any pings were submitted before the delayed init finished
-    // we will send them now.
-    yield this._sendPersistedPings();
+    // we will send them now. We don't wait on sending as this could take some time.
+    this._sendPersistedPings();
 
     // Check the pending pings on disk now.
-    yield this._checkPendingPings();
+    let haveOverduePings = yield this._checkPendingPings();
+    if (haveOverduePings) {
+      // We don't wait on sending as this could take some time.
+      this._sendPersistedPings();
+    }
   }),
 
+  /**
+   * Discard old pings from the pending pings and detect overdue ones.
+   * @return {Boolean} True if we have overdue pings, false otherwise.
+   */
   _checkPendingPings: Task.async(function*() {
     // Scan the pending pings - that gives us a list sorted by last modified, descending.
     let infos = yield TelemetryStorage.loadPendingPingList();
     this._log.info("_checkPendingPings - pending ping count: " + infos.length);
     if (infos.length == 0) {
       this._log.trace("_checkPendingPings - no pending pings");
       return;
     }
@@ -314,39 +340,40 @@ let TelemetrySendImpl = {
     Services.telemetry.getHistogramById('TELEMETRY_FILES_EVICTED')
                       .add(evictedCount);
 
     // Check for overdue pings.
     const overduePings = infos.filter((info) =>
       (now.getTime() - info.lastModificationDate) > OVERDUE_PING_FILE_AGE);
     this._overduePingCount = overduePings.length;
 
-
     if (overduePings.length > 0) {
       this._log.trace("_checkForOverduePings - Have " + overduePings.length +
-                       " overdue pending pings, sending " + infos.length +
+                       " overdue pending pings, ready to send " + infos.length +
                        " pings now.");
-      yield this._sendPersistedPings();
+      return true;
     }
+
+    return false;
    }),
 
   shutdown: Task.async(function*() {
     for (let topic of this.OBSERVER_TOPICS) {
       Services.obs.removeObserver(this, topic);
     }
 
     // We can't send anymore now.
     this._sendingEnabled = false;
 
     // Clear scheduled ping sends.
     this._clearPingSendTimer();
     // Cancel any outgoing requests.
     yield this._cancelOutgoingRequests();
     // ... and wait for any outstanding async ping activity.
-    yield this._connectionsBarrier.wait();
+    yield this.promisePendingPingActivity();
   }),
 
   reset: function() {
     this._log.trace("reset");
 
     this._overduePingCount = 0;
     this._discardedPingsCount = 0;
     this._evictedPingsCount = 0;
@@ -365,17 +392,17 @@ let TelemetrySendImpl = {
     switch(topic) {
     case TOPIC_IDLE_DAILY:
       this._sendPersistedPings();
       break;
     }
   },
 
   submitPing: function(ping) {
-    if (!this._canSend()) {
+    if (!this._canSend(ping)) {
       this._log.trace("submitPing - Telemetry is not allowed to send pings.");
       return Promise.resolve();
     }
 
     // Check if we can send pings now.
     const now = Policy.now();
     const nextPingSendTime = this._getNextPingSendTime(now);
     const throttled = (nextPingSendTime > now.getTime());
@@ -545,17 +572,17 @@ let TelemetrySendImpl = {
       }
     }
 
     let slug = pathComponents.join("/");
     return "/submit/telemetry/" + slug;
   },
 
   _doPing: function(ping, id, isPersisted) {
-    if (!this._canSend()) {
+    if (!this._canSend(ping)) {
       // We can't send the pings to the server, so don't try to.
       this._log.trace("_doPing - Sending is disabled.");
       return Promise.resolve();
     }
 
     this._log.trace("_doPing - server: " + this._server + ", persisted: " + isPersisted +
                     ", id: " + id);
     const isNewPing = isV4PingFormat(ping);
@@ -649,30 +676,36 @@ let TelemetrySendImpl = {
     startTime = new Date();
     request.send(payloadStream);
 
     return deferred.promise;
   },
 
   /**
    * Check if pings can be sent to the server. If FHR is not allowed to upload,
-   * pings are not sent to the server (Telemetry is a sub-feature of FHR).
+   * pings are not sent to the server (Telemetry is a sub-feature of FHR). If trying
+   * to send a deletion ping, don't block it.
    * If unified telemetry is off, don't send pings if Telemetry is disabled.
    *
+   * @param {Object} [ping=null] A ping to be checked.
    * @return {Boolean} True if pings can be send to the servers, false otherwise.
    */
-  _canSend: function() {
+  _canSend: function(ping = null) {
     // We only send pings from official builds, but allow overriding this for tests.
     if (!Telemetry.isOfficialTelemetry && !this._testMode) {
       return false;
     }
 
     // With unified Telemetry, the FHR upload setting controls whether we can send pings.
     // The Telemetry pref enables sending extended data sets instead.
     if (IS_UNIFIED_TELEMETRY) {
+      // Deletion pings are sent even if the upload is disabled.
+      if (ping && isDeletionPing(ping)) {
+        return true;
+      }
       return Preferences.get(PREF_FHR_UPLOAD_ENABLED, false);
     }
 
     // Without unified Telemetry, the Telemetry enabled pref controls ping sending.
     return Preferences.get(PREF_TELEMETRY_ENABLED, false);
   },
 
   _reschedulePingSendTimer: function(timestamp) {
@@ -688,11 +721,25 @@ let TelemetrySendImpl = {
     }
   },
 
   /**
    * Track any pending ping send and save tasks through the promise passed here.
    * This is needed to block shutdown on any outstanding ping activity.
    */
   _trackPendingPingTask: function (promise) {
-    this._connectionsBarrier.client.addBlocker("Waiting for ping task", promise);
+    let clear = () => this._pendingPingActivity.delete(promise);
+    promise.then(clear, clear);
+    this._pendingPingActivity.add(promise);
+  },
+
+  /**
+   * Return a promise that allows to wait on pending pings.
+   * @return {Object<Promise>} A promise resolved when all the pending pings promises
+   *         are resolved.
+   */
+  promisePendingPingActivity: function () {
+    this._log.trace("promisePendingPingActivity - Waiting for ping task");
+    return Promise.all([for (p of this._pendingPingActivity) p.catch(ex => {
+      this._log.error("promisePendingPingActivity - ping activity had an error", ex);
+    })]);
   },
 };
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/docs/deletion-ping.rst
@@ -0,0 +1,17 @@
+
+"deletion" ping
+===============
+
+This ping is generated when a user turns off FHR upload from the Preferences panel, changing the related ``datareporting.healthreport.uploadEnabled`` preference. This requests that all associated data from that user be deleted.
+
+This ping contains the client id and no environment data.
+
+Structure::
+
+    {
+      version: 4,
+      type: "deletion",
+      ... common ping data
+      clientId: <UUID>,
+      payload: { }
+    }
\ No newline at end of file
--- a/toolkit/components/telemetry/docs/index.rst
+++ b/toolkit/components/telemetry/docs/index.rst
@@ -14,9 +14,10 @@ Client-side, this consists of:
 
 .. toctree::
    :maxdepth: 2
 
    pings
    common-ping
    environment
    main-ping
+   deletion-ping
    preferences
--- a/toolkit/components/telemetry/docs/pings.rst
+++ b/toolkit/components/telemetry/docs/pings.rst
@@ -26,19 +26,20 @@ The telemetry server team is working tow
 * `5XX` - there was a server-side error, the client should try to resubmit later
 
 Ping types
 ==========
 
 * :doc:`main <main-ping>` - contains the information collected by Telemetry (Histograms, hang stacks, ...)
 * :doc:`saved-session <main-ping>` - has the same format as a main ping, but it contains the *"classic"* Telemetry payload with measurements covering the whole browser session. This is only a separate type to make storage of saved-session easier server-side. This is temporary and will be removed soon.
 * :doc:`crash <crash-ping>` - a ping that is captured and sent after Firefox crashes.
+* :doc:`uitour-ping` - a ping submitted via the UITour API
 * ``activation`` - *planned* - sent right after installation or profile creation
 * ``upgrade`` - *planned* - sent right after an upgrade
-* ``deletion`` - *planned* - on opt-out we may have to tell the server to delete user data
+* :doc:`deletion <deletion-ping>` - sent when FHR upload is disabled, requesting deletion of the data associated with this user
 
 Archiving
 =========
 
 When archiving is enabled through the relative preference, pings submitted to ``TelemetryController`` are also stored locally in the user profile directory, in `<profile-dir>/datareporting/archived`.
 
 To allow for cheaper lookup of archived pings, storage follows a specific naming scheme for both the directory and the ping file name: `<YYYY-MM>/<timestamp>.<UUID>.<type>.json`.
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/docs/uitour-ping.rst
@@ -0,0 +1,24 @@
+
+"uitour-tag" ping
+=================
+
+This ping is submitted via the UITour setTreatmentTag API. It may be used by
+the tour to record what settings were made by a user or to track the result of
+A/B experiments.
+
+The client ID is submitted with this ping.
+
+Structure::
+
+    {
+      version: 1,
+      type: "uitour-tag",
+      clientId: <string>,
+      payload: {
+        tagName: <string>,
+        tagValue: <string>
+      }
+    }
+
+See also: :doc:`common ping fields <common-ping>`
+
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -16,16 +16,17 @@ Cu.import("resource://gre/modules/Teleme
 Cu.import("resource://gre/modules/TelemetryStorage.jsm", this);
 Cu.import("resource://gre/modules/TelemetrySend.jsm", this);
 Cu.import("resource://gre/modules/TelemetryArchive.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 Cu.import("resource://gre/modules/Promise.jsm", this);
 Cu.import("resource://gre/modules/Preferences.jsm");
 
 const PING_FORMAT_VERSION = 4;
+const DELETION_PING_TYPE = "deletion";
 const TEST_PING_TYPE = "test-ping-type";
 
 const PLATFORM_VERSION = "1.9.2";
 const APP_VERSION = "1";
 const APP_NAME = "XPCShell";
 
 const PREF_BRANCH = "toolkit.telemetry.";
 const PREF_ENABLED = PREF_BRANCH + "enabled";
@@ -169,16 +170,35 @@ add_task(function* test_simplePing() {
   // Make sure the version in the query string matches the new ping format version.
   let params = request.queryString.split("&");
   Assert.ok(params.find(p => p == ("v=" + PING_FORMAT_VERSION)));
 
   let ping = decodeRequestPayload(request);
   checkPingFormat(ping, TEST_PING_TYPE, false, false);
 });
 
+add_task(function* test_deletionPing() {
+  const isUnified = Preferences.get(PREF_UNIFIED, false);
+  if (!isUnified) {
+    // Skipping the test if unified telemetry is off, as no deletion ping will
+    // be generated.
+    return;
+  }
+
+  // Disable FHR upload: this should trigger a deletion ping.
+  Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);
+
+  let request = yield gRequestIterator.next();
+  let ping = decodeRequestPayload(request);
+  checkPingFormat(ping, DELETION_PING_TYPE, true, false);
+
+  // Restore FHR Upload.
+  Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);
+});
+
 add_task(function* test_pingHasClientId() {
   // Send a ping with a clientId.
   yield sendPing(true, false);
 
   let request = yield gRequestIterator.next();
   let ping = decodeRequestPayload(request);
   checkPingFormat(ping, TEST_PING_TYPE, true, false);
 
@@ -226,16 +246,24 @@ add_task(function* test_archivePings() {
 
   // Disable ping upload so that pings don't get sent.
   // With unified telemetry the FHR upload pref controls this,
   // with non-unified telemetry the Telemetry enabled pref.
   const isUnified = Preferences.get(PREF_UNIFIED, false);
   const uploadPref = isUnified ? PREF_FHR_UPLOAD_ENABLED : PREF_ENABLED;
   Preferences.set(uploadPref, false);
 
+  // If we're using unified telemetry, disabling ping upload will generate a "deletion"
+  // ping. Catch it.
+  if (isUnified) {
+    let request = yield gRequestIterator.next();
+    let ping = decodeRequestPayload(request);
+    checkPingFormat(ping, DELETION_PING_TYPE, true, false);
+  }
+
   // Register a new Ping Handler that asserts if a ping is received, then send a ping.
   registerPingHandler(() => Assert.ok(false, "Telemetry must not send pings if not allowed to."));
   let pingId = yield sendPing(true, true);
 
   // Check that the ping was archived, even with upload disabled.
   let ping = yield TelemetryArchive.promiseArchivedPingById(pingId);
   Assert.equal(ping.id, pingId, "TelemetryController should still archive pings.");
 
--- a/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
@@ -314,16 +314,17 @@ add_task(function* test_overdue_old_form
   yield TelemetryStorage.savePing(PING_NO_PAYLOAD, true);
   yield TelemetryStorage.savePingToFile(PING_NO_SLUG, PING_FILES_PATHS[3], true);
 
   for (let f in PING_FILES_PATHS) {
     yield File.setDates(PING_FILES_PATHS[f], null, Date.now() - OVERDUE_PING_FILE_AGE);
   }
 
   yield TelemetryController.reset();
+  yield TelemetrySend.testWaitOnOutgoingPings();
   assertReceivedPings(OLD_FORMAT_PINGS);
 
   // |TelemetryStorage.cleanup| doesn't know how to remove a ping with no slug or id,
   // so remove it manually so that the next test doesn't fail.
   yield OS.File.remove(PING_FILES_PATHS[3]);
 
   yield clearPendingPings();
 });
@@ -340,16 +341,17 @@ add_task(function* test_overdue_pings_tr
     { num: OVERDUE_PINGS, age: OVERDUE_PING_FILE_AGE },
   ];
   let pings = yield createSavedPings(pingTypes);
   let recentPings = pings.slice(0, RECENT_PINGS);
   let expiredPings = pings.slice(RECENT_PINGS, RECENT_PINGS + EXPIRED_PINGS);
   let overduePings = pings.slice(-OVERDUE_PINGS);
 
   yield TelemetryController.reset();
+  yield TelemetrySend.testWaitOnOutgoingPings();
   assertReceivedPings(TOTAL_EXPECTED_PINGS);
 
   yield assertNotSaved(recentPings);
   yield assertNotSaved(expiredPings);
   yield assertNotSaved(overduePings);
 
   yield clearPendingPings();
 });
@@ -393,16 +395,17 @@ add_task(function* test_overdue_old_form
     // Make sure the version in the query string matches the old ping format version.
     let params = request.queryString.split("&");
     Assert.ok(params.find(p => p == "v=1"));
 
     receivedPings++;
   });
 
   yield TelemetryController.reset();
+  yield TelemetrySend.testWaitOnOutgoingPings();
   Assert.equal(receivedPings, 1, "We must receive a ping in the old format.");
 
   yield clearPendingPings();
 });
 
 add_task(function* teardown() {
   yield stopHttpServer();
 });
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -207,16 +207,19 @@ PopupNotifications.prototype = {
    * @param secondaryActions
    *        An optional JavaScript array describing the notification's alternate
    *        actions. The array should contain objects with the same properties
    *        as mainAction. These are used to populate the notification button's
    *        dropdown menu.
    * @param options
    *        An options JavaScript object holding additional properties for the
    *        notification. The following properties are currently supported:
+   *        origin:      A string representing the origin of the site presenting
+   *                     a notification so it can be shown to the user (possibly
+   *                     with a favicon). e.g. https://example.com:8080
    *        persistence: An integer. The notification will not automatically
    *                     dismiss for this many page loads.
    *        timeout:     A time in milliseconds. The notification will not
    *                     automatically dismiss before this time.
    *        persistWhileVisible:
    *                     A boolean. If true, a visible notification will always
    *                     persist across location changes.
    *        dismissed:   Whether the notification should be added as a dismissed