Bug 1344924 - Contextual onboarding for search suggestions in the awesomebar. r=adw draft
authorMarco Bonardo <mbonardo@mozilla.com>
Wed, 05 Apr 2017 15:01:02 +0200
changeset 579391 819ba7a77998940fb230fb531dd9fcf1301cfcd5
parent 579118 41958333867b0f537271dbd4cb4ba9e8a67a85a8
child 628992 8b299952341f76cd0e385b901093894e23116c4b
push id59236
push usermak77@bonardo.net
push dateWed, 17 May 2017 09:19:22 +0000
reviewersadw
bugs1344924
milestone55.0a1
Bug 1344924 - Contextual onboarding for search suggestions in the awesomebar. r=adw Make the onboarding being opt-in or opt-out depending on the default value of the browser.urlbar.sugges.searches pref. In both cases respect userMadeSearchSuggestionChoice though, so we won't further nag users that made a choice already. MozReview-Commit-ID: D4rRMRbdMrW
browser/app/profile/firefox.js
browser/base/content/browser.css
browser/base/content/test/urlbar/browser.ini
browser/base/content/test/urlbar/browser_autocomplete_a11y_label.js
browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js
browser/base/content/test/urlbar/browser_urlbarSearchSuggestionsNotification.js
browser/base/content/test/urlbar/browser_urlbarSearchSuggestions_opt-in.js
browser/base/content/test/urlbar/browser_urlbarSearchSuggestions_opt-out.js
browser/base/content/test/urlbar/browser_urlbarSearchTelemetry.js
browser/base/content/urlbarBindings.xml
browser/components/extensions/test/browser/browser_ext_omnibox.js
browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
browser/locales/en-US/chrome/browser/browser.dtd
browser/modules/test/browser/browser_UsageTelemetry_urlbar.js
browser/themes/shared/urlbarSearchSuggestionsNotification.inc.css
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -315,20 +315,21 @@ pref("browser.urlbar.match.url", "@");
 
 // The default behavior for the urlbar can be configured to use any combination
 // of the match filters with each additional filter adding more results (union).
 pref("browser.urlbar.suggest.history",              true);
 pref("browser.urlbar.suggest.bookmark",             true);
 pref("browser.urlbar.suggest.openpage",             true);
 pref("browser.urlbar.suggest.searches",             false);
 pref("browser.urlbar.userMadeSearchSuggestionsChoice", false);
-// 4 here means the suggestion notification will be automatically
-// hidden the 4th day, so it will actually be shown on 3 different days.
+// The suggestion opt-in notification will be shown on 4 different days.
 pref("browser.urlbar.daysBeforeHidingSuggestionsPrompt", 4);
 pref("browser.urlbar.lastSuggestionsPromptDate", 20160601);
+// The suggestion opt-out hint will be hidden after being shown 4 times.
+pref("browser.urlbar.timesBeforeHidingSuggestionsHint", 4);
 
 // Limit the number of characters sent to the current search engine to fetch
 // suggestions.
 pref("browser.urlbar.maxCharsForSearchSuggestions", 20);
 
 // Restrictions to current suggestions can also be applied (intersection).
 // Typed suggestion works only if history is set to true.
 pref("browser.urlbar.suggest.history.onlyTyped",    false);
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -584,22 +584,22 @@ html|input.urlbar-input[textoverflow]:no
 #PopupAutoCompleteRichResult {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#urlbar-rich-result-popup");
 }
 
 #PopupAutoCompleteRichResult.showSearchSuggestionsNotification {
   transition: height 100ms;
 }
 
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] {
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] {
   visibility: collapse;
   transition: margin-top 100ms;
 }
 
-#PopupAutoCompleteRichResult.showSearchSuggestionsNotification > hbox[anonid="search-suggestions-notification"] {
+#PopupAutoCompleteRichResult.showSearchSuggestionsNotification > deck[anonid="search-suggestions-notification"] {
   visibility: visible;
 }
 
 #PopupAutoCompleteRichResult > richlistbox {
   transition: height 100ms;
 }
 
 #PopupAutoCompleteRichResult.showSearchSuggestionsNotification > richlistbox {
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -75,17 +75,21 @@ support-files =
 [browser_urlbarPrivateBrowsingWindowChange.js]
 [browser_urlbarRaceWithTabs.js]
 [browser_urlbarRevert.js]
 [browser_urlbarSearchSingleWordNotification.js]
 [browser_urlbarSearchSuggestions.js]
 support-files =
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
-[browser_urlbarSearchSuggestionsNotification.js]
+[browser_urlbarSearchSuggestions_opt-in.js]
+support-files =
+  searchSuggestionEngine.xml
+  searchSuggestionEngine.sjs
+[browser_urlbarSearchSuggestions_opt-out.js]
 support-files =
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
 [browser_urlbarSearchTelemetry.js]
 support-files =
   searchSuggestionEngine.xml
   searchSuggestionEngine.sjs
 [browser_urlbarStop.js]
--- a/browser/base/content/test/urlbar/browser_autocomplete_a11y_label.js
+++ b/browser/base/content/test/urlbar/browser_autocomplete_a11y_label.js
@@ -20,21 +20,22 @@ add_task(async function switchToTab() {
   gBrowser.removeTab(tab);
 });
 
 add_task(async function searchSuggestions() {
   let engine = await promiseNewSearchEngine(TEST_ENGINE_BASENAME);
   let oldCurrentEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
   Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
   Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
   registerCleanupFunction(function() {
     Services.search.currentEngine = oldCurrentEngine;
     Services.prefs.clearUserPref(SUGGEST_ALL_PREF);
-    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+    Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
   });
 
   await promiseAutocompleteResultPopup("foo");
   // Don't assume that the search doesn't match history or bookmarks left around
   // by earlier tests.
   Assert.ok(gURLBar.popup.richlistbox.children.length >= 3,
             "Should get at least heuristic result + two search suggestions");
   // The first expected search is the search term itself since the heuristic
--- a/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions.js
@@ -1,19 +1,20 @@
 const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
 const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
 
 // Must run first.
 add_task(async function prepare() {
+  let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
   Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
   let engine = await promiseNewSearchEngine(TEST_ENGINE_BASENAME);
   let oldCurrentEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
   registerCleanupFunction(async function() {
-    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+    Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
     Services.search.currentEngine = oldCurrentEngine;
 
     // Clicking suggestions causes visits to search results pages, so clear that
     // history now.
     await PlacesTestUtils.clearHistory();
 
     // Make sure the popup is closed for the next test.
     gURLBar.blur();
rename from browser/base/content/test/urlbar/browser_urlbarSearchSuggestionsNotification.js
rename to browser/base/content/test/urlbar/browser_urlbarSearchSuggestions_opt-in.js
--- a/browser/base/content/test/urlbar/browser_urlbarSearchSuggestionsNotification.js
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions_opt-in.js
@@ -3,44 +3,47 @@ const SUGGEST_URLBAR_PREF = "browser.url
 const CHOICE_PREF = "browser.urlbar.userMadeSearchSuggestionsChoice";
 const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
 
 // Must run first.
 add_task(async function prepare() {
   let engine = await promiseNewSearchEngine(TEST_ENGINE_BASENAME);
   let oldCurrentEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
+  let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
+  let defaults = Services.prefs.getDefaultBranch("browser.urlbar.");
+  let searchSuggestionsDefault = defaults.getBoolPref("suggest.searches");
+  defaults.setBoolPref("suggest.searches", false);
   registerCleanupFunction(async function() {
+    defaults.setBoolPref("suggest.searches", searchSuggestionsDefault);
     Services.search.currentEngine = oldCurrentEngine;
     Services.prefs.clearUserPref(SUGGEST_ALL_PREF);
-    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+    Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
 
     // Disable the notification for future tests so it doesn't interfere with
     // them.  clearUserPref() won't work because by default the pref is false.
-    await setUserMadeChoicePref(true);
+    Services.prefs.setBoolPref(CHOICE_PREF, true);
 
     // Make sure the popup is closed for the next test.
     gURLBar.blur();
     Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
   });
 });
 
 add_task(async function focus() {
   // Focusing the urlbar used to open the popup in order to show the
   // notification, but it doesn't anymore.  Make sure it does not.
-  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
-  await setUserMadeChoicePref(false);
+  setupVisibleNotification();
   gURLBar.blur();
   gURLBar.focus();
   Assert.ok(!gURLBar.popup.popupOpen, "popup should remain closed");
 });
 
 add_task(async function dismissWithoutResults() {
-  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
-  await setUserMadeChoicePref(false);
+  setupVisibleNotification();
   gURLBar.blur();
   gURLBar.focus();
   let popupPromise = promisePopupShown(gURLBar.popup);
   gURLBar.openPopup();
   await popupPromise;
   Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
   assertVisible(true);
   Assert.equal(gURLBar.popup._matchCount, 0, "popup should have no results");
@@ -55,18 +58,17 @@ add_task(async function dismissWithoutRe
   gURLBar.focus();
   Assert.ok(!gURLBar.popup.popupOpen, "popup should remain closed");
   await promiseAutocompleteResultPopup("foo");
   Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
   assertVisible(false);
 });
 
 add_task(async function dismissWithResults() {
-  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
-  await setUserMadeChoicePref(false);
+  setupVisibleNotification();
   gURLBar.blur();
   gURLBar.focus();
   await promiseAutocompleteResultPopup("foo");
   Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
   assertVisible(true);
   Assert.ok(gURLBar.popup._matchCount > 0, "popup should have results");
   let disableButton = document.getAnonymousElementByAttribute(
     gURLBar.popup, "anonid", "search-suggestions-notification-disable"
@@ -79,18 +81,17 @@ add_task(async function dismissWithResul
   gURLBar.focus();
   Assert.ok(!gURLBar.popup.popupOpen, "popup should remain closed");
   await promiseAutocompleteResultPopup("foo");
   Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
   assertVisible(false);
 });
 
 add_task(async function disable() {
-  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
-  await setUserMadeChoicePref(false);
+  setupVisibleNotification();
   gURLBar.blur();
   gURLBar.focus();
   await promiseAutocompleteResultPopup("foo");
   Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
   assertVisible(true);
   let disableButton = document.getAnonymousElementByAttribute(
     gURLBar.popup, "anonid", "search-suggestions-notification-disable"
   );
@@ -98,19 +99,17 @@ add_task(async function disable() {
   disableButton.click();
   await transitionPromise;
   gURLBar.blur();
   await promiseAutocompleteResultPopup("foo");
   Assert.ok(!suggestionsPresent());
 });
 
 add_task(async function enable() {
-  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
-  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
-  await setUserMadeChoicePref(false);
+  setupVisibleNotification();
   gURLBar.blur();
   gURLBar.focus();
   await promiseAutocompleteResultPopup("foo");
   assertVisible(true);
   Assert.ok(!suggestionsPresent());
   let enableButton = document.getAnonymousElementByAttribute(
     gURLBar.popup, "anonid", "search-suggestions-notification-enable"
   );
@@ -126,31 +125,30 @@ add_task(async function enable() {
   // Suggestions should still be present in a new search of course.
   await promiseAutocompleteResultPopup("bar");
   Assert.ok(suggestionsPresent());
 });
 
 add_task(async function privateWindow() {
   // Since suggestions are disabled in private windows, the notification should
   // not appear even when suggestions are otherwise enabled.
+  setupVisibleNotification();
   let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
   win.gURLBar.blur();
   win.gURLBar.focus();
   await promiseAutocompleteResultPopup("foo", win);
   assertVisible(false, win);
   win.gURLBar.blur();
   await BrowserTestUtils.closeWindow(win);
 });
 
 add_task(async function multipleWindows() {
   // Opening multiple windows, using their urlbars, and then dismissing the
   // notification in one should dismiss the notification in all.
-  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
-  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
-  await setUserMadeChoicePref(false);
+  setupVisibleNotification();
 
   gURLBar.focus();
   await promiseAutocompleteResultPopup("win1");
   assertVisible(true);
 
   let win2 = await BrowserTestUtils.openNewBrowserWindow();
   win2.gURLBar.focus();
   await promiseAutocompleteResultPopup("win2", win2);
@@ -179,49 +177,29 @@ add_task(async function multipleWindows(
 
   await BrowserTestUtils.closeWindow(win2);
   await BrowserTestUtils.closeWindow(win3);
 });
 
 add_task(async function enableOutsideNotification() {
   // Setting the suggest.searches pref outside the notification (e.g., by
   // ticking the checkbox in the preferences window) should hide it.
-  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
-  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
-  await setUserMadeChoicePref(false);
-
+  setupVisibleNotification();
   Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
   gURLBar.focus();
   await promiseAutocompleteResultPopup("foo");
   assertVisible(false);
 });
 
-/**
- * Setting the choice pref triggers a pref observer in the urlbar, which hides
- * the notification if it's present.  This function returns a promise that's
- * resolved once the observer fires.
- *
- * @param userMadeChoice  A boolean, the pref's new value.
- * @return A Promise that's resolved when the observer fires -- or, if the pref
- *         is currently the given value, that's resolved immediately.
- */
-function setUserMadeChoicePref(userMadeChoice) {
-  return new Promise(resolve => {
-    let currentUserMadeChoice = Services.prefs.getBoolPref(CHOICE_PREF);
-    if (currentUserMadeChoice != userMadeChoice) {
-      Services.prefs.addObserver(CHOICE_PREF, function obs(subj, topic, data) {
-        Services.prefs.removeObserver(CHOICE_PREF, obs);
-        resolve();
-      });
-    }
-    Services.prefs.setBoolPref(CHOICE_PREF, userMadeChoice);
-    if (currentUserMadeChoice == userMadeChoice) {
-      resolve();
-    }
-  });
+function setupVisibleNotification() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  // Toggle to reset the whichNotification cache.
+  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
 }
 
 function suggestionsPresent() {
   let controller = gURLBar.popup.input.controller;
   let matchCount = controller.matchCount;
   for (let i = 0; i < matchCount; i++) {
     let url = controller.getValueAt(i);
     let mozActionMatch = url.match(/^moz-action:([^,]+),(.*)$/);
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchSuggestions_opt-out.js
@@ -0,0 +1,118 @@
+// The order of the tests here matters!
+
+const SUGGEST_ALL_PREF = "browser.search.suggest.enabled";
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const CHOICE_PREF = "browser.urlbar.userMadeSearchSuggestionsChoice";
+const TIMES_PREF = "browser.urlbar.timesBeforeHidingSuggestionsHint";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+add_task(async function prepare() {
+  let engine = await promiseNewSearchEngine(TEST_ENGINE_BASENAME);
+  let oldCurrentEngine = Services.search.currentEngine;
+  Services.search.currentEngine = engine;
+  let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
+  let defaults = Services.prefs.getDefaultBranch("browser.urlbar.");
+  let searchSuggestionsDefault = defaults.getBoolPref("suggest.searches");
+  defaults.setBoolPref("suggest.searches", true);
+  let suggestionsChoice = Services.prefs.getBoolPref(CHOICE_PREF);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  registerCleanupFunction(async function() {
+    defaults.setBoolPref("suggest.searches", searchSuggestionsDefault);
+    Services.search.currentEngine = oldCurrentEngine;
+    Services.prefs.clearUserPref(SUGGEST_ALL_PREF);
+    Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
+    Services.prefs.setBoolPref(CHOICE_PREF, suggestionsChoice);
+
+    // Make sure the popup is closed for the next test.
+    gURLBar.blur();
+    Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+  });
+});
+
+add_task(async function focus() {
+  // Focusing the urlbar should open the popup in order to show the
+  // notification.
+  setupVisibleHint();
+  gURLBar.blur();
+  let popupPromise = promisePopupShown(gURLBar.popup);
+  gURLBar.focus();
+  await popupPromise;
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(true);
+  Assert.equal(gURLBar.popup._matchCount, 0, "popup should have no results");
+
+  // Start searching.
+  EventUtils.synthesizeKey("r", {});
+  EventUtils.synthesizeKey("n", {});
+  EventUtils.synthesizeKey("d", {});
+  await promiseSearchComplete();
+  Assert.ok(suggestionsPresent());
+  assertVisible(true);
+
+  // Check the Change Options link.
+  let changeOptionsLink = document.getElementById("search-suggestions-change-settings");
+  let prefsPromise = BrowserTestUtils.waitForLocationChange(gBrowser, "about:preferences#general-search");
+  changeOptionsLink.click();
+  await prefsPromise;
+  Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+});
+
+
+add_task(async function privateWindow() {
+  // Since suggestions are disabled in private windows, the notification should
+  // not appear even when suggestions are otherwise enabled.
+  setupVisibleHint();
+  let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+  await promiseAutocompleteResultPopup("foo", win);
+  assertVisible(false, win);
+  win.gURLBar.blur();
+  await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function enableOutsideNotification() {
+  // Setting the suggest.searches pref outside the notification (e.g., by
+  // ticking the checkbox in the preferences window) should hide it.
+  setupVisibleHint();
+  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
+  await promiseAutocompleteResultPopup("foo");
+  assertVisible(false);
+});
+
+add_task(async function userMadeChoice() {
+  // If the user made a choice already, he should not see the hint.
+  setupVisibleHint();
+  Services.prefs.setBoolPref(CHOICE_PREF, true);
+  await promiseAutocompleteResultPopup("foo");
+  assertVisible(false);
+});
+
+function setupVisibleHint() {
+  Services.prefs.clearUserPref(TIMES_PREF);
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  // Toggle to reset the whichNotification cache.
+  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
+  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
+}
+
+function suggestionsPresent() {
+  let controller = gURLBar.popup.input.controller;
+  let matchCount = controller.matchCount;
+  for (let i = 0; i < matchCount; i++) {
+    let url = controller.getValueAt(i);
+    let mozActionMatch = url.match(/^moz-action:([^,]+),(.*)$/);
+    if (mozActionMatch) {
+      let [, type, paramStr] = mozActionMatch;
+      let params = JSON.parse(paramStr);
+      if (type == "searchengine" && "searchSuggestion" in params) {
+        return true;
+      }
+    }
+  }
+  return false;
+}
+
+function assertVisible(visible, win = window) {
+  let style =
+    win.getComputedStyle(win.gURLBar.popup.searchSuggestionsNotification);
+  Assert.equal(style.visibility, visible ? "visible" : "collapse");
+}
--- a/browser/base/content/test/urlbar/browser_urlbarSearchTelemetry.js
+++ b/browser/base/content/test/urlbar/browser_urlbarSearchTelemetry.js
@@ -2,23 +2,24 @@
 
 Cu.import("resource:///modules/BrowserUITelemetry.jsm");
 
 const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
 const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
 
 // Must run first.
 add_task(async function prepare() {
+  let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
   Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
   let engine = await promiseNewSearchEngine(TEST_ENGINE_BASENAME);
   let oldCurrentEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
 
   registerCleanupFunction(async function() {
-    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+    Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
     Services.search.currentEngine = oldCurrentEngine;
 
     // Clicking urlbar results causes visits to their associated pages, so clear
     // that history now.
     await PlacesTestUtils.clearHistory();
 
     // Make sure the popup is closed for the next test.
     gURLBar.blur();
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -65,25 +65,28 @@ file, You can obtain one at http://mozil
       <field name="ExtensionSearchHandler" readonly="true">
         (Components.utils.import("resource://gre/modules/ExtensionSearchHandler.jsm", {})).ExtensionSearchHandler;
       </field>
 
       <constructor><![CDATA[
         this._prefs = Components.classes["@mozilla.org/preferences-service;1"]
                                 .getService(Components.interfaces.nsIPrefService)
                                 .getBranch("browser.urlbar.");
+        this._prefs.addObserver("", this);
 
-        this._prefs.addObserver("", this);
+        this._defaultPrefs = Components.classes["@mozilla.org/preferences-service;1"]
+                                       .getService(Components.interfaces.nsIPrefService)
+                                       .getDefaultBranch("browser.urlbar.");
+
         this.clickSelectsAll = this._prefs.getBoolPref("clickSelectsAll");
         this.doubleClickSelectsAll = this._prefs.getBoolPref("doubleClickSelectsAll");
         this.completeDefaultIndex = this._prefs.getBoolPref("autoFill");
         this.timeout = this._prefs.getIntPref("delay");
         this._formattingEnabled = this._prefs.getBoolPref("formatting.enabled");
         this._mayTrimURLs = this._prefs.getBoolPref("trimURLs");
-        this._cacheUserMadeSearchSuggestionsChoice();
         this.inputField.controllers.insertControllerAt(0, this._copyCutController);
         this.inputField.addEventListener("paste", this);
         this.inputField.addEventListener("mousedown", this);
         this.inputField.addEventListener("mousemove", this);
         this.inputField.addEventListener("mouseout", this);
         this.inputField.addEventListener("overflow", this);
         this.inputField.addEventListener("underflow", this);
 
@@ -940,24 +943,18 @@ file, You can obtain one at http://mozil
               case "formatting.enabled":
                 this._formattingEnabled = this._prefs.getBoolPref(aData);
                 break;
               case "suggest.searches":
                 // Mirror the value for future use, see the comment in the
                 // binding's constructor.
                 this._prefs.setBoolPref("searchSuggestionsChoice",
                   this._prefs.getBoolPref("suggest.searches"));
-                // fall-through.
-              case "userMadeSearchSuggestionsChoice":
-                this._cacheUserMadeSearchSuggestionsChoice();
-                if (this._userMadeSearchSuggestionsChoice) {
-                  this.popup.searchSuggestionsNotificationWasDismissed(
-                    this._prefs.getBoolPref("suggest.searches")
-                  );
-                }
+                // Clear the cached value to allow changing conditions in tests.
+                delete this._whichSearchSuggestionsNotification;
                 break;
               case "trimURLs":
                 this._mayTrimURLs = this._prefs.getBoolPref(aData);
                 break;
               case "oneOffSearches":
                 this._enableOrDisableOneOffSearches();
                 break;
             }
@@ -1198,40 +1195,80 @@ file, You can obtain one at http://mozil
               this.popup._isFirstResultHeuristic) {
             this.mController.handleText();
             return false;
           }
           return this.mController.handleDelete();
         ]]></body>
       </method>
 
-      <field name="_userMadeSearchSuggestionsChoice"><![CDATA[
-        false
-      ]]></field>
+      <property name="_userMadeSearchSuggestionsChoice" readonly="true">
+        <getter><![CDATA[
+          return this._prefs.getBoolPref("userMadeSearchSuggestionsChoice") ||
+                 this._defaultPrefs.getBoolPref("suggest.searches") != this._prefs.getBoolPref("suggest.searches");
+        ]]></getter>
+      </property>
+
+      <property name="whichSearchSuggestionsNotification" readonly="true">
+        <getter><![CDATA[
+          // Once we return "none" once, we'll always return "none".
+          // If available, use the cached value, rather than running all of the
+          // checks again at every locationbar focus.
+          if (this._whichSearchSuggestionsNotification) {
+            return this._whichSearchSuggestionsNotification;
+          }
 
-      <method name="_cacheUserMadeSearchSuggestionsChoice">
+          if (Services.prefs.getBoolPref("browser.search.suggest.enabled") &&
+              !this.inPrivateContext &&
+              // When _urlbarFocused is true, tabbrowser would close the
+              // popup if it's opened here, so don't show the notification.
+              !gBrowser.selectedBrowser._urlbarFocused &&
+              // In any case, if the user made a choice we should not nag him.
+              !this._userMadeSearchSuggestionsChoice) {
+            let enabledByDefault = this._defaultPrefs.getBoolPref("suggest.searches");
+            if (!enabledByDefault &&
+                this._prefs.getIntPref("daysBeforeHidingSuggestionsPrompt")) {
+              return "opt-in";
+            }
+            if (enabledByDefault &&
+                // Has not been switched off.
+                this._prefs.getBoolPref("suggest.searches") &&
+                this._prefs.getIntPref("timesBeforeHidingSuggestionsHint")) {
+              return "opt-out";
+            }
+          }
+          return this._whichSearchSuggestionsNotification = "none";
+        ]]></getter>
+      </property>
+
+      <method name="updateSearchSuggestionsNotificationImpressions">
+        <parameter name="whichNotification"/>
         <body><![CDATA[
-          this._userMadeSearchSuggestionsChoice =
-            this._prefs.getBoolPref("userMadeSearchSuggestionsChoice") ||
-            this._prefs.getBoolPref("suggest.searches");
+          if (whichNotification == "none") {
+            throw new Error("Unexpected notification type");
+          }
+
+          let useDays = whichNotification == "opt-in";
+          let prefName = useDays ? "daysBeforeHidingSuggestionsPrompt"
+                                 : "timesBeforeHidingSuggestionsHint";
+          let remaining = this._prefs.getIntPref(prefName);
+          if (remaining <= 0)
+            return;
+
+          let now = new Date();
+          let date = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate();
+
+          let previousDate = this._prefs.getIntPref("lastSuggestionsPromptDate");
+          if (!useDays || previousDate != date) {
+            this._prefs.setIntPref(prefName, remaining - 1);
+          }
+          this._prefs.setIntPref("lastSuggestionsPromptDate", date);
         ]]></body>
       </method>
 
-      <property name="shouldShowSearchSuggestionsNotification" readonly="true">
-        <getter><![CDATA[
-          return !this._userMadeSearchSuggestionsChoice &&
-                 !this.inPrivateContext &&
-                 // When _urlbarFocused is true, tabbrowser would close the
-                 // popup if it's opened here, so don't show the notification.
-                 !gBrowser.selectedBrowser._urlbarFocused &&
-                 Services.prefs.getBoolPref("browser.search.suggest.enabled") &&
-                 this._prefs.getIntPref("daysBeforeHidingSuggestionsPrompt");
-        ]]></getter>
-      </property>
-
     </implementation>
 
     <handlers>
       <handler event="keydown"><![CDATA[
         if (this._noActionKeys.has(event.keyCode) &&
             this.popup.selectedIndex >= 0 &&
             !this._pressedNoActionKeys.has(event.keyCode)) {
           if (this._pressedNoActionKeys.size == 0) {
@@ -1253,16 +1290,48 @@ file, You can obtain one at http://mozil
 
       <handler event="focus"><![CDATA[
         if (event.originalTarget == this.inputField) {
           this._hideURLTooltip();
           this.formatValue();
           if (this.getAttribute("pageproxystate") != "valid") {
             UpdatePopupNotificationsVisibility();
           }
+
+          // We may want to show the search suggestions hint.
+          if (!gBrowser.selectedBrowser._urlbarFocused) {
+            // We show the opt-out notification on every kind of focus to the urlbar
+            // included opening a new tab, but we want to enforce at least one
+            // notification when the user focuses it with the mouse.
+            let whichNotification = this.whichSearchSuggestionsNotification;
+            if (whichNotification == "opt-out" &&
+                this._showSearchSuggestionNotificationOnMouseFocus === undefined) {
+              this._showSearchSuggestionNotificationOnMouseFocus = true;
+            }
+
+            // Check whether the focus change came from a user mouse action.
+            let focusMethod = Services.focus.getLastFocusMethod(window);
+            let mouseFocused = !!(focusMethod & Services.focus.FLAG_BYMOUSE);
+            if (this._showSearchSuggestionNotificationOnMouseFocus &&
+                mouseFocused) {
+              // Force showing the opt-out notification.
+              this._whichSearchSuggestionsNotification = whichNotification = "opt-out";
+            }
+
+            if (whichNotification == "opt-out") {
+              try {
+                this.popup.openAutocompletePopup(this, this);
+              } finally {
+                if (mouseFocused) {
+                  delete this._whichSearchSuggestionsNotification;
+                  this._showSearchSuggestionNotificationOnMouseFocus = false;
+                }
+              }
+            }
+          }
         }
       ]]></handler>
 
       <handler event="blur"><![CDATA[
         if (event.originalTarget == this.inputField) {
           this._clearNoActions();
           this.formatValue();
           if (this.getAttribute("pageproxystate") != "valid") {
@@ -1330,58 +1399,91 @@ file, You can obtain one at http://mozil
 
     <resources>
       <stylesheet src="chrome://browser/content/search/searchbarBindings.css"/>
       <stylesheet src="chrome://browser/skin/searchbar.css"/>
     </resources>
 
     <content ignorekeys="true" level="top" consumeoutsideclicks="never"
              aria-owns="richlistbox">
-      <xul:hbox anonid="search-suggestions-notification"
+      <xul:deck anonid="search-suggestions-notification"
                 align="center"
                 role="alert"
-                aria-describedby="search-suggestions-notification-text">
-        <xul:description flex="1">
-          &urlbar.searchSuggestionsNotification.question;
-          <!-- Several things here are to make the label accessibile via an
-               accesskey so that a11y doesn't suck: the accesskey, using an
-               onclick handler instead of an href attribute, the control
-               attribute, and having the control attribute refer to a valid ID
-               that is the label itself. -->
-          <xul:label id="search-suggestions-notification-learn-more"
+                selectedIndex="0">
+        <!-- OPT-IN -->
+        <xul:hbox flex="1" align="center" anonid="search-suggestions-opt-in">
+          <xul:description flex="1" id="search-suggestions-question">
+            &urlbar.searchSuggestionsNotification.question;
+            <!-- Several things here are to make the label accessibile via an
+                accesskey so that a11y doesn't suck: the accesskey, using an
+                onclick handler instead of an href attribute, the control
+                attribute, and having the control attribute refer to a valid ID
+                that is the label itself. -->
+            <xul:label id="search-suggestions-learn-more"
+                      class="text-link"
+                      role="link"
+                      value="&urlbar.searchSuggestionsNotification.learnMore;"
+                      accesskey="&urlbar.searchSuggestionsNotification.learnMore.accesskey;"
+                      onclick="document.getBindingParent(this).openSearchSuggestionsNotificationLearnMoreURL();"
+                      control="search-suggestions-learn-more"/>
+          </xul:description>
+          <xul:button anonid="search-suggestions-notification-disable"
+                      label="&urlbar.searchSuggestionsNotification.disable;"
+                      accesskey="&urlbar.searchSuggestionsNotification.disable.accesskey;"
+                      onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(false);"/>
+          <xul:button anonid="search-suggestions-notification-enable"
+                      label="&urlbar.searchSuggestionsNotification.enable;"
+                      accesskey="&urlbar.searchSuggestionsNotification.enable.accesskey;"
+                      onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(true);"/>
+        </xul:hbox>
+        <!-- OPT-OUT -->
+        <xul:hbox flex="1" align="center" anonid="search-suggestions-opt-out">
+          <xul:image class="ac-site-icon" type="searchengine"/>
+          <xul:hbox anonid="search-suggestions-hint-typing">
+            <xul:description class="ac-title-text">&brandShortName;</xul:description>
+          </xul:hbox>
+          <xul:hbox anonid="search-suggestions-hint-box" flex="1">
+            <xul:description id="search-suggestions-hint">
+              <html:span class="prefix">&#x1f4a1; &urlbar.searchSuggestionsNotification.hintPrefix;</html:span>
+              <html:span>&urlbar.searchSuggestionsNotification.hint;</html:span>
+            </xul:description>
+          </xul:hbox>
+          <xul:label id="search-suggestions-change-settings"
                      class="text-link"
                      role="link"
-                     value="&urlbar.searchSuggestionsNotification.learnMore;"
-                     accesskey="&urlbar.searchSuggestionsNotification.learnMore.accesskey;"
-                     onclick="document.getBindingParent(this).openSearchSuggestionsNotificationLearnMoreURL();"
-                     control="search-suggestions-notification-learn-more"/>
-        </xul:description>
-        <xul:button anonid="search-suggestions-notification-disable"
-                    label="&urlbar.searchSuggestionsNotification.disable;"
-                    accesskey="&urlbar.searchSuggestionsNotification.disable.accesskey;"
-                    onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(false);"/>
-        <xul:button anonid="search-suggestions-notification-enable"
-                    label="&urlbar.searchSuggestionsNotification.enable;"
-                    accesskey="&urlbar.searchSuggestionsNotification.enable.accesskey;"
-                    onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(true);"/>
-      </xul:hbox>
+#ifdef XP_WIN
+                     value="&urlbar.searchSuggestionsNotification.changeSettingsWin;"
+                     accesskey="&urlbar.searchSuggestionsNotification.changeSettingsWin.accesskey;"
+#else
+                     value="&urlbar.searchSuggestionsNotification.changeSettingsUnix;"
+                     accesskey="&urlbar.searchSuggestionsNotification.changeSettingsUnix.accesskey;"
+#endif
+                     onclick="openPreferences('general-search');"
+                     control="search-suggestions-change-settings"/>
+        </xul:hbox>
+      </xul:deck>
       <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox"
                        flex="1"/>
       <xul:hbox anonid="footer">
         <children/>
         <xul:vbox anonid="one-off-search-buttons"
                   class="search-one-offs"
                   compact="true"
                   includecurrentengine="true"
                   disabletab="true"
                   flex="1"/>
       </xul:hbox>
     </content>
 
     <implementation>
+      <field name="DOMWindowUtils">
+        window.QueryInterface(Ci.nsIInterfaceRequestor)
+              .getInterface(Ci.nsIDOMWindowUtils);
+      </field>
+
       <field name="_maxResults">0</field>
 
       <field name="_bundle" readonly="true">
         Cc["@mozilla.org/intl/stringbundle;1"].
           getService(Ci.nsIStringBundleService).
           createBundle("chrome://browser/locale/places/places.properties");
       </field>
 
@@ -1458,17 +1560,20 @@ file, You can obtain one at http://mozil
           this.input.mIgnoreFocus = false;
 
           Services.prefs.setBoolPref(
             "browser.urlbar.suggest.searches", enableSuggestions
           );
           Services.prefs.setBoolPref(
             "browser.urlbar.userMadeSearchSuggestionsChoice", true
           );
-          // The input's pref observer will now hide the notification.
+          // Hide the notification.
+          this.searchSuggestionsNotificationWasDismissed(
+            Services.prefs.getBoolPref("browser.urlbar.suggest.searches")
+          );
         ]]></body>
       </method>
 
       <!-- Override this so that navigating between items results in an item
            always being selected. -->
       <method name="getNextIndex">
         <parameter name="reverse"/>
         <parameter name="amount"/>
@@ -1546,39 +1651,16 @@ file, You can obtain one at http://mozil
         <parameter name="aInput"/>
         <parameter name="aElement"/>
         <body>
           <![CDATA[
           // initially the panel is hidden
           // to avoid impacting startup / new window performance
           aInput.popup.hidden = false;
 
-          let showNotification = aInput.shouldShowSearchSuggestionsNotification;
-          if (showNotification) {
-            let prefs = aInput._prefs;
-            let now = new Date();
-            let date = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate();
-            let previousDate = prefs.getIntPref("lastSuggestionsPromptDate");
-            if (previousDate < date) {
-              let remainingDays =
-                prefs.getIntPref("daysBeforeHidingSuggestionsPrompt") - 1;
-              prefs.setIntPref("daysBeforeHidingSuggestionsPrompt",
-                               remainingDays);
-              prefs.setIntPref("lastSuggestionsPromptDate", date);
-              if (!remainingDays)
-                showNotification = false;
-            }
-          }
-
-          if (showNotification) {
-            this._showSearchSuggestionsNotification();
-          } else if (this.classList.contains("showSearchSuggestionsNotification")) {
-            this._hideSearchSuggestionsNotification();
-          }
-
           this._openAutocompletePopup(aInput, aElement);
           ]]>
         </body>
       </method>
 
       <method name="_openAutocompletePopup">
         <parameter name="aInput"/>
         <parameter name="aElement"/>
@@ -1629,37 +1711,78 @@ file, You can obtain one at http://mozil
             this.siteIconStart = popupDirection == "rtl" ? identityRect.right
                                                          : identityRect.left;
           } else {
             // Reset the alignment so that the site icons are positioned
             // according to whatever's in the CSS.
             this.siteIconStart = undefined;
           }
 
+          try {
+            let whichNotification = aInput.whichSearchSuggestionsNotification;
+            if (whichNotification != "none") {
+              aInput.updateSearchSuggestionsNotificationImpressions(whichNotification);
+              this._showSearchSuggestionsNotification(whichNotification, popupDirection);
+            } else if (this.classList.contains("showSearchSuggestionsNotification")) {
+              this._hideSearchSuggestionsNotification();
+            }
+          } catch (ex) {
+            // Not critical for the urlbar functionality, just report the error.
+            Components.utils.reportError(ex);
+          }
+
           // Position the popup below the navbar.  To get the y-coordinate,
           // which is an offset from the bottom of the input, subtract the
           // bottom of the navbar from the buttom of the input.
           let yOffset =
-            document.getElementById("nav-bar").getBoundingClientRect().bottom -
-            aInput.getBoundingClientRect().bottom;
+            this.DOMWindowUtils.getBoundsWithoutFlushing(document.getElementById("nav-bar")).bottom -
+            this.DOMWindowUtils.getBoundsWithoutFlushing(aInput).bottom;
           this.openPopup(aElement, "after_start", 0, yOffset, false, false);
         ]]></body>
       </method>
 
       <method name="_updateFooterVisibility">
         <body>
           <![CDATA[
           this.footer.collapsed = this._matchCount == 0;
           ]]>
         </body>
       </method>
 
       <method name="_showSearchSuggestionsNotification">
+        <parameter name="whichNotification"/>
+        <parameter name="popupDirection"/>
         <body>
           <![CDATA[
+          let deckIndex = 0;
+          if (whichNotification == "opt-out") {
+            deckIndex = 1;
+
+            if (this.siteIconStart) {
+              let rect = this.DOMWindowUtils.getBoundsWithoutFlushing(window.document.documentElement);
+              let padding = popupDirection == "rtl" ? rect.right - this.siteIconStart
+                                                    : this.siteIconStart;
+              this.searchSuggestionsNotification.style.paddingInlineStart = padding + "px";
+            } else {
+              this.searchSuggestionsNotification.style.removeProperty("padding-inline-start");
+            }
+
+            // We want to animate the opt-out hint only once.
+            if (!this._firstSearchSuggestionsNotification) {
+              this._firstSearchSuggestionsNotification = true;
+              this.searchSuggestionsNotification.setAttribute("animate", "true");
+            }
+          }
+          this.searchSuggestionsNotification.setAttribute("selectedIndex", deckIndex);
+
+          let ariaDescElt = whichNotification == "opt-in" ?
+            "search-suggestions-question" : "search-suggestions-hint";
+
+          this.searchSuggestionsNotification.setAttribute("aria-describedby", ariaDescElt);
+
           // With the notification shown, the listbox's height can sometimes be
           // too small when it's flexed, as it normally is.  Also, it can start
           // out slightly scrolled down.  Both problems appear together, most
           // often when the popup is very narrow and the notification's text
           // must wrap.  Work around them by removing the flex.
           //
           // But without flexing the listbox, the listbox's height animation
           // sometimes fails to complete, leaving the popup too tall.  Work
@@ -1699,16 +1822,17 @@ file, You can obtain one at http://mozil
       </method>
 
       <method name="_hideSearchSuggestionsNotification">
         <body>
           <![CDATA[
           this.classList.remove("showSearchSuggestionsNotification");
           this.richlistbox.flex = 1;
           this.removeAttribute("dontanimate");
+          this.searchSuggestionsNotification.removeAttribute("animate");
           if (this._matchCount) {
             // Update popup height.
             this._invalidate();
           } else {
             this.closePopup();
           }
           ]]>
         </body>
--- a/browser/components/extensions/test/browser/browser_ext_omnibox.js
+++ b/browser/components/extensions/test/browser/browser_ext_omnibox.js
@@ -3,25 +3,16 @@
 "use strict";
 
 // The no-cpows-in-tests check isn't very smart, simply warning if it finds
 // a variable named `content`. For Chrome compatibility, the Omnibox API uses
 // that name for setting the text of a suggestion, and that's all this test uses
 // it for, so we can disable it for this test.
 /* eslint-disable mozilla/no-cpows-in-tests */
 
-function setup() {
-  const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
-  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
-
-  registerCleanupFunction(() => {
-    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
-  });
-}
-
 add_task(async function() {
   let keyword = "test";
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "omnibox": {
         "keyword": keyword,
       },
@@ -245,17 +236,16 @@ add_task(async function() {
 
     EventUtils.synthesizeMouseAtCenter(gURLBar.popup.richlistbox.children[0], {});
     await expectEvent("on-input-entered-fired", {
       text,
       disposition: "currentTab",
     });
   }
 
-  await setup();
   await extension.startup();
 
   await SimpleTest.promiseFocus(window);
 
   await testInputEvents();
 
   // Test the heuristic result with default suggestions.
   await testHeuristicResult("Generated extension", false /* setDefaultSuggestion */);
--- a/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
+++ b/browser/components/extensions/test/browser/browser_ext_webNavigation_urlbar_transitions.js
@@ -55,22 +55,23 @@ function addSearchEngine(basename) {
         reject();
       },
     });
   });
 }
 
 async function prepareSearchEngine() {
   let oldCurrentEngine = Services.search.currentEngine;
+  let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
   Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
   let engine = await addSearchEngine(TEST_ENGINE_BASENAME);
   Services.search.currentEngine = engine;
 
   registerCleanupFunction(async function() {
-    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+    Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
     Services.search.currentEngine = oldCurrentEngine;
 
     // Make sure the popup is closed for the next test.
     gURLBar.blur();
     gURLBar.popup.selectedIndex = -1;
     gURLBar.popup.hidePopup();
     ok(!gURLBar.popup.popupOpen, "popup should be closed");
 
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -413,17 +413,26 @@ These should match what Safari and other
 <!ENTITY urlbar.searchSuggestionsNotification.question "Would you like to improve your search experience with suggestions?">
 <!ENTITY urlbar.searchSuggestionsNotification.learnMore "Learn more…">
 <!ENTITY urlbar.searchSuggestionsNotification.learnMore.accesskey "l">
 <!ENTITY urlbar.searchSuggestionsNotification.disable "No">
 <!ENTITY urlbar.searchSuggestionsNotification.disable.accesskey "n">
 <!ENTITY urlbar.searchSuggestionsNotification.enable "Yes">
 <!ENTITY urlbar.searchSuggestionsNotification.enable.accesskey "y">
 
-<!-- 
+<!-- LOCALIZATION NOTE (urlbar.searchSuggestionsNotification.hintPrefix): Shown just before the suggestions opt-out hint. -->
+<!ENTITY urlbar.searchSuggestionsNotification.hintPrefix "Tip:">
+<!-- LOCALIZATION NOTE (urlbar.searchSuggestionsNotification.hint): &#x1F50E; is the magnifier icon emoji, please don't change it. -->
+<!ENTITY urlbar.searchSuggestionsNotification.hint "Get help finding things! Look for the &#x1F50E; next to search suggestions.">
+<!ENTITY urlbar.searchSuggestionsNotification.changeSettingsWin "Change Options…">
+<!ENTITY urlbar.searchSuggestionsNotification.changeSettingsWin.accesskey "C">
+<!ENTITY urlbar.searchSuggestionsNotification.changeSettingsUnix "Change Preferences…">
+<!ENTITY urlbar.searchSuggestionsNotification.changeSettingsUnix.accesskey "C">
+
+<!--
   Comment duplicated from browser-sets.inc:
 
   Search Command Key Logic works like this:
 
   Unix: Ctrl+J (0.8, 0.9 support)
         Ctrl+K (cross platform binding)
   Mac:  Cmd+K (cross platform binding)
         Cmd+Opt+F (platform convention)
--- a/browser/modules/test/browser/browser_UsageTelemetry_urlbar.js
+++ b/browser/modules/test/browser/browser_UsageTelemetry_urlbar.js
@@ -67,16 +67,17 @@ add_task(async function setup() {
   let engine = Services.search.getEngineByName("MozSearch");
   let originalEngine = Services.search.currentEngine;
   Services.search.currentEngine = engine;
 
   // And the first one-off engine.
   Services.search.moveEngine(engine, 0);
 
   // Enable search suggestions in the urlbar.
+  let suggestionsEnabled = Services.prefs.getBoolPref(SUGGEST_URLBAR_PREF);
   Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, true);
 
   // Enable the urlbar one-off buttons.
   Services.prefs.setBoolPref(ONEOFF_URLBAR_PREF, true);
 
   // Enable Extended Telemetry.
   await SpecialPowers.pushPrefEnv({"set": [["toolkit.telemetry.enabled", true]]});
 
@@ -91,17 +92,17 @@ add_task(async function setup() {
   // test when it selects results in the urlbar.
   await PlacesTestUtils.clearHistory();
 
   // Make sure to restore the engine once we're done.
   registerCleanupFunction(async function() {
     Services.telemetry.canRecordExtended = oldCanRecord;
     Services.search.currentEngine = originalEngine;
     Services.search.removeEngine(engine);
-    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+    Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, suggestionsEnabled);
     Services.prefs.clearUserPref(ONEOFF_URLBAR_PREF);
     await PlacesTestUtils.clearHistory();
     Services.telemetry.setEventRecordingEnabled("navigation", false);
   });
 });
 
 add_task(async function test_simpleQuery() {
   // Let's reset the counts.
--- a/browser/themes/shared/urlbarSearchSuggestionsNotification.inc.css
+++ b/browser/themes/shared/urlbarSearchSuggestionsNotification.inc.css
@@ -1,54 +1,163 @@
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] {
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] {
   border-bottom: 1px solid var(--panel-separator-color);
-  background-color: hsla(210, 4%, 10%, 0.07);
+  padding-inline-start: 0;
+  padding-inline-end: 6px;
+  min-height: 3em;
+}
+
+/* Opt-in notification */
+
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] > hbox[anonid="search-suggestions-opt-in"] {
   padding: 6px 0;
   padding-inline-start: 44px;
-  padding-inline-end: 6px;
+  background-color: hsla(210, 4%, 10%, 0.07);
   background-image: url("chrome://browser/skin/info.svg");
   background-clip: padding-box;
   background-position: 20px center;
   background-repeat: no-repeat;
   background-size: 16px 16px;
 }
 
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"]:-moz-locale-dir(rtl) {
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] > hbox[anonid="search-suggestions-opt-in"]:-moz-locale-dir(rtl) {
   background-position: right 20px center;
 }
 
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description {
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] > hbox[anonid="search-suggestions-opt-in"] > description {
   margin: 0;
   padding: 0;
 }
 
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description > label.text-link {
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] > hbox[anonid="search-suggestions-opt-in"] > description > label.text-link {
   margin-inline-start: 0;
 }
 
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button {
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] button {
   -moz-appearance: none;
   min-width: 80px;
   border-radius: 3px;
-  padding: 4px 16px;
   margin: 0;
   margin-inline-start: 10px;
 }
 
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button[anonid="search-suggestions-notification-disable"] {
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] button[anonid="search-suggestions-notification-disable"] {
   color: hsl(210, 0%, 38%);
   background-color: hsl(210, 0%, 88%);
   border: 1px solid hsl(210, 0%, 82%);
 }
-
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button[anonid="search-suggestions-notification-disable"]:hover {
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] button[anonid="search-suggestions-notification-disable"]:hover {
   background-color: hsl(210, 0%, 84%);
 }
 
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button[anonid="search-suggestions-notification-enable"] {
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] button[anonid="search-suggestions-notification-enable"] {
   color: white;
   background-color: hsl(93, 82%, 44%);
   border: 1px solid hsl(93, 82%, 44%);
 }
-
-#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button[anonid="search-suggestions-notification-enable"]:hover {
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] button[anonid="search-suggestions-notification-enable"]:hover {
   background-color: hsl(93, 82%, 40%);
 }
+
+/* Opt-out hint */
+
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] > hbox[anonid="search-suggestions-opt-out"] {
+  font: message-box;
+}
+
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] hbox[anonid="search-suggestions-hint-box"] > description {
+  margin: auto;
+  padding: 4px 8px;
+  background-color: #ffeebe;
+  border: 1px solid #ffdf81;
+  border-radius: 4px;
+  color: #7d3500;
+}
+
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] hbox[anonid="search-suggestions-hint-box"] > description > html|span {
+  unicode-bidi: embed;
+}
+
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"] hbox[anonid="search-suggestions-hint-box"] > description > html|span.prefix {
+  font-weight: bold;
+}
+
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"][animate] > hbox[anonid="search-suggestions-opt-out"] > .ac-site-icon {
+  transform: scale(0);
+  animation-name: search-suggestions-hint-grow;
+  animation-duration: 500ms;
+  animation-delay: 500ms;
+  animation-iteration-count: 1;
+  animation-timing-function: ease-in-out;
+  animation-fill-mode: forwards;
+}
+
+@keyframes search-suggestions-hint-grow {
+  0%   { transform: scale(0); }
+  40%  { transform: scale(1.5); }
+  60%  { transform: scale(1); }
+  80%  { transform: scale(1.25); }
+  100% { transform: scale(1); }
+}
+
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"][animate] hbox[anonid="search-suggestions-hint-typing"] > .ac-title-text {
+  text-overflow: clip;
+}
+
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"][animate] hbox[anonid="search-suggestions-hint-typing"] {
+  overflow: hidden;
+  max-width: 8ch;
+  width: 0;
+  animation-name: search-suggestions-hint-typing;
+  animation-duration: 500ms;
+  animation-delay: 750ms;
+  animation-iteration-count: 1;
+  animation-fill-mode: forwards;
+}
+
+@keyframes search-suggestions-hint-typing {
+  from { width: 0; }
+  to   { width: 8ch; }
+}
+
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"][animate] hbox[anonid="search-suggestions-hint-box"] {
+  opacity: 0;
+  animation-duration: 250ms;
+  animation-delay: 1500ms;
+  animation-iteration-count: 1;
+  animation-fill-mode: forwards;
+}
+
+/* Margin-inline-start can't be animated yet */
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"][animate] hbox[anonid="search-suggestions-hint-box"]:-moz-locale-dir(ltr) {
+  margin-left: 160px;
+  animation-name: search-suggestions-hint-buildin-ltr;
+}
+
+@keyframes search-suggestions-hint-buildin-ltr {
+  from  { margin-left: 160px; opacity: 0; }
+  to    { margin-left: 0; opacity: 1; }
+}
+
+#PopupAutoCompleteRichResult > deck[anonid="search-suggestions-notification"][animate] hbox[anonid="search-suggestions-hint-box"]:-moz-locale-dir(rtl) {
+  /* Should be margin-inline-start but that can't be animated yet */
+  margin-right: 160px;
+  animation-name: search-suggestions-hint-buildin-rtl;
+}
+
+@keyframes search-suggestions-hint-buildin-rtl {
+  from  { margin-right: 160px; opacity: 0; }
+  to    { margin-right: 0; opacity: 1; }
+}
+
+#search-suggestions-change-settings {
+  opacity: 0;
+  animation-name: search-suggestions-hint-fadein;
+  animation-duration: 500ms;
+  animation-delay: 1800ms;
+  animation-iteration-count: 1;
+  animation-fill-mode: forwards;
+}
+
+@keyframes search-suggestions-hint-fadein {
+  from  { opacity: 0 }
+  to    { opacity: 1 }
+}