Merge m-c to inbound, a=merge CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Tue, 12 Apr 2016 15:36:02 -0700
changeset 330803 3e3dd1575611181dc179316703e105622cdecbb6
parent 330802 b9b725d135289eed40c18112555d7b009ac9ee47 (current diff)
parent 330751 fb921246e2d60f521f83defed54e30a38df1be3e (diff)
child 330804 81d0c00bb82f3c62dc279d31199f168c9c648fb6
push id6048
push userkmoir@mozilla.com
push dateMon, 06 Jun 2016 19:02:08 +0000
treeherdermozilla-beta@46d72a56c57d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone48.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to inbound, a=merge CLOSED TREE MozReview-Commit-ID: 8m3KF4mqAKl
--- a/.eslintignore
+++ b/.eslintignore
@@ -99,16 +99,17 @@ devtools/client/projecteditor/**
 devtools/client/promisedebugger/**
 devtools/client/responsivedesign/**
 devtools/client/scratchpad/**
 devtools/client/shadereditor/**
 devtools/client/shared/**
 devtools/client/sourceeditor/**
 devtools/client/webaudioeditor/**
 devtools/client/webconsole/**
+!devtools/client/webconsole/panel.js
 devtools/client/webide/**
 devtools/server/**
 devtools/shared/*.js
 devtools/shared/*.jsm
 devtools/shared/apps/**
 devtools/shared/client/**
 devtools/shared/discovery/**
 devtools/shared/gcli/**
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1836,16 +1836,30 @@ function loadOneOrMoreURIs(aURIString)
   try {
     gBrowser.loadTabs(aURIString.split("|"), false, true);
   }
   catch (e) {
   }
 }
 
 function focusAndSelectUrlBar() {
+  // In customize mode, the url bar is disabled. If a new tab is opened or the
+  // user switches to a different tab, this function gets called before we've
+  // finished leaving customize mode, and the url bar will still be disabled.
+  // We can't focus it when it's disabled, so we need to re-run ourselves when
+  // we've finished leaving customize mode.
+  if (CustomizationHandler.isExitingCustomizeMode) {
+    gNavToolbox.addEventListener("aftercustomization", function afterCustomize() {
+      gNavToolbox.removeEventListener("aftercustomization", afterCustomize);
+      focusAndSelectUrlBar();
+    });
+
+    return true;
+  }
+
   if (gURLBar) {
     if (window.fullScreen)
       FullScreen.showNavToolbox();
 
     gURLBar.select();
     if (document.activeElement == gURLBar.inputField)
       return true;
   }
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -467,17 +467,17 @@ nsContextMenu.prototype = {
     var onMedia = (this.onVideo || this.onAudio);
     // Several mutually exclusive items... play/pause, mute/unmute, show/hide
     this.showItem("context-media-play",  onMedia && (this.target.paused || this.target.ended));
     this.showItem("context-media-pause", onMedia && !this.target.paused && !this.target.ended);
     this.showItem("context-media-mute",   onMedia && !this.target.muted);
     this.showItem("context-media-unmute", onMedia && this.target.muted);
     this.showItem("context-media-playbackrate", onMedia);
     this.showItem("context-media-showcontrols", onMedia && !this.target.controls);
-    this.showItem("context-media-hidecontrols", onMedia && this.target.controls);
+    this.showItem("context-media-hidecontrols", this.target.controls && (this.onVideo || (this.onAudio && !this.inSyntheticDoc)));
     this.showItem("context-video-fullscreen", this.onVideo && this.target.ownerDocument.fullscreenElement == null);
     var statsShowing = this.onVideo && this.target.mozMediaStatisticsShowing;
     this.showItem("context-video-showstats", this.onVideo && this.target.controls && !statsShowing);
     this.showItem("context-video-hidestats", this.onVideo && this.target.controls && statsShowing);
     this.showItem("context-media-eme-learnmore", this.onDRMMedia);
     this.showItem("context-media-eme-separator", this.onDRMMedia);
 
     // Disable them when there isn't a valid media source loaded.
--- a/browser/base/content/test/general/browser_contextmenu.js
+++ b/browser/base/content/test/general/browser_contextmenu.js
@@ -21,18 +21,26 @@ add_task(function* test_setup() {
   const chrome_base = "chrome://mochitests/content/browser/browser/base/content/test/general/";
   const contextmenu_common = chrome_base + "contextmenu_common.js";
   Services.scriptloader.loadSubScript(contextmenu_common, this);
 
   yield ContentTask.spawn(gBrowser.selectedBrowser, null, function*() {
     let doc = content.document;
     let videoIframe = doc.querySelector("#test-video-in-iframe");
     let video = videoIframe.contentDocument.querySelector("video");
+    let awaitPause = ContentTaskUtils.waitForEvent(video, "pause");
     video.pause();
-    yield ContentTaskUtils.waitForCondition(() => video.paused, "iframe video should be paused");
+    yield awaitPause;
+
+    let audioIframe = doc.querySelector("#test-audio-in-iframe");
+    // media documents always use a <video> tag.
+    let audio = audioIframe.contentDocument.querySelector("video");
+    awaitPause = ContentTaskUtils.waitForEvent(audio, "pause");
+    audio.pause();
+    yield awaitPause;
   });
 });
 
 let plainTextItems;
 add_task(function* test_plaintext() {
   plainTextItems = ["context-navigation",   null,
                         ["context-back",         false,
                          "context-forward",      false,
@@ -257,16 +265,46 @@ add_task(function* test_video_in_iframe(
           "context-saveframe",         true,
           "---",                       null,
           "context-printframe",        true,
           "---",                       null,
           "context-viewframeinfo",     true], null]
   );
 });
 
+add_task(function* test_audio_in_iframe() {
+  yield test_contextmenu("#test-audio-in-iframe",
+    ["context-media-play",         true,
+     "context-media-mute",         true,
+     "context-media-playbackrate", null,
+         ["context-media-playbackrate-050x", true,
+          "context-media-playbackrate-100x", true,
+          "context-media-playbackrate-150x", true,
+          "context-media-playbackrate-200x", true], null,
+     "---",                        null,
+     "context-copyaudiourl",       true,
+     "---",                        null,
+     "context-saveaudio",          true,
+     "context-sendaudio",          true,
+     "frame",                null,
+         ["context-showonlythisframe", true,
+          "context-openframeintab",    true,
+          "context-openframe",         true,
+          "---",                       null,
+          "context-reloadframe",       true,
+          "---",                       null,
+          "context-bookmarkframe",     true,
+          "context-saveframe",         true,
+          "---",                       null,
+          "context-printframe",        true,
+          "---",                       null,
+          "context-viewframeinfo",     true], null]
+  );
+});
+
 add_task(function* test_image_in_iframe() {
   yield test_contextmenu("#test-image-in-iframe",
     ["context-viewimage",            true,
      "context-copyimage-contents",   true,
      "context-copyimage",            true,
      "---",                          null,
      "context-saveimage",            true,
      "context-sendimage",            true,
--- a/browser/base/content/test/general/browser_search_favicon.js
+++ b/browser/base/content/test/general/browser_search_favicon.js
@@ -48,13 +48,12 @@ add_task(function*() {
 
   let urlHbox = result._urlText.parentNode.parentNode;
   ok(urlHbox.classList.contains("ac-url"), "URL hbox sanity check");
   is_element_hidden(urlHbox, "URL element should be hidden");
 
   let actionHbox = result._actionText.parentNode.parentNode;
   ok(actionHbox.classList.contains("ac-action"), "Action hbox sanity check");
   is_element_hidden(actionHbox, "Action element should be hidden because it is not selected");
-  // \u2014 == em dash
-  is(result._actionText.textContent, "\u2014Search with SearchEngine", "Action text should be as expected");
+  is(result._actionText.textContent, "Search with SearchEngine", "Action text should be as expected");
 
   gBrowser.removeCurrentTab();
 });
--- a/browser/base/content/test/general/browser_urlbarDecode.js
+++ b/browser/base/content/test/general/browser_urlbarDecode.js
@@ -89,11 +89,10 @@ function* checkInput(inputStr) {
 
   let itemTypeStr = item.getAttribute("type");
   let itemTypes = itemTypeStr.split(" ").sort();
   Assert.equal(itemTypes.toString(),
                ["action", "heuristic", "visiturl"].toString(),
                "type");
 
   Assert.equal(item._titleText.textContent, inputStr.replace("\\","/"), "Visible title");
-  // \u2014 == em dash
-  Assert.equal(item._actionText.textContent, "\u2014Visit", "Visible action");
+  Assert.equal(item._actionText.textContent, "Visit", "Visible action");
 }
--- a/browser/base/content/test/general/subtst_contextmenu.html
+++ b/browser/base/content/test/general/subtst_contextmenu.html
@@ -15,16 +15,17 @@ Browser context menu subtest.
 <video controls id="test-video-ok"  src="video.ogg" width="100" height="100" style="background-color: green"></video>
 <video id="test-audio-in-video" src="audio.ogg" width="100" height="100" style="background-color: red"></video>
 <video controls id="test-video-bad" src="bogus.duh" width="100" height="100" style="background-color: orange"></video>
 <video controls id="test-video-bad2" width="100" height="100" style="background-color: yellow">
   <source src="bogus.duh" type="video/durrrr;">
 </video>
 <iframe id="test-iframe" width="98"  height="98" style="border: 1px solid black"></iframe>
 <iframe id="test-video-in-iframe" src="video.ogg" width="98" height="98" style="border: 1px solid black"></iframe>
+<iframe id="test-audio-in-iframe" src="audio.ogg" width="98" height="98" style="border: 1px solid black"></iframe>
 <iframe id="test-image-in-iframe" src="ctxmenu-image.png" width="98" height="98" style="border: 1px solid black"></iframe>
 <textarea id="test-textarea">chssseesbbbie</textarea> <!-- a weird word which generates only one suggestion -->
 <div id="test-contenteditable" contenteditable="true">chssseefsbbbie</div> <!-- a more weird word which generates no suggestions -->
 <div id="test-contenteditable-spellcheck-false" contenteditable="true" spellcheck="false">test</div> <!-- No Check Spelling menu item -->
 <div id="test-dom-full-screen">DOM full screen FTW</div>
 <div contextmenu="myMenu">
   <p id="test-pagemenu" hopeless="true">I've got a context menu!</p>
   <menu id="myMenu" type="context">
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -1136,23 +1136,25 @@ notification[value="translation"] menuli
 html|span.ac-tag {
   background-color: MenuText;
   color: Menu;
   border-radius: 2px;
   border: 1px solid transparent;
   padding: 0 1px;
 }
 
+.ac-separator,
 .ac-url,
 .ac-action {
   font-size: 12px;
   color: -moz-nativehyperlinktext;
 }
 
 .ac-title[selected=true],
+.ac-separator[selected],
 .ac-url[selected=true],
 .ac-action[selected=true] {
   color: inherit !important;
 }
 
 .ac-tags-text[selected] > html|span.ac-tag {
   background-color: HighlightText;
   color: Highlight;
--- a/browser/themes/linux/preferences/in-content/dialog.css
+++ b/browser/themes/linux/preferences/in-content/dialog.css
@@ -1,13 +1,13 @@
 /* - This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this file,
    - You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 %include ../../../shared/incontentprefs/dialog.inc.css
 
-label,
+label:not(.menu-text),
 textbox,
 description,
 .tab-text,
 caption > label {
   font-size: 1.2em;
 }
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1756,42 +1756,45 @@ html|span.ac-tag {
   border: 1px solid transparent;
   padding: 0 1px;
 }
 
 html|span.ac-emphasize-text-tag {
   color: hsl(0, 0%, 50%);
 }
 
+.ac-separator,
+.ac-url,
+.ac-action {
+  font-size: 12px;
+}
+
+.ac-separator {
+  color: hsl(0, 0%, 50%);
+}
+
 .ac-url {
-  font-size: 12px;
   color: hsl(210, 77%, 47%);
 }
 
 html|span.ac-emphasize-text-url {
   color: hsl(210, 86%, 64%);
 }
 
 .ac-action {
-  font-size: 12px;
   color: hsl(178, 100%, 28%);
 }
 
-html|span.ac-title-urlaction-separator {
-  color: hsl(0, 0%, 50%);
-}
-
 .ac-title[selected],
+.ac-separator[selected],
 .ac-url[selected],
 .ac-action[selected],
 .ac-title-text[selected] > html|span.ac-emphasize-text,
 .ac-url-text[selected] > html|span.ac-emphasize-text,
-.ac-action-text[selected] > html|span.ac-emphasize-text,
-.ac-url-text[selected] > html|span.ac-title-urlaction-separator,
-.ac-action-text[selected] > html|span.ac-title-urlaction-separator {
+.ac-action-text[selected] > html|span.ac-emphasize-text {
   color: hsl(0, 0%, 100%);
 }
 
 .ac-tags-text[selected] > html|span.ac-tag {
   background-color: hsl(0, 0%, 100%);
   color: hsl(210, 80%, 40%);
 }
 
--- a/browser/themes/osx/preferences/in-content/dialog.css
+++ b/browser/themes/osx/preferences/in-content/dialog.css
@@ -4,17 +4,17 @@
 
 %include ../../../shared/incontentprefs/dialog.inc.css
 
 prefwindow,
 .windowDialog {
   font: message-box !important;
 }
 
-label,
+label:not(.menu-text),
 textbox,
 description,
 .tab-text,
 caption > label {
   font-size: 1.3em;
 }
 
 button {
--- a/browser/themes/windows/browser-aero.css
+++ b/browser/themes/windows/browser-aero.css
@@ -325,18 +325,21 @@
     }
   }
 
   #main-window[darkwindowframe="true"] #toolbar-menubar:not(:-moz-lwtheme):not(:-moz-window-inactive),
   #main-window[darkwindowframe="true"] #TabsToolbar:not(:-moz-lwtheme):not(:-moz-window-inactive) {
     color: white;
   }
 
-  #toolbar-menubar:not(:-moz-lwtheme) {
-    text-shadow: 0 0 .5em white, 0 0 .5em white, 0 1px 0 rgba(255,255,255,.4);
+  @media (-moz-os-version: windows-vista),
+         (-moz-os-version: windows-win7),{
+    #toolbar-menubar:not(:-moz-lwtheme) {
+      text-shadow: 0 0 .5em white, 0 0 .5em white, 0 1px 0 rgba(255,255,255,.4);
+    }
   }
 
   /* Show borders on vista through win8, but not on win10 and later: */
   @media (-moz-os-version: windows-vista),
          (-moz-os-version: windows-win7),
          (-moz-os-version: windows-win8) {
     /* Vertical toolbar border */
     #main-window:not([customizing])[sizemode=normal] #navigator-toolbox:not(:-moz-lwtheme)::after,
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1444,42 +1444,45 @@ html|span.ac-tag {
   border: 1px solid transparent;
   padding: 0 1px;
 }
 
 html|span.ac-emphasize-text-tag {
   color: hsl(0, 0%, 50%);
 }
 
+.ac-separator,
+.ac-url,
+.ac-action {
+  font-size: 12px;
+}
+
+.ac-separator {
+  color: hsl(0, 0%, 50%);
+}
+
 .ac-url {
-  font-size: 12px;
   color: hsl(210, 77%, 47%);
 }
 
 html|span.ac-emphasize-text-url {
   color: hsl(210, 86%, 64%);
 }
 
 .ac-action {
-  font-size: 12px;
   color: hsl(178, 100%, 28%);
 }
 
-html|span.ac-title-urlaction-separator {
-  color: hsl(0, 0%, 50%);
-}
-
 .ac-title[selected=true],
+.ac-separator[selected],
 .ac-url[selected=true],
 .ac-action[selected=true],
 .ac-title-text[selected=true] > html|span.ac-emphasize-text,
 .ac-url-text[selected=true] > html|span.ac-emphasize-text,
-.ac-action-text[selected=true] > html|span.ac-emphasize-text,
-.ac-url-text[selected=true] > html|span.ac-title-urlaction-separator,
-.ac-action-text[selected=true] > html|span.ac-title-urlaction-separator {
+.ac-action-text[selected=true] > html|span.ac-emphasize-text {
   color: hsl(0, 0%, 100%);
 }
 
 .ac-tags-text[selected] > html|span.ac-tag {
   background-color: hsl(0, 0%, 100%);
   color: hsl(210, 80%, 40%);
 }
 
@@ -1498,20 +1501,20 @@ html|span.ac-title-urlaction-separator {
   }
 
   html|span.ac-tag,
   html|span.ac-emphasize-text-tag {
     background-color: -moz-FieldText;
     color: -moz-Field;
   }
 
+  .ac-separator,
   .ac-url,
   .ac-action,
-  html|span.ac-emphasize-text-url,
-  html|span.ac-title-urlaction-separator {
+  html|span.ac-emphasize-text-url {
     color: -moz-nativehyperlinktext;
   }
 
   .ac-tags-text[selected] > html|span.ac-tag,
   .ac-tags-text[selected] > html|span.ac-tag > html|span.ac-emphasize-text-tag {
     background-color: HighlightText;
     color: Highlight;
   }
--- a/browser/themes/windows/preferences/in-content/dialog.css
+++ b/browser/themes/windows/preferences/in-content/dialog.css
@@ -1,13 +1,13 @@
 /* - This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this file,
    - You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 %include ../../../shared/incontentprefs/dialog.inc.css
 
-label,
+label:not(.menu-text),
 textbox,
 description,
 .tab-text,
 caption > label {
   font-size: 1.2em;
 }
--- a/devtools/client/webconsole/panel.js
+++ b/devtools/client/webconsole/panel.js
@@ -1,121 +1,113 @@
 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set ft= javascript ts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const {Cc, Ci, Cu} = require("chrome");
+const {Cu} = require("chrome");
 const promise = require("promise");
 
 loader.lazyGetter(this, "HUDService", () => require("devtools/client/webconsole/hudservice"));
 loader.lazyGetter(this, "EventEmitter", () => require("devtools/shared/event-emitter"));
 
 /**
  * A DevToolPanel that controls the Web Console.
  */
-function WebConsolePanel(iframeWindow, toolbox)
-{
+function WebConsolePanel(iframeWindow, toolbox) {
   this._frameWindow = iframeWindow;
   this._toolbox = toolbox;
   EventEmitter.decorate(this);
 }
 
 exports.WebConsolePanel = WebConsolePanel;
 
 WebConsolePanel.prototype = {
   hud: null,
 
   /**
    * Called by the WebConsole's onkey command handler.
    * If the WebConsole is opened, check if the JSTerm's input line has focus.
    * If not, focus it.
    */
-  focusInput: function WCP_focusInput()
-  {
+  focusInput: function() {
     this.hud.jsterm.focus();
   },
 
   /**
    * Open is effectively an asynchronous constructor.
    *
    * @return object
    *         A promise that is resolved when the Web Console completes opening.
    */
-  open: function WCP_open()
-  {
+  open: function() {
     let parentDoc = this._toolbox.doc;
     let iframe = parentDoc.getElementById("toolbox-panel-iframe-webconsole");
 
     // Make sure the iframe content window is ready.
     let deferredIframe = promise.defer();
     let win, doc;
     if ((win = iframe.contentWindow) &&
         (doc = win.document) &&
         doc.readyState == "complete") {
       deferredIframe.resolve(null);
-    }
-    else {
+    } else {
       iframe.addEventListener("load", function onIframeLoad() {
         iframe.removeEventListener("load", onIframeLoad, true);
         deferredIframe.resolve(null);
       }, true);
     }
 
     // Local debugging needs to make the target remote.
     let promiseTarget;
     if (!this.target.isRemote) {
       promiseTarget = this.target.makeRemote();
-    }
-    else {
+    } else {
       promiseTarget = promise.resolve(this.target);
     }
 
     // 1. Wait for the iframe to load.
     // 2. Wait for the remote target.
     // 3. Open the Web Console.
     return deferredIframe.promise
       .then(() => promiseTarget)
-      .then((aTarget) => {
-        this._frameWindow._remoteTarget = aTarget;
+      .then((target) => {
+        this._frameWindow._remoteTarget = target;
 
         let webConsoleUIWindow = iframe.contentWindow.wrappedJSObject;
         let chromeWindow = iframe.ownerDocument.defaultView;
         return HUDService.openWebConsole(this.target, webConsoleUIWindow,
                                          chromeWindow);
       })
-      .then((aWebConsole) => {
-        this.hud = aWebConsole;
+      .then((webConsole) => {
+        this.hud = webConsole;
         this._isReady = true;
         this.emit("ready");
         return this;
-      }, (aReason) => {
+      }, (reason) => {
         let msg = "WebConsolePanel open failed. " +
-                  aReason.error + ": " + aReason.message;
+                  reason.error + ": " + reason.message;
         dump(msg + "\n");
         Cu.reportError(msg);
       });
   },
 
-  get target()
-  {
+  get target() {
     return this._toolbox.target;
   },
 
   _isReady: false,
-  get isReady()
-  {
+  get isReady() {
     return this._isReady;
   },
 
-  destroy: function WCP_destroy()
-  {
+  destroy: function() {
     if (this._destroyer) {
       return this._destroyer;
     }
 
     this._destroyer = this.hud.destroy();
     this._destroyer.then(() => this.emit("destroyed"));
 
     return this._destroyer;
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -154,16 +154,20 @@
         </activity-alias>
 
         <service android:name="org.mozilla.gecko.GeckoService" />
 
         <activity android:name="org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt"
                   android:launchMode="singleTop"
                   android:theme="@style/OverlayActivity" />
 
+        <activity android:name="org.mozilla.gecko.promotion.HomeScreenPrompt"
+                  android:launchMode="singleTop"
+                  android:theme="@style/OverlayActivity" />
+
         <!-- The main reason for the Tab Queue build flag is to not mess with the VIEW intent filter
              before the rest of the plumbing is in place -->
 
         <service android:name="org.mozilla.gecko.tabqueue.TabQueueService" />
 
         <activity android:name="org.mozilla.gecko.tabqueue.TabQueuePrompt"
                   android:launchMode="singleTop"
                   android:theme="@style/OverlayActivity" />
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -50,16 +50,17 @@ import org.mozilla.gecko.javaaddons.Java
 import org.mozilla.gecko.menu.GeckoMenu;
 import org.mozilla.gecko.menu.GeckoMenuItem;
 import org.mozilla.gecko.mozglue.ContextUtils;
 import org.mozilla.gecko.mozglue.ContextUtils.SafeIntent;
 import org.mozilla.gecko.overlays.ui.ShareDialog;
 import org.mozilla.gecko.permissions.Permissions;
 import org.mozilla.gecko.preferences.ClearOnShutdownPref;
 import org.mozilla.gecko.preferences.GeckoPreferences;
+import org.mozilla.gecko.promotion.AddToHomeScreenPromotion;
 import org.mozilla.gecko.prompts.Prompt;
 import org.mozilla.gecko.prompts.PromptListItem;
 import org.mozilla.gecko.reader.SavedReaderViewHelper;
 import org.mozilla.gecko.reader.ReaderModeUtils;
 import org.mozilla.gecko.reader.ReadingListHelper;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.restrictions.RestrictedProfileConfiguration;
 import org.mozilla.gecko.restrictions.Restrictions;
@@ -225,16 +226,17 @@ public class BrowserApp extends GeckoApp
     private ToolbarProgressView mProgressView;
     private FirstrunAnimationContainer mFirstrunAnimationContainer;
     private HomePager mHomePager;
     private TabsPanel mTabsPanel;
     private ViewGroup mHomePagerContainer;
     private ActionModeCompat mActionMode;
     private TabHistoryController tabHistoryController;
     private ZoomedView mZoomedView;
+    private AddToHomeScreenPromotion mAddToHomeScreenPromotion;
 
     private static final int GECKO_TOOLS_MENU = -1;
     private static final int ADDON_MENU_OFFSET = 1000;
     public static final String TAB_HISTORY_FRAGMENT_TAG = "tabHistoryFragment";
 
     private static class MenuItemInfo {
         public int id;
         public String label;
@@ -774,16 +776,18 @@ public class BrowserApp extends GeckoApp
                        .andFallback(new Runnable() {
                            @Override
                            public void run() {
                                showUpdaterPermissionSnackbar();
                            }
                        })
                       .run();
         }
+
+        mAddToHomeScreenPromotion = new AddToHomeScreenPromotion(this);
     }
 
     /**
      * Initializes the default Switchboard URLs the first time.
      * @param intent
      */
     private void initSwitchboard(Intent intent) {
         if (Experiments.isDisabled(new SafeIntent(intent)) || !AppConstants.MOZ_SWITCHBOARD) {
@@ -1011,30 +1015,34 @@ public class BrowserApp extends GeckoApp
         }
 
         EventDispatcher.getInstance().unregisterGeckoThreadListener((GeckoEventListener) this,
             "Prompt:ShowTop");
 
         processTabQueue();
 
         mScreenshotObserver.start();
+
+        mAddToHomeScreenPromotion.resume();
     }
 
     @Override
     public void onPause() {
         super.onPause();
 
         // Needed for Adjust to get accurate session measurements
         AdjustConstants.getAdjustHelper().onPause();
 
         // Register for Prompt:ShowTop so we can foreground this activity even if it's hidden.
         EventDispatcher.getInstance().registerGeckoThreadListener((GeckoEventListener) this,
             "Prompt:ShowTop");
 
         mScreenshotObserver.stop();
+
+        mAddToHomeScreenPromotion.pause();
     }
 
     @Override
     public void onStart() {
         super.onStart();
 
         // Queue this work so that the first launch of the activity doesn't
         // trigger profile init too early.
@@ -3715,18 +3723,22 @@ public class BrowserApp extends GeckoApp
         if (isViewMultipleAction) {
             List<String> urls = intent.getStringArrayListExtra("urls");
             if (urls != null) {
                 openUrls(urls);
             }
 
             // Launched from a "content notification"
             if (intent.hasExtra(CheckForUpdatesAction.EXTRA_CONTENT_NOTIFICATION)) {
+                Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this));
+
                 Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.NOTIFICATION, "content_update");
                 Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.INTENT, "content_update");
+
+                Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this));
             }
         }
 
         if (!mInitialized || !Intent.ACTION_MAIN.equals(action)) {
             return;
         }
 
         // Check to see how many times the app has been launched.
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoApp.java
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko;
 
 import org.mozilla.gecko.AppConstants.Versions;
 import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.URLMetadataTable;
+import org.mozilla.gecko.db.UrlAnnotations;
 import org.mozilla.gecko.favicons.Favicons;
 import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
 import org.mozilla.gecko.gfx.BitmapUtils;
 import org.mozilla.gecko.gfx.FullScreenState;
 import org.mozilla.gecko.gfx.Layer;
 import org.mozilla.gecko.gfx.LayerView;
 import org.mozilla.gecko.gfx.PluginLayer;
 import org.mozilla.gecko.health.HealthRecorder;
@@ -1802,48 +1803,23 @@ public abstract class GeckoApp
 
     @Override
     public String getDefaultUAString() {
         return HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET :
                                           AppConstants.USER_AGENT_FENNEC_MOBILE;
     }
 
     @Override
-    public void createShortcut(final String title, final String URI) {
-        ThreadUtils.assertOnBackgroundThread();
-        final BrowserDB db = GeckoProfile.get(getApplicationContext()).getDB();
-
-        final ContentResolver cr = getContext().getContentResolver();
-        final Map<String, Map<String, Object>> metadata = db.getURLMetadata().getForURLs(cr,
-                Collections.singletonList(URI),
-                Collections.singletonList(URLMetadataTable.TOUCH_ICON_COLUMN)
-        );
-
-        final Map<String, Object> row = metadata.get(URI);
-
-        String touchIconURL = null;
-
-        if (row != null) {
-            touchIconURL = (String) row.get(URLMetadataTable.TOUCH_ICON_COLUMN);
-        }
-
-        OnFaviconLoadedListener listener = new OnFaviconLoadedListener() {
+    public void createShortcut(final String title, final String url) {
+        Favicons.getPreferredIconForHomeScreenShortcut(this, url, new OnFaviconLoadedListener() {
             @Override
             public void onFaviconLoaded(String url, String faviconURL, Bitmap favicon) {
                 doCreateShortcut(title, url, favicon);
             }
-        };
-
-        // Retrieve the icon while bypassing the cache. Homescreen icon creation is a one-off event, hence it isn't
-        // useful to cache these icons. (Android takes care of storing homescreen icons after a shortcut
-        // has been created.)
-        // The cache is also (currently) limited to 32dp, hence we explicitly need to avoid accessing those icons.
-        // If touchIconURL is null, then Favicons falls back to finding the best possible favicon for
-        // the site URI, hence we can use this call even when there is no touchIcon defined.
-        Favicons.getPreferredSizeFaviconForPage(getApplicationContext(), URI, touchIconURL, listener);
+        });
     }
 
     private void doCreateShortcut(final String aTitle, final String aURI, final Bitmap aIcon) {
         // The intent to be launched by the shortcut.
         Intent shortcutIntent = new Intent();
         shortcutIntent.setAction(GeckoApp.ACTION_HOMESCREEN_SHORTCUT);
         shortcutIntent.setData(Uri.parse(aURI));
         shortcutIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME,
@@ -1859,16 +1835,20 @@ public abstract class GeckoApp
             intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, aURI);
         }
 
         // Do not allow duplicate items.
         intent.putExtra("duplicate", false);
 
         intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
         getApplicationContext().sendBroadcast(intent);
+
+        // Remember interaction
+        final UrlAnnotations urlAnnotations = GeckoProfile.get(getApplicationContext()).getDB().getUrlAnnotations();
+        urlAnnotations.insertHomeScreenShortcut(getContentResolver(), aURI, true);
     }
 
     private void processAlertCallback(SafeIntent intent) {
         String alertName = "";
         String alertCookie = "";
         Uri data = intent.getData();
         if (data != null) {
             alertName = data.getQueryParameter("name");
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
@@ -544,17 +544,26 @@ public class BrowserContract {
             FEED_SUBSCRIPTION("feed_subscription"),
 
             /**
              * Indicates that this URL (if stored as a bookmark) should be opened into reader view.
              *
              * Key:   reader_view
              * Value: String "true" to indicate that we would like to open into reader view.
              */
-            READER_VIEW("reader_view");
+            READER_VIEW("reader_view"),
+
+            /**
+             * Indicator that the user interacted with the URL in regards to home screen shortcuts.
+             *
+             * Key:   home_screen_shortcut
+             * Value: True: User created an home screen shortcut for this URL
+             *        False: User declined to create a shortcut for this URL
+             */
+            HOME_SCREEN_SHORTCUT("home_screen_shortcut");
 
             private final String dbValue;
 
             Key(final String dbValue) { this.dbValue = dbValue; }
             public String getDbValue() { return dbValue; }
         }
 
         public enum SyncStatus {
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
@@ -88,16 +88,18 @@ public interface BrowserDB {
      */
     public abstract Cursor getAllVisitedHistory(ContentResolver cr);
 
     /**
      * Can return <code>null</code>.
      */
     public abstract Cursor getRecentHistory(ContentResolver cr, int limit);
 
+    public abstract Cursor getHistoryForURL(ContentResolver cr, String uri);
+
     public abstract Cursor getRecentHistoryBetweenTime(ContentResolver cr, int historyLimit, long start, long end);
 
     public abstract long getPrePathLastVisitedTimeMilliseconds(ContentResolver cr, String prePath);
 
     public abstract void expireHistory(ContentResolver cr, ExpirePriority priority);
 
     public abstract void removeHistoryEntry(ContentResolver cr, String url);
 
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
@@ -707,16 +707,28 @@ public class LocalBrowserDB implements B
                         Combined.TITLE,
                         Combined.DATE_LAST_VISITED,
                         Combined.VISITS },
                 History.DATE_LAST_VISITED + " >= " + start + " AND " + History.DATE_LAST_VISITED + " < " + end,
                 null,
                 History.DATE_LAST_VISITED + " DESC");
     }
 
+    public Cursor getHistoryForURL(ContentResolver cr, String uri) {
+        return cr.query(mHistoryUriWithProfile,
+                new String[] {
+                    History.VISITS,
+                    History.DATE_LAST_VISITED
+                },
+                History.URL + "= ?",
+                new String[] { uri },
+                History.DATE_LAST_VISITED + " DESC"
+        );
+    }
+
     @Override
     public long getPrePathLastVisitedTimeMilliseconds(ContentResolver cr, String prePath) {
         if (prePath == null) {
             return 0;
         }
         // If we don't end with a trailing slash, then both https://foo.com and https://foo.company.biz will match.
         if (!prePath.endsWith("/")) {
             prePath = prePath + "/";
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalUrlAnnotations.java
@@ -40,16 +40,28 @@ public class LocalUrlAnnotations impleme
     /**
      * Insert mapping from website URL to URL of the feed.
      */
     @Override
     public void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl) {
         insertAnnotation(cr, originUrl, Key.FEED, feedUrl);
     }
 
+    @Override
+    public boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url) {
+        return hasResultsForSelection(cr,
+                BrowserContract.UrlAnnotations.URL + " = ?",
+                new String[]{url});
+    }
+
+    @Override
+    public void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut) {
+        insertAnnotation(cr, url, Key.HOME_SCREEN_SHORTCUT, String.valueOf(hasCreatedShortCut));
+    }
+
     /**
      * Returns true if there's a mapping from the given website URL to a feed URL. False otherwise.
      */
     @Override
     public boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl) {
         return hasResultsForSelection(cr,
                 BrowserContract.UrlAnnotations.URL + " = ? AND " + BrowserContract.UrlAnnotations.KEY + " = ?",
                 new String[]{websiteUrl, Key.FEED.getDbValue()});
--- a/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/StubBrowserDB.java
@@ -130,16 +130,22 @@ class StubUrlAnnotations implements UrlA
     @Override
     public void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl) {}
 
     @Override
     public void insertReaderViewUrl(ContentResolver cr, String pageURL) {}
 
     @Override
     public void deleteReaderViewUrl(ContentResolver cr, String pageURL) {}
+
+    @Override
+    public boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url) { return false; }
+
+    @Override
+    public void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut) {}
 }
 
 /*
  * This base implementation just stubs all methods. For the
  * real implementations, see LocalBrowserDB.java.
  */
 public class StubBrowserDB implements BrowserDB {
     private final StubSearches searches = new StubSearches();
@@ -204,16 +210,21 @@ public class StubBrowserDB implements Br
         return null;
     }
 
     public Cursor getRecentHistory(ContentResolver cr, int limit) {
         return null;
     }
 
     @Override
+    public Cursor getHistoryForURL(ContentResolver cr, String uri) {
+        return null;
+    }
+
+    @Override
     public Cursor getRecentHistoryBetweenTime(ContentResolver cr, int limit, long time, long end) {
         return null;
     }
 
     @Override
     public long getPrePathLastVisitedTimeMilliseconds(ContentResolver cr, String prePath) { return 0; }
 
     public void expireHistory(ContentResolver cr, BrowserContract.ExpirePriority priority) {
--- a/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/UrlAnnotations.java
@@ -23,9 +23,27 @@ public interface UrlAnnotations {
     void updateFeedSubscription(ContentResolver cr, FeedSubscription subscription);
     boolean hasFeedSubscription(ContentResolver cr, String feedUrl);
     void insertFeedSubscription(ContentResolver cr, FeedSubscription subscription);
     boolean hasFeedUrlForWebsite(ContentResolver cr, String websiteUrl);
     void insertFeedUrl(ContentResolver cr, String originUrl, String feedUrl);
 
     void insertReaderViewUrl(ContentResolver cr, String pageURL);
     void deleteReaderViewUrl(ContentResolver cr, String pageURL);
+
+    /**
+     * Did the user ever interact with this URL in regards to home screen shortcuts?
+     *
+     * @return true if the user has created a home screen shortcut or declined to create one in the
+     *         past. This method will still return true if the shortcut has been removed from the
+     *         home screen by the user.
+     */
+    boolean hasAcceptedOrDeclinedHomeScreenShortcut(ContentResolver cr, String url);
+
+    /**
+     * Insert an indication that the user has interacted with this URL in regards to home screen
+     * shortcuts.
+     *
+     * @param hasCreatedShortCut True if a home screen shortcut has been created for this URL. False
+     *                           if the user has actively declined to create a shortcut for this URL.
+     */
+    void insertHomeScreenShortcut(ContentResolver cr, String url, boolean hasCreatedShortCut);
 }
--- a/mobile/android/base/java/org/mozilla/gecko/favicons/Favicons.java
+++ b/mobile/android/base/java/org/mozilla/gecko/favicons/Favicons.java
@@ -3,20 +3,22 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.gecko.favicons;
 
 import android.graphics.drawable.Drawable;
 import org.mozilla.gecko.AboutPages;
 import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.URLMetadataTable;
 import org.mozilla.gecko.favicons.cache.FaviconCache;
 import org.mozilla.gecko.util.GeckoJarReader;
 import org.mozilla.gecko.util.NonEvictingLruCache;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.res.Resources;
@@ -28,16 +30,17 @@ import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 public class Favicons {
     private static final String LOGTAG = "GeckoFavicons";
 
     // A magic URL representing the app's own favicon, used for about: pages.
@@ -591,9 +594,46 @@ public class Favicons {
      *
      * @param url page URL to get a large favicon image for.
      * @param onFaviconLoadedListener listener to call back with the result.
      */
     public static void getPreferredSizeFaviconForPage(Context context, String url, String iconURL, OnFaviconLoadedListener onFaviconLoadedListener) {
         int preferredSize = GeckoAppShell.getPreferredIconSize();
         loadUncachedFavicon(context, url, iconURL, LoadFaviconTask.FLAG_BYPASS_CACHE_WHEN_DOWNLOADING_ICONS, preferredSize, onFaviconLoadedListener);
     }
+
+    /**
+     * Load the icon that is the most suitable for using as a home screen shortcut.
+     *
+     * This method will try to load a 'touch icon' first. If not available it will fallback to use
+     * the best available favicon.
+     *
+     * This implementation sidesteps the cache and will load the icon from the database or the
+     * internet. See getPreferredSizeFaviconForPage().
+     */
+    public static void getPreferredIconForHomeScreenShortcut(Context context, String url, OnFaviconLoadedListener onFaviconLoadedListener) {
+        ThreadUtils.assertOnBackgroundThread();
+
+        final BrowserDB db = GeckoProfile.get(context).getDB();
+
+        final ContentResolver cr = context.getContentResolver();
+        final Map<String, Map<String, Object>> metadata = db.getURLMetadata().getForURLs(cr,
+                Collections.singletonList(url),
+                Collections.singletonList(URLMetadataTable.TOUCH_ICON_COLUMN)
+        );
+
+        final Map<String, Object> row = metadata.get(url);
+
+        String touchIconURL = null;
+
+        if (row != null) {
+            touchIconURL = (String) row.get(URLMetadataTable.TOUCH_ICON_COLUMN);
+        }
+
+        // Retrieve the icon while bypassing the cache. Homescreen icon creation is a one-off event, hence it isn't
+        // useful to cache these icons. (Android takes care of storing homescreen icons after a shortcut
+        // has been created.)
+        // The cache is also (currently) limited to 32dp, hence we explicitly need to avoid accessing those icons.
+        // If touchIconURL is null, then Favicons falls back to finding the best possible favicon for
+        // the site URI, hence we can use this call even when there is no touchIcon defined.
+        getPreferredSizeFaviconForPage(context, url, touchIconURL, onFaviconLoadedListener);
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/FeedService.java
@@ -143,12 +143,26 @@ public class FeedService extends IntentS
     }
 
     public static boolean isInExperiment(Context context) {
         return SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_12HRS) ||
                SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_5PM) ||
                SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_8AM);
     }
 
+    public static String getEnabledExperiment(Context context) {
+        String experiment = null;
+
+        if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_12HRS)) {
+            experiment = Experiments.CONTENT_NOTIFICATIONS_12HRS;
+        } else if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_8AM)) {
+            experiment = Experiments.CONTENT_NOTIFICATIONS_8AM;
+        } else if (SwitchBoard.isInExperiment(context, Experiments.CONTENT_NOTIFICATIONS_5PM)) {
+            experiment = Experiments.CONTENT_NOTIFICATIONS_5PM;
+        }
+
+        return experiment;
+    }
+
     private boolean isPreferenceEnabled() {
         return GeckoSharedPrefs.forApp(this).getBoolean(GeckoPreferences.PREFS_NOTIFICATIONS_CONTENT, true);
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckForUpdatesAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/CheckForUpdatesAction.java
@@ -23,16 +23,17 @@ import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.BrowserApp;
 import org.mozilla.gecko.GeckoApp;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.UrlAnnotations;
 import org.mozilla.gecko.feeds.FeedFetcher;
+import org.mozilla.gecko.feeds.FeedService;
 import org.mozilla.gecko.feeds.parser.Feed;
 import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.util.StringUtils;
 
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -114,17 +115,19 @@ public class CheckForUpdatesAction exten
         }
 
         if (feedCount == 1) {
             showNotificationForSingleUpdate(updatedFeeds.get(0));
         } else {
             showNotificationForMultipleUpdates(updatedFeeds);
         }
 
+        Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
         Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.NOTIFICATION, "content_update");
+        Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
     }
 
     private void showNotificationForSingleUpdate(Feed feed) {
         final String date = DateFormat.getMediumDateFormat(context).format(new Date(feed.getLastItem().getTimestamp()));
 
         NotificationCompat.BigTextStyle style = new NotificationCompat.BigTextStyle()
                 .bigText(feed.getLastItem().getTitle())
                 .setBigContentTitle(feed.getTitle())
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollSubscriptionsAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/EnrollSubscriptionsAction.java
@@ -13,28 +13,30 @@ import android.text.TextUtils;
 
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.UrlAnnotations;
 import org.mozilla.gecko.feeds.FeedService;
 import org.mozilla.gecko.feeds.knownsites.KnownSiteBlogger;
 import org.mozilla.gecko.feeds.knownsites.KnownSite;
 import org.mozilla.gecko.feeds.knownsites.KnownSiteMedium;
+import org.mozilla.gecko.feeds.knownsites.KnownSiteTumblr;
 import org.mozilla.gecko.feeds.knownsites.KnownSiteWordpress;
 
 /**
  * EnrollSubscriptionsAction: Search for bookmarks of known sites we can subscribe to.
  */
 public class EnrollSubscriptionsAction extends FeedAction {
     private static final String LOGTAG = "FeedEnrollAction";
 
     private static final KnownSite[] knownSites = {
         new KnownSiteMedium(),
         new KnownSiteBlogger(),
         new KnownSiteWordpress(),
+        new KnownSiteTumblr(),
     };
 
     private Context context;
 
     public EnrollSubscriptionsAction(Context context) {
         this.context = context;
     }
 
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeToFeedAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/SubscribeToFeedAction.java
@@ -9,16 +9,17 @@ import android.content.Context;
 import android.content.Intent;
 import android.os.Bundle;
 
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.UrlAnnotations;
 import org.mozilla.gecko.feeds.FeedFetcher;
+import org.mozilla.gecko.feeds.FeedService;
 import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
 
 /**
  * SubscribeToFeedAction: Try to fetch a feed and create a subscription if successful.
  */
 public class SubscribeToFeedAction extends FeedAction {
     private static final String LOGTAG = "FeedSubscribeAction";
 
@@ -66,11 +67,13 @@ public class SubscribeToFeedAction exten
 
         log("Subscribing to feed: " + response.feed.getTitle());
         log("          Last item: " + response.feed.getLastItem().getTitle());
 
         final FeedSubscription subscription = FeedSubscription.create(feedUrl, response);
 
         urlAnnotations.insertFeedSubscription(context.getContentResolver(), subscription);
 
+        Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
         Telemetry.sendUIEvent(TelemetryContract.Event.SAVE, TelemetryContract.Method.SERVICE, "content_update");
+        Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
     }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawSubscriptionsAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/action/WithdrawSubscriptionsAction.java
@@ -11,16 +11,17 @@ import android.content.Intent;
 import android.database.Cursor;
 
 import org.json.JSONException;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.UrlAnnotations;
+import org.mozilla.gecko.feeds.FeedService;
 import org.mozilla.gecko.feeds.subscriptions.FeedSubscription;
 
 /**
  * WithdrawSubscriptionsAction: Look for feeds to unsubscribe from.
  */
 public class WithdrawSubscriptionsAction extends FeedAction {
     private static final String LOGTAG = "FeedWithdrawAction";
 
@@ -79,17 +80,19 @@ public class WithdrawSubscriptionsAction
             while (cursor.moveToNext()) {
                 final FeedSubscription subscription = FeedSubscription.fromCursor(cursor);
 
                 if (!urlAnnotations.hasWebsiteForFeedUrl(resolver, subscription.getFeedUrl())) {
                     log("Removing subscription for feed: " + subscription.getFeedUrl());
 
                     urlAnnotations.deleteFeedSubscription(resolver, subscription);
 
+                    Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
                     Telemetry.sendUIEvent(TelemetryContract.Event.UNSAVE, TelemetryContract.Method.SERVICE, "content_update");
+                    Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(context));
                 }
             }
         } catch (JSONException e) {
             log("Could not deserialize subscription", e);
         } finally {
             cursor.close();
         }
     }
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/feeds/knownsites/KnownSiteTumblr.java
@@ -0,0 +1,33 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.feeds.knownsites;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Tumblr.com
+ */
+public class KnownSiteTumblr implements KnownSite {
+    @Override
+    public String getURLSearchString() {
+        return ".tumblr.com";
+    }
+
+    @Override
+    public String getFeedFromURL(String url) {
+        final Pattern pattern = Pattern.compile("https?://(.*?).tumblr.com(/.*)?");
+        final Matcher matcher = pattern.matcher(url);
+        if (matcher.matches()) {
+            final String username = matcher.group(1);
+            if (username.equals("www")) {
+                return null;
+            }
+            return "http://" + username + ".tumblr.com/rss";
+        }
+        return null;
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
+++ b/mobile/android/base/java/org/mozilla/gecko/preferences/GeckoPreferences.java
@@ -382,17 +382,19 @@ OnSharedPreferenceChangeListener
         if (intentExtras != null && intentExtras.containsKey(DataReportingNotification.ALERT_NAME_DATAREPORTING_NOTIFICATION)) {
             Telemetry.sendUIEvent(TelemetryContract.Event.LAUNCH, Method.NOTIFICATION, "settings-data-choices");
             NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
             notificationManager.cancel(DataReportingNotification.ALERT_NAME_DATAREPORTING_NOTIFICATION.hashCode());
         }
 
         // Launched from "Notifications settings" action button in a notification.
         if (intentExtras != null && intentExtras.containsKey(CheckForUpdatesAction.EXTRA_CONTENT_NOTIFICATION)) {
+            Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this));
             Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, Method.BUTTON, "notification-settings");
+            Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, FeedService.getEnabledExperiment(this));
         }
     }
 
     /**
      * Initializes the action bar configuration in code.
      *
      * Declaring these attributes in XML does not work on some devices for an unknown reason
      * (e.g. the back button stops working or the logo disappears; see bug 1152314) so we
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/AddToHomeScreenPromotion.java
@@ -0,0 +1,208 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.promotion;
+
+import android.app.Activity;
+import android.database.Cursor;
+import android.support.annotation.WorkerThread;
+import android.util.Log;
+
+import com.keepsafe.switchboard.SwitchBoard;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.mozilla.gecko.AboutPages;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.Tab;
+import org.mozilla.gecko.Tabs;
+import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.db.BrowserDB;
+import org.mozilla.gecko.db.UrlAnnotations;
+import org.mozilla.gecko.util.Experiments;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import ch.boye.httpclientandroidlib.util.TextUtils;
+
+/**
+ * Promote "Add to home screen" if user visits website often.
+ */
+public class AddToHomeScreenPromotion implements Tabs.OnTabsChangedListener {
+    private static class URLHistory {
+        public final long visits;
+        public final long lastVisit;
+
+        private URLHistory(long visits, long lastVisit) {
+            this.visits = visits;
+            this.lastVisit = lastVisit;
+        }
+    }
+
+    private static final String LOGTAG = "GeckoPromoteShortcut";
+
+    private static final String EXPERIMENT_MINIMUM_TOTAL_VISITS = "minimumTotalVisits";
+    private static final String EXPERIMENT_LAST_VISIT_MINIMUM_AGE = "lastVisitMinimumAgeMs";
+    private static final String EXPERIMENT_LAST_VISIT_MAXIMUM_AGE = "lastVisitMaximumAgeMs";
+
+    private Activity activity;
+    private boolean isEnabled;
+    private int minimumVisits;
+    private int lastVisitMinimumAgeMs;
+    private int lastVisitMaximumAgeMs;
+
+    public AddToHomeScreenPromotion(Activity activity) {
+        this.activity = activity;
+
+        initializeExperiment();
+    }
+
+    public void resume() {
+        Tabs.registerOnTabsChangedListener(this);
+    }
+
+    public void pause() {
+        Tabs.unregisterOnTabsChangedListener(this);
+    }
+
+    private void initializeExperiment() {
+        if (!SwitchBoard.isInExperiment(activity, Experiments.PROMOTE_ADD_TO_HOMESCREEN)) {
+            Log.v(LOGTAG, "Experiment not enabled");
+            // Experiment is not enabled. No need to try to read values.
+            return;
+        }
+
+        JSONObject values = SwitchBoard.getExperimentValuesFromJson(activity, Experiments.PROMOTE_ADD_TO_HOMESCREEN);
+        if (values == null) {
+            // We didn't get any values for this experiment. Let's disable it instead of picking default
+            // values that might be bad.
+            return;
+        }
+
+        try {
+            initializeWithValues(
+                    values.getInt(EXPERIMENT_MINIMUM_TOTAL_VISITS),
+                    values.getInt(EXPERIMENT_LAST_VISIT_MINIMUM_AGE),
+                    values.getInt(EXPERIMENT_LAST_VISIT_MAXIMUM_AGE));
+        } catch (JSONException e) {
+            Log.w(LOGTAG, "Could not read experiment values", e);
+        }
+    }
+
+    private void initializeWithValues(int minimumVisits, int lastVisitMinimumAgeMs, int lastVisitMaximumAgeMs) {
+        this.isEnabled = true;
+
+        this.minimumVisits = minimumVisits;
+        this.lastVisitMinimumAgeMs = lastVisitMinimumAgeMs;
+        this.lastVisitMaximumAgeMs = lastVisitMaximumAgeMs;
+    }
+
+    @Override
+    public void onTabChanged(final Tab tab, Tabs.TabEvents msg, Object data) {
+        if (tab == null) {
+            return;
+        }
+
+        if (!Tabs.getInstance().isSelectedTab(tab)) {
+            // We only ever want to show this promotion for the current tab.
+            return;
+        }
+
+        if (Tabs.TabEvents.LOADED == msg) {
+            ThreadUtils.postToBackgroundThread(new Runnable() {
+                @Override
+                public void run() {
+                    maybeShowPromotionForUrl(tab.getURL(), tab.getTitle());
+                }
+            });
+        }
+    }
+
+    private void maybeShowPromotionForUrl(String url, String title) {
+        if (!isEnabled) {
+            return;
+        }
+
+        if (!shouldShowPromotion(url, title)) {
+            return;
+        }
+
+        HomeScreenPrompt.show(activity, url, title);
+    }
+
+    private boolean shouldShowPromotion(String url, String title) {
+        if (TextUtils.isEmpty(url) || TextUtils.isEmpty(title)) {
+            // We require an URL and a title for the shortcut.
+            return false;
+        }
+
+        if (AboutPages.isAboutPage(url)) {
+            // No promotion for our internal sites.
+            return false;
+        }
+
+        if (!url.startsWith("https://")) {
+            // Only promote websites that are served over HTTPS.
+            return false;
+        }
+
+        URLHistory history = getHistoryForURL(url);
+        if (history == null) {
+            // There's no history for this URL yet or we can't read it right now. Just ignore.
+            return false;
+        }
+
+        if (history.visits < minimumVisits) {
+            // This URL has not been visited often enough.
+            return false;
+        }
+
+        if (history.lastVisit > System.currentTimeMillis() - lastVisitMinimumAgeMs) {
+            // The last visit is too new. Do not show promotion. This is mostly to avoid that the
+            // promotion shows up for a quick refreshs and in the worst case the last visit could
+            // be the current visit (race).
+            return false;
+        }
+
+        if (history.lastVisit < System.currentTimeMillis() - lastVisitMaximumAgeMs) {
+            // The last visit is to old. Do not show promotion.
+            return false;
+        }
+
+        if (hasAcceptedOrDeclinedHomeScreenShortcut(url)) {
+            // The user has already created a shortcut in the past or actively declined to create one.
+            // Let's not ask again for this url - We do not want to be annoying.
+            return false;
+        }
+
+        return true;
+    }
+
+    protected boolean hasAcceptedOrDeclinedHomeScreenShortcut(String url) {
+        final UrlAnnotations urlAnnotations = GeckoProfile.get(activity).getDB().getUrlAnnotations();
+        return urlAnnotations.hasAcceptedOrDeclinedHomeScreenShortcut(activity.getContentResolver(), url);
+    }
+
+    protected URLHistory getHistoryForURL(String url) {
+        final GeckoProfile profile = GeckoProfile.get(activity);
+        final BrowserDB browserDB = profile.getDB();
+
+        Cursor cursor = null;
+        try {
+            cursor = browserDB.getHistoryForURL(activity.getContentResolver(), url);
+
+            if (cursor.moveToFirst()) {
+                return new URLHistory(
+                    cursor.getInt(cursor.getColumnIndex(BrowserContract.History.VISITS)),
+                    cursor.getLong(cursor.getColumnIndex(BrowserContract.History.DATE_LAST_VISITED)));
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+
+        return null;
+    }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/promotion/HomeScreenPrompt.java
@@ -0,0 +1,247 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.promotion;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.Locales;
+import org.mozilla.gecko.R;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.db.UrlAnnotations;
+import org.mozilla.gecko.favicons.Favicons;
+import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
+import org.mozilla.gecko.util.Experiments;
+import org.mozilla.gecko.util.ThreadUtils;
+
+/**
+ * Prompt to promote adding the current website to the home screen.
+ */
+public class HomeScreenPrompt extends Locales.LocaleAwareActivity implements OnFaviconLoadedListener {
+    private static final String EXTRA_TITLE = "title";
+    private static final String EXTRA_URL = "url";
+
+    private static final String TELEMETRY_EXTRA = "home_screen_promotion";
+
+    private View containerView;
+    private ImageView iconView;
+    private String title;
+    private String url;
+    private boolean isAnimating;
+    private boolean hasAccepted;
+
+    public static void show(Context context, String url, String title) {
+        Intent intent = new Intent(context, HomeScreenPrompt.class);
+        intent.putExtra(EXTRA_TITLE, title);
+        intent.putExtra(EXTRA_URL, url);
+        context.startActivity(intent);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        fetchDataFromIntent();
+        setupViews();
+        loadShortcutIcon();
+
+        slideIn();
+
+        Telemetry.startUISession(TelemetryContract.Session.EXPERIMENT, Experiments.PROMOTE_ADD_TO_HOMESCREEN);
+
+        // Technically this isn't triggered by a "service". But it's also triggered by a background task and without
+        // actual user interaction.
+        Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.SERVICE, TELEMETRY_EXTRA);
+    }
+
+    @Override
+    protected void onDestroy() {
+        super.onDestroy();
+
+        Telemetry.stopUISession(TelemetryContract.Session.EXPERIMENT, Experiments.PROMOTE_ADD_TO_HOMESCREEN);
+    }
+
+    private void fetchDataFromIntent() {
+        final Bundle extras = getIntent().getExtras();
+
+        title = extras.getString(EXTRA_TITLE);
+        url = extras.getString(EXTRA_URL);
+    }
+
+    private void setupViews() {
+        setContentView(R.layout.homescreen_prompt);
+
+        ((TextView) findViewById(R.id.title)).setText(title);
+
+        Uri uri = Uri.parse(url);
+        ((TextView) findViewById(R.id.host)).setText(uri.getHost());
+
+        containerView = findViewById(R.id.container);
+        iconView = (ImageView) findViewById(R.id.icon);
+
+        findViewById(R.id.add).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                hasAccepted = true;
+
+                addToHomeScreen();
+            }
+        });
+
+        findViewById(R.id.close).setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                rememberRejection();
+                slideOut();
+
+                Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BUTTON, TELEMETRY_EXTRA);
+            }
+        });
+    }
+
+    private void addToHomeScreen() {
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                GeckoAppShell.createShortcut(title, url);
+
+                Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.BUTTON, TELEMETRY_EXTRA);
+
+                goToHomeScreen();
+            }
+        });
+    }
+
+    /**
+     * Finish this activity and launch the default home screen activity.
+     */
+    private void goToHomeScreen() {
+        Intent intent = new Intent(Intent.ACTION_MAIN);
+
+        intent.addCategory(Intent.CATEGORY_HOME);
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        startActivity(intent);
+
+        finish();
+    }
+
+    private void loadShortcutIcon() {
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                Favicons.getPreferredIconForHomeScreenShortcut(HomeScreenPrompt.this, url, HomeScreenPrompt.this);
+            }
+        });
+    }
+
+    private void slideIn() {
+        containerView.setTranslationY(500);
+        containerView.setAlpha(0);
+
+        final Animator translateAnimator = ObjectAnimator.ofFloat(containerView, "translationY", 0);
+        translateAnimator.setDuration(400);
+
+        final Animator alphaAnimator = ObjectAnimator.ofFloat(containerView, "alpha", 1);
+        alphaAnimator.setStartDelay(200);
+        alphaAnimator.setDuration(600);
+
+        final AnimatorSet set = new AnimatorSet();
+        set.playTogether(alphaAnimator, translateAnimator);
+        set.setStartDelay(400);
+
+        set.start();
+    }
+
+    /**
+     * Remember that the user rejected creating a home screen shortcut for this URL.
+     */
+    private void rememberRejection() {
+        if (hasAccepted) {
+            // User has already accepted to create a shortcut.
+            return;
+        }
+
+        final UrlAnnotations urlAnnotations = GeckoProfile.get(this).getDB().getUrlAnnotations();
+        urlAnnotations.insertHomeScreenShortcut(getContentResolver(), url, false);
+    }
+
+    private void slideOut() {
+        if (isAnimating) {
+            return;
+        }
+
+        isAnimating = true;
+
+        ObjectAnimator animator = ObjectAnimator.ofFloat(containerView, "translationY", containerView.getHeight());
+        animator.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                finish();
+            }
+
+        });
+        animator.start();
+    }
+
+    @Override
+    public void finish() {
+        super.finish();
+
+        // Don't perform an activity-dismiss animation.
+        overridePendingTransition(0, 0);
+    }
+
+    @Override
+    public void onBackPressed() {
+        rememberRejection();
+        slideOut();
+
+        Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BACK, TELEMETRY_EXTRA);
+    }
+
+    /**
+     * User clicked outside of the prompt.
+     */
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        rememberRejection();
+        slideOut();
+
+        // Not really an action triggered by the "back" button but with the same effect: Finishing this
+        // activity and going back to the previous one.
+        Telemetry.sendUIEvent(TelemetryContract.Event.CANCEL, TelemetryContract.Method.BACK, TELEMETRY_EXTRA);
+
+        return true;
+    }
+
+    @Override
+    public void onFaviconLoaded(String url, String faviconURL, final Bitmap favicon) {
+        if (favicon == null) {
+            return;
+        }
+
+        ThreadUtils.postToUiThread(new Runnable() {
+            @Override
+            public void run() {
+                iconView.setImageBitmap(favicon);
+            }
+        });
+    }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java
+++ b/mobile/android/base/java/org/mozilla/gecko/toolbar/BrowserToolbar.java
@@ -718,38 +718,42 @@ public abstract class BrowserToolbar ext
         setUrlEditLayoutVisibility(false, animator);
     }
 
     protected void setUrlEditLayoutVisibility(final boolean showEditLayout, PropertyAnimator animator) {
         if (showEditLayout) {
             urlEditLayout.prepareShowAnimation(animator);
         }
 
+        // If this view is GONE, we trigger a measure pass when setting the view to
+        // VISIBLE. Since this will occur during the toolbar open animation, it causes jank.
+        final int hiddenViewVisibility = View.INVISIBLE;
+
         if (animator == null) {
             final View viewToShow = (showEditLayout ? urlEditLayout : urlDisplayLayout);
             final View viewToHide = (showEditLayout ? urlDisplayLayout : urlEditLayout);
 
-            viewToHide.setVisibility(View.GONE);
+            viewToHide.setVisibility(hiddenViewVisibility);
             viewToShow.setVisibility(View.VISIBLE);
             return;
         }
 
         animator.addPropertyAnimationListener(new PropertyAnimationListener() {
             @Override
             public void onPropertyAnimationStart() {
                 if (!showEditLayout) {
-                    urlEditLayout.setVisibility(View.GONE);
+                    urlEditLayout.setVisibility(hiddenViewVisibility);
                     urlDisplayLayout.setVisibility(View.VISIBLE);
                 }
             }
 
             @Override
             public void onPropertyAnimationEnd() {
                 if (showEditLayout) {
-                    urlDisplayLayout.setVisibility(View.GONE);
+                    urlDisplayLayout.setVisibility(hiddenViewVisibility);
                     urlEditLayout.setVisibility(View.VISIBLE);
                 }
             }
         });
     }
 
     private void setUIMode(final UIMode uiMode) {
         this.uiMode = uiMode;
--- a/mobile/android/base/java/org/mozilla/gecko/util/Experiments.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/Experiments.java
@@ -40,16 +40,19 @@ public class Experiments {
     // on the client, they are not part of the server config.
     public static final String ONBOARDING2_A = "onboarding2-a"; // Control: Single (blue) welcome screen
     public static final String ONBOARDING2_B = "onboarding2-b"; // 4 static Feature slides
     public static final String ONBOARDING2_C = "onboarding2-c"; // 4 static + 1 clickable (Data saving) Feature slides
 
     // Synchronizing the catalog of downloadable content from Kinto
     public static final String DOWNLOAD_CONTENT_CATALOG_SYNC = "download-content-catalog-sync";
 
+    // Promotion for "Add to homescreen"
+    public static final String PROMOTE_ADD_TO_HOMESCREEN = "promote-add-to-homescreen";
+
     public static final String PREF_ONBOARDING_VERSION = "onboarding_version";
 
     private static volatile Boolean disabled = null;
 
     /**
      * Determines whether Switchboard is disabled by the MOZ_DISABLE_SWITCHBOARD
      * environment variable. We need to read this value from the intent string
      * extra because environment variables from our test harness aren't set
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -767,8 +767,10 @@ just addresses the organization to follo
 
 <!ENTITY eol_notification_title2 "&brandShortName; will no longer update">
 <!ENTITY eol_notification_summary "Tap to learn more">
 
 <!-- LOCALIZATION NOTE (whatsnew_notification_title, whatsnew_notification_summary): These strings
      are used for a system notification that's shown to users after the app updates. -->
 <!ENTITY whatsnew_notification_title "&brandShortName; is up to date">
 <!ENTITY whatsnew_notification_summary "Find out what\'s new in this version">
+
+<!ENTITY promotion_add_to_homescreen "Add to home screen">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -281,16 +281,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'feeds/action/SubscribeToFeedAction.java',
     'feeds/action/WithdrawSubscriptionsAction.java',
     'feeds/FeedAlarmReceiver.java',
     'feeds/FeedFetcher.java',
     'feeds/FeedService.java',
     'feeds/knownsites/KnownSite.java',
     'feeds/knownsites/KnownSiteBlogger.java',
     'feeds/knownsites/KnownSiteMedium.java',
+    'feeds/knownsites/KnownSiteTumblr.java',
     'feeds/knownsites/KnownSiteWordpress.java',
     'feeds/parser/Feed.java',
     'feeds/parser/Item.java',
     'feeds/parser/SimpleFeedParser.java',
     'feeds/subscriptions/FeedSubscription.java',
     'FilePicker.java',
     'FilePickerResultHandler.java',
     'FindInPageBar.java',
@@ -504,16 +505,18 @@ gbjar.sources += ['java/org/mozilla/geck
     'preferences/PrivateDataPreference.java',
     'preferences/SearchEnginePreference.java',
     'preferences/SearchPreferenceCategory.java',
     'preferences/SetHomepagePreference.java',
     'preferences/SyncPreference.java',
     'PrefsHelper.java',
     'PrintHelper.java',
     'PrivateTab.java',
+    'promotion/AddToHomeScreenPromotion.java',
+    'promotion/HomeScreenPrompt.java',
     'prompts/ColorPickerInput.java',
     'prompts/IconGridInput.java',
     'prompts/IntentChooserPrompt.java',
     'prompts/IntentHandler.java',
     'prompts/Prompt.java',
     'prompts/PromptInput.java',
     'prompts/PromptListAdapter.java',
     'prompts/PromptListItem.java',
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -598,9 +598,11 @@
   <string name="eol_notification_summary">&eol_notification_summary;</string>
   <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/honeycomb -->
   <string name="eol_notification_url">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/unsupported-version</string>
 
   <string name="whatsnew_notification_title">&whatsnew_notification_title;</string>
   <string name="whatsnew_notification_summary">&whatsnew_notification_summary;</string>
   <!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/new-android -->
   <string name="whatsnew_notification_url">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/new-android</string>
+
+  <string name="promotion_add_to_homescreen">&promotion_add_to_homescreen;</string>
 </resources>
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/res/layout/homescreen_prompt.xml
@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!-- 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/. -->
+
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:clipChildren="false"
+    android:clipToPadding="false">
+
+    <RelativeLayout
+        android:id="@+id/container"
+        android:layout_width="@dimen/overlay_prompt_container_width"
+        android:layout_height="wrap_content"
+        android:layout_gravity="bottom|center"
+        android:background="@android:color/white"
+        android:clickable="true"
+        android:orientation="vertical">
+
+        <ImageView
+            android:id="@+id/close"
+            android:layout_width="24dp"
+            android:layout_height="24dp"
+            android:layout_alignParentRight="true"
+            android:layout_marginLeft="10dp"
+            android:layout_marginRight="30dp"
+            android:layout_marginTop="30dp"
+            android:ellipsize="end"
+            android:maxLines="2"
+            android:padding="6dp"
+            android:src="@drawable/tab_close_active" />
+
+        <TextView
+            android:id="@+id/title"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="6dp"
+            android:layout_marginLeft="30dp"
+            android:layout_marginTop="30dp"
+            android:layout_toLeftOf="@id/close"
+            android:fontFamily="sans-serif-light"
+            android:textColor="@color/text_and_tabs_tray_grey"
+            android:textSize="20sp"
+            tools:text="The Pokedex" />
+
+        <TextView
+            android:id="@+id/host"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_below="@id/title"
+            android:layout_marginBottom="20dp"
+            android:layout_marginLeft="30dp"
+            android:layout_marginRight="30dp"
+            android:ellipsize="end"
+            android:maxLines="1"
+            android:textColor="@color/placeholder_grey"
+            android:textSize="16sp"
+            tools:text="pokedex.org" />
+
+        <ImageView
+            android:id="@+id/icon"
+            android:layout_width="50dp"
+            android:layout_height="50dp"
+            android:layout_below="@id/host"
+            android:layout_marginBottom="20dp"
+            android:layout_marginLeft="30dp"
+            android:src="@drawable/icon" />
+
+        <Button
+            android:id="@+id/add"
+            style="@style/Widget.BaseButton"
+            android:layout_width="wrap_content"
+            android:layout_height="50dp"
+            android:layout_alignParentRight="true"
+            android:layout_below="@id/host"
+            android:layout_marginBottom="20dp"
+            android:layout_marginLeft="20dp"
+            android:layout_marginRight="30dp"
+            android:background="@drawable/button_background_action_orange_round"
+            android:paddingLeft="16dp"
+            android:paddingRight="16dp"
+            android:text="@string/promotion_add_to_homescreen"
+            android:textColor="@android:color/white"
+            android:textSize="16sp" />
+
+    </RelativeLayout>
+</merge>
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/feeds/knownsites/TestKnownSiteTumblr.java
@@ -0,0 +1,62 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.feeds.knownsites;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.helpers.AssertUtil;
+
+@RunWith(TestRunner.class)
+public class TestKnownSiteTumblr {
+    /**
+     * Test that the search string is a substring of some known URLs.
+     */
+    @Test
+    public void testURLSearchString() {
+        final KnownSite tumblr = new KnownSiteTumblr();
+        final String searchString = tumblr.getURLSearchString();
+
+        AssertUtil.assertContains(
+                "http://contentnotifications.tumblr.com/",
+                searchString);
+
+        AssertUtil.assertContains(
+                "https://contentnotifications.tumblr.com",
+                searchString);
+
+        AssertUtil.assertContains(
+                "http://contentnotifications.tumblr.com/post/142684202402/content-notification-firefox-for-android-480",
+                searchString);
+
+        AssertUtil.assertContainsNot(
+                "http://www.mozilla.org",
+                searchString);
+    }
+
+    /**
+     * Test that we get a feed URL for valid Medium URLs.
+     */
+    @Test
+    public void testGettingFeedFromURL() {
+        final KnownSite tumblr = new KnownSiteTumblr();
+
+        Assert.assertEquals(
+                "http://contentnotifications.tumblr.com/rss",
+                tumblr.getFeedFromURL("http://contentnotifications.tumblr.com/")
+        );
+
+        Assert.assertEquals(
+                "http://staff.tumblr.com/rss",
+                tumblr.getFeedFromURL("https://staff.tumblr.com/post/141928246566/replies-are-back-and-the-sun-is-shining-on-the")
+        );
+
+        Assert.assertNull(tumblr.getFeedFromURL("https://www.tumblr.com"));
+
+        Assert.assertNull(tumblr.getFeedFromURL("http://www.mozilla.org"));
+    }
+}
--- a/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_webrequest.html
@@ -152,18 +152,20 @@ function backgroundScript() {
 
   let frameIDs = new Map();
 
   let recorded = {requested: [],
                   beforeSendHeaders: [],
                   beforeRedirect: [],
                   sendHeaders: [],
                   responseStarted: [],
+                  responseStarted2: [],
                   error: [],
-                  completed: []};
+                  completed: [],
+                 };
   let testHeaders = {
     request: {
       added: {
         "X-WebRequest-request": "text",
         "X-WebRequest-request-binary": "binary",
       },
       modified: {
         "User-Agent": "WebRequest",
@@ -412,16 +414,17 @@ function backgroundScript() {
   }
 
   browser.webRequest.onBeforeRequest.addListener(onBeforeRequest, {urls: ["<all_urls>"]}, ["blocking"]);
   browser.webRequest.onBeforeSendHeaders.addListener(onBeforeSendHeaders, {urls: ["<all_urls>"]}, ["blocking", "requestHeaders"]);
   browser.webRequest.onSendHeaders.addListener(onSendHeaders, {urls: ["<all_urls>"]}, ["requestHeaders"]);
   browser.webRequest.onBeforeRedirect.addListener(onBeforeRedirect, {urls: ["<all_urls>"]});
   browser.webRequest.onHeadersReceived.addListener(onHeadersReceived, {urls: ["<all_urls>"]}, ["blocking", "responseHeaders"]);
   browser.webRequest.onResponseStarted.addListener(checkIpAndRecord.bind(null, "responseStarted"), {urls: ["<all_urls>"]});
+  browser.webRequest.onResponseStarted.addListener(checkIpAndRecord.bind(null, "responseStarted2"), {urls: ["<all_urls>"]});
   browser.webRequest.onErrorOccurred.addListener(onErrorOccurred, {urls: ["<all_urls>"]});
   browser.webRequest.onCompleted.addListener(onCompleted, {urls: ["<all_urls>"]}, ["responseHeaders"]);
 
   function onTestMessage(msg) {
     if (msg == "skipCompleted") {
       checkCompleted = false;
       browser.test.sendMessage("ackSkipCompleted");
     } else {
@@ -494,17 +497,17 @@ function* test_once(skipCompleted) {
 
   compareLists(recorded.requested, expected_requested, "requested");
   compareLists(recorded.beforeSendHeaders, expected_beforeSendHeaders, "beforeSendHeaders");
   compareLists(recorded.sendHeaders, expected_sendHeaders, "sendHeaders");
   compareLists(recorded.beforeRedirect, expected_redirect, "beforeRedirect");
   compareLists(recorded.responseStarted, expected_response, "responseStarted");
   compareLists(recorded.error, expected_error, "error");
   compareLists(recorded.completed, expected_complete, "completed");
-
+  compareLists(recorded.responseStarted2, recorded.responseStarted, "multiple non-blocking listeners");
   yield extension.unload();
   info("webrequest extension unloaded");
 }
 
 // Run the test twice to make sure it works with caching.
 add_task(function* () { yield test_once(false); });
 add_task(function* () { yield test_once(true); });
 </script>
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -1,10 +1,10 @@
 [DEFAULT]
 head = head.js
 tail =
 firefox-appdir = browser
-skip-if = toolkit == 'android' || toolkit == 'gonk'
+skip-if = toolkit == 'gonk'
 
 [test_locale_data.js]
 [test_locale_converter.js]
 [test_ext_schemas.js]
 [test_getAPILevelForWindow.js]
\ No newline at end of file
--- a/toolkit/components/telemetry/docs/core-ping.rst
+++ b/toolkit/components/telemetry/docs/core-ping.rst
@@ -73,16 +73,19 @@ engine from the search plugins (in order
 
 * In the distribution
 * From the localized plugins shipped with the browser
 * The third-party plugins that are installed in the profile directory
 
 If the plugins fail to create a search engine instance, this field is also
 ``null``.
 
+This field can also be ``null`` when a custom search engine is set as the
+default.
+
 profileDate
 ~~~~~~~~~~~
 On Android, this value is created at profile creation time and retrieved or,
 for legacy profiles, taken from the package install time (note: this is not the
 same exact metric as profile creation time but we compromised in favor of ease
 of implementation).
 
 Additionally on Android, this field may be ``null`` in the unlikely event that
--- a/toolkit/content/autocomplete.css
+++ b/toolkit/content/autocomplete.css
@@ -25,11 +25,12 @@ richlistitem {
   text-overflow: ellipsis;
   white-space: nowrap;
 }
 
 .ac-tags[empty] {
   display: none;
 }
 
-.ac-action[actiontype=searchengine]:not([selected]) {
+.ac-action[actiontype=searchengine]:not([selected]),
+.ac-separator[actiontype=searchengine]:not([selected]) {
   display: none;
 }
--- a/toolkit/content/tests/chrome/test_autocomplete_emphasis.xul
+++ b/toolkit/content/tests/chrome/test_autocomplete_emphasis.xul
@@ -134,33 +134,20 @@ function nextTest() {
   synthesizeKey("VK_DOWN", {});
 }
 
 function checkSearchCompleted() {
   let autocomplete = $("richautocomplete");
   let result = autocomplete.popup.richlistbox.firstChild;
 
   for (let attribute of [result._titleText, result._urlText]) {
-
-    let numChildren = currentTest.emphasis.length;
-    let childNodeStart = 0;
-    if (attribute == result._urlText) {
-      // For the URL description, the first child is the em dash separator that
-      // visually separates the the title string from the URL string.
-      numChildren++;
-      childNodeStart = 1;
-      let node = attribute.childNodes[0];
-      ok(node.classList.contains("ac-title-urlaction-separator"),
-         "First child of URL text should be separator");
-    }
-
-    is(attribute.childNodes.length, numChildren, "The element should have the expected number of children.");
-
+    is(attribute.childNodes.length, currentTest.emphasis.length,
+       "The element should have the expected number of children.");
     for (let i = 0; i < currentTest.emphasis.length; i++) {
-      let node = attribute.childNodes[childNodeStart + i];
+      let node = attribute.childNodes[i];
       // Emphasized parts strictly alternate.
       if ((i % 2 == 0) == currentTest.emphasizeFirst) {
         // Check that this part is correctly emphasized.
         is(node.nodeName, "span", ". That child should be a span node");
         ok(node.classList.contains("ac-emphasize-text"), ". That child should be emphasized");
         is(node.textContent, currentTest.emphasis[i], ". That emphasis should be as expected.");
       } else {
         // Check that this part is _not_ emphasized.
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1354,16 +1354,22 @@ extends="chrome://global/content/binding
                 align="center"
                 xbl:inherits="selected">
         <xul:description class="ac-text-overflow-container">
           <xul:description anonid="tags-text"
                            class="ac-tags-text"
                            xbl:inherits="selected"/>
         </xul:description>
       </xul:hbox>
+      <xul:hbox anonid="separator"
+                class="ac-separator"
+                align="center"
+                xbl:inherits="selected,actiontype">
+        <xul:description class="ac-separator-text">—</xul:description>
+      </xul:hbox>
       <xul:hbox class="ac-url"
                 align="center"
                 xbl:inherits="selected,actiontype">
         <xul:description class="ac-text-overflow-container">
           <xul:description anonid="url-text"
                            class="ac-url-text"
                            xbl:inherits="selected"/>
         </xul:description>
@@ -1392,16 +1398,19 @@ extends="chrome://global/content/binding
             this, "anonid", "title-text"
           );
           this._tags = document.getAnonymousElementByAttribute(
             this, "anonid", "tags"
           );
           this._tagsText = document.getAnonymousElementByAttribute(
             this, "anonid", "tags-text"
           );
+          this._separator = document.getAnonymousElementByAttribute(
+            this, "anonid", "separator"
+          );
           this._urlText = document.getAnonymousElementByAttribute(
             this, "anonid", "url-text"
           );
           this._actionText = document.getAnonymousElementByAttribute(
             this, "anonid", "action-text"
           );
           this._adjustAcItem();
         ]]>
@@ -1555,27 +1564,16 @@ extends="chrome://global/content/binding
         <parameter name="aText"/>
         <parameter name="aNoEmphasis"/>
         <body>
           <![CDATA[
           // Get rid of all previous text
           while (aDescriptionElement.hasChildNodes())
             aDescriptionElement.removeChild(aDescriptionElement.firstChild);
 
-          // Add a separator to the front of the URL and action.
-          if (aText &&
-              (aDescriptionElement == this._urlText ||
-               aDescriptionElement == this._actionText)) {
-            let span =
-              document.createElementNS("http://www.w3.org/1999/xhtml", "span");
-            aDescriptionElement.appendChild(span);
-            span.className = "ac-title-urlaction-separator";
-            span.textContent = "—";
-          }
-
           // If aNoEmphasis is specified, don't add any emphasis
           if (aNoEmphasis) {
             aDescriptionElement.appendChild(document.createTextNode(aText));
             return;
           }
 
           // Get the indices that separate match and non-match text
           let search = this.getAttribute("text");
@@ -1997,45 +1995,49 @@ extends="chrome://global/content/binding
           this._actionText.style.removeProperty("max-width");
           ]]>
         </body>
       </method>
 
       <!-- This method truncates the displayed strings as necessary. -->
       <method name="_handleOverflow">
         <body><![CDATA[
+          let itemRect = this.parentNode.getBoundingClientRect();
           let titleRect = this._titleText.getBoundingClientRect();
           let tagsRect = this._tagsText.getBoundingClientRect();
+          let separatorRect = this._separator.getBoundingClientRect();
           let urlRect = this._urlText.getBoundingClientRect();
           let actionRect = this._actionText.getBoundingClientRect();
-          let urlActionWidth = Math.max(urlRect.width, actionRect.width);
+          let separatorURLActionWidth =
+            separatorRect.width + Math.max(urlRect.width, actionRect.width);
 
           // Total width for the title and URL/action is the width of the item
           // minus the start of the title text minus a little extra padding.
-          // This extra padding amount is basically arbitrary but balances out
-          // the listbox's padding on the left side.
+          // This extra padding amount is basically arbitrary but keeps the text
+          // from getting too close to the popup's edge.
           let extraPadding = 30;
-          let itemWidth =
-            this.parentNode.getBoundingClientRect().width -
-            this._titleText.getBoundingClientRect().left -
-            extraPadding;
+          let dir =
+            this.ownerDocument.defaultView.getComputedStyle(this).direction;
+          let titleStart = dir == "rtl" ? itemRect.right - titleRect.right
+                                        : titleRect.left - itemRect.left;
+          let itemWidth = itemRect.width - titleStart - extraPadding;
 
           if (this._tags.hasAttribute("empty")) {
             // The tags box is not displayed in this case.
             tagsRect.width = 0;
           }
 
           let titleTagsWidth = titleRect.width + tagsRect.width;
-          if (titleTagsWidth + urlActionWidth > itemWidth) {
+          if (titleTagsWidth + separatorURLActionWidth > itemWidth) {
             // Title + tags + URL/action overflows the item width.
 
             // The percentage of the item width allocated to the title and tags.
             let titleTagsPct = 0.66;
 
-            let titleTagsAvailable = itemWidth - urlActionWidth;
+            let titleTagsAvailable = itemWidth - separatorURLActionWidth;
             let titleTagsMaxWidth = Math.max(
               titleTagsAvailable,
               itemWidth * titleTagsPct
             );
             if (titleTagsWidth > titleTagsMaxWidth) {
               // Title + tags overflows the max title + tags width.
 
               // The percentage of the title + tags width allocated to the
@@ -2050,21 +2052,21 @@ extends="chrome://global/content/binding
               let tagsAvailable = titleTagsMaxWidth - titleRect.width;
               let tagsMaxWidth = Math.max(
                 tagsAvailable,
                 titleTagsMaxWidth * (1 - titlePct)
               );
               this._titleText.style.maxWidth = titleMaxWidth + "px";
               this._tagsText.style.maxWidth = tagsMaxWidth + "px";
             }
-            let urlActionAvailable = itemWidth - titleTagsWidth;
             let urlActionMaxWidth = Math.max(
-              urlActionAvailable,
+              itemWidth - titleTagsWidth,
               itemWidth * (1 - titleTagsPct)
             );
+            urlActionMaxWidth -= separatorRect.width;
             this._urlText.style.maxWidth = urlActionMaxWidth + "px";
             this._actionText.style.maxWidth = urlActionMaxWidth + "px";
           }
         ]]></body>
       </method>
 
       <method name="handleOverUnderflow">
         <body>
--- a/toolkit/modules/addons/WebRequest.jsm
+++ b/toolkit/modules/addons/WebRequest.jsm
@@ -570,17 +570,17 @@ HttpObserverManager = {
       let result = null;
       try {
         result = callback(data);
       } catch (e) {
         Cu.reportError(e);
       }
 
       if (!result || !opts.blocking) {
-        return true;
+        continue;
       }
       if (result.cancel) {
         channel.cancel(Cr.NS_ERROR_ABORT);
         this.errorCheck(channel, loadContext);
         return false;
       }
       if (result.redirectUrl) {
         channel.redirectTo(BrowserUtils.makeURI(result.redirectUrl));
--- a/toolkit/themes/linux/global/autocomplete.css
+++ b/toolkit/themes/linux/global/autocomplete.css
@@ -143,30 +143,32 @@ html|span.ac-tag {
   -moz-margin-end: 2px;
 }
 
 .ac-tags {
   -moz-margin-start: 0;
   -moz-margin-end: 4px;
 }
 
-html|span.ac-title-urlaction-separator {
+.ac-separator {
   padding-left: 0;
   padding-right: 6px;
 }
 
 /* Better align the URL/action with the title. */
 .ac-tags,
+.ac-separator,
 .ac-url,
 .ac-action {
   margin-bottom: -2px;
 }
 
 .ac-title-text,
 .ac-tags-text,
+.ac-separator-text,
 .ac-url-text,
 .ac-action-text,
 .ac-text-overflow-container {
   padding: 0 !important;
   margin: 0 !important;
 }
 
 /* ::::: textboxes inside toolbarpaletteitems ::::: */
--- a/toolkit/themes/osx/global/autocomplete.css
+++ b/toolkit/themes/osx/global/autocomplete.css
@@ -121,30 +121,32 @@ html|span.ac-tag {
   -moz-margin-end: 2px;
 }
 
 .ac-tags {
   -moz-margin-start: 0;
   -moz-margin-end: 4px;
 }
 
-html|span.ac-title-urlaction-separator {
+.ac-separator {
   -moz-margin-start: 0;
   -moz-margin-end: 6px;
 }
 
 /* Better align the URL/action with the title. */
 .ac-tags,
+.ac-separator,
 .ac-url,
 .ac-action {
   margin-bottom: -2px;
 }
 
 .ac-title-text,
 .ac-tags-text,
+.ac-separator-text,
 .ac-url-text,
 .ac-action-text,
 .ac-text-overflow-container {
   padding: 0 !important;
   margin: 0 !important;
 }
 
 /* ::::: textboxes inside toolbarpaletteitems ::::: */
--- a/toolkit/themes/shared/in-content/common.inc.css
+++ b/toolkit/themes/shared/in-content/common.inc.css
@@ -100,17 +100,17 @@ xul|groupbox {
   -moz-appearance: none;
   border: none;
   margin: 15px 0 0;
   -moz-padding-start: 0;
   -moz-padding-end: 0;
   font-size: 1.25rem;
 }
 
-xul|groupbox xul|label:not(.menu-text),
+xul|groupbox xul|label:not(.menu-accel):not(.menu-text),
 xul|groupbox xul|description {
   /* !important needed to override toolkit !important rule */
   -moz-margin-start: 0 !important;
   -moz-margin-end: 0 !important;
 }
 
 /* tabpanels and tabs */
 
--- a/toolkit/themes/windows/global/autocomplete.css
+++ b/toolkit/themes/windows/global/autocomplete.css
@@ -125,30 +125,32 @@ html|span.ac-tag {
   -moz-margin-end: 2px;
 }
 
 .ac-tags {
   -moz-margin-start: 0;
   -moz-margin-end: 4px;
 }
 
-html|span.ac-title-urlaction-separator {
+.ac-separator {
   padding-left: 0;
   padding-right: 6px;
 }
 
 /* Better align the URL/action with the title. */
 .ac-tags,
+.ac-separator,
 .ac-url,
 .ac-action {
   margin-bottom: -2px;
 }
 
 .ac-title-text,
 .ac-tags-text,
+.ac-separator-text,
 .ac-url-text,
 .ac-action-text,
 .ac-text-overflow-container {
   padding: 0 !important;
   margin: 0 !important;
 }
 
 /* ::::: textboxes inside toolbarpaletteitems ::::: */
--- a/widget/windows/nsWindow.cpp
+++ b/widget/windows/nsWindow.cpp
@@ -7486,16 +7486,18 @@ nsWindow::DealWithPopups(HWND aWnd, UINT
   if (!popup) {
     return false;
   }
 
   static bool sSendingNCACTIVATE = false;
   static bool sPendingNCACTIVATE = false;
   uint32_t popupsToRollup = UINT32_MAX;
 
+  bool consumeRollupEvent = false;
+
   nsWindow* popupWindow = static_cast<nsWindow*>(popup.get());
   UINT nativeMessage = WinUtils::GetNativeMessage(aMessage);
   switch (nativeMessage) {
     case WM_LBUTTONDOWN:
     case WM_RBUTTONDOWN:
     case WM_MBUTTONDOWN:
     case WM_NCLBUTTONDOWN:
     case WM_NCRBUTTONDOWN:
@@ -7506,23 +7508,26 @@ nsWindow::DealWithPopups(HWND aWnd, UINT
       }
       return false;
 
     case WM_MOUSEWHEEL:
     case WM_MOUSEHWHEEL:
       // We need to check if the popup thinks that it should cause closing
       // itself when mouse wheel events are fired outside the rollup widget.
       if (!EventIsInsideWindow(popupWindow)) {
+        // Check if we should consume this event even if we don't roll-up:
+        consumeRollupEvent =
+          rollupListener->ShouldConsumeOnMouseWheelEvent();
         *aResult = MA_ACTIVATE;
         if (rollupListener->ShouldRollupOnMouseWheelEvent() &&
             GetPopupsToRollup(rollupListener, &popupsToRollup)) {
           break;
         }
       }
-      return false;
+      return consumeRollupEvent;
 
     case WM_ACTIVATEAPP:
       break;
 
     case WM_ACTIVATE:
       // NOTE: Don't handle WA_INACTIVE for preventing popup taking focus
       // because we cannot distinguish it's caused by mouse or not.
       if (LOWORD(aWParam) == WA_ACTIVE && aLParam) {
@@ -7631,30 +7636,31 @@ nsWindow::DealWithPopups(HWND aWnd, UINT
 
     default:
       return false;
   }
 
   // Only need to deal with the last rollup for left mouse down events.
   NS_ASSERTION(!mLastRollup, "mLastRollup is null");
 
-  bool consumeRollupEvent;
   if (nativeMessage == WM_LBUTTONDOWN) {
     POINT pt;
     pt.x = GET_X_LPARAM(aLParam);
     pt.y = GET_Y_LPARAM(aLParam);
     ::ClientToScreen(aWnd, &pt);
     nsIntPoint pos(pt.x, pt.y);
 
     consumeRollupEvent =
-      rollupListener->Rollup(popupsToRollup, true, &pos, &mLastRollup);
+      rollupListener->Rollup(popupsToRollup, true, &pos, &mLastRollup) ||
+      consumeRollupEvent;
     NS_IF_ADDREF(mLastRollup);
   } else {
     consumeRollupEvent =
-      rollupListener->Rollup(popupsToRollup, true, nullptr, nullptr);
+      rollupListener->Rollup(popupsToRollup, true, nullptr, nullptr) ||
+      consumeRollupEvent;
   }
 
   // Tell hook to stop processing messages
   sProcessHook = false;
   sRollupMsgId = 0;
   sRollupMsgWnd = nullptr;
 
   // If we are NOT supposed to be consuming events, let it go through