Merge m-c to inbound, a=merge CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Tue, 12 Apr 2016 15:36:02 -0700
changeset 316706 3e3dd1575611181dc179316703e105622cdecbb6
parent 316705 b9b725d135289eed40c18112555d7b009ac9ee47 (current diff)
parent 316654 fb921246e2d60f521f83defed54e30a38df1be3e (diff)
child 316707 81d0c00bb82f3c62dc279d31199f168c9c648fb6
push id9480
push userjlund@mozilla.com
push dateMon, 25 Apr 2016 17:12:58 +0000
treeherdermozilla-aurora@0d6a91c76a9e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone48.0a1
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