Bug 959567 - Change urlbar search suggestions from opt-out to opt-in, add opt-in prompt to urlbar popup. r=mak
authorDrew Willcoxon <adw@mozilla.com>
Thu, 06 Aug 2015 20:13:00 -0700
changeset 256663 4e2489f7cc7d09bb279c62b34b5d56836f4a980a
parent 256662 4218f5a569ad33636d8ae2918ddbbbbd6e5f32ac
child 256664 2dac39ceb9903de46375abc09916df0e5b454a26
child 256686 91de9c6708006c45824bc972b576fb5532a8b9ff
push id14494
push userdwillcoxon@mozilla.com
push dateFri, 07 Aug 2015 03:13:11 +0000
treeherderfx-team@4e2489f7cc7d [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmak
bugs959567
milestone42.0a1
Bug 959567 - Change urlbar search suggestions from opt-out to opt-in, add opt-in prompt to urlbar popup. r=mak
browser/app/profile/firefox.js
browser/base/content/browser.css
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_urlbarSearchSuggestionsNotification.js
browser/base/content/urlbarBindings.xml
browser/locales/en-US/chrome/browser/browser.dtd
browser/themes/linux/browser.css
browser/themes/linux/jar.mn
browser/themes/osx/browser.css
browser/themes/osx/jar.mn
browser/themes/shared/info.svg
browser/themes/shared/urlbarSearchSuggestionsNotification.inc.css
browser/themes/windows/browser.css
browser/themes/windows/jar.mn
testing/marionette/client/marionette/tests/unit/unit-tests.ini
testing/profiles/prefs_general.js
toolkit/components/places/UnifiedComplete.js
toolkit/components/places/tests/unifiedcomplete/test_enabled.js
toolkit/content/widgets/autocomplete.xml
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -305,17 +305,18 @@ pref("browser.urlbar.restrict.searches",
 pref("browser.urlbar.match.title", "#");
 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",             true);
+pref("browser.urlbar.suggest.searches",             false);
+pref("browser.urlbar.userMadeSearchSuggestionsChoice", false);
 
 // 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
@@ -472,16 +472,37 @@ panel[noactions] > richlistbox > richlis
 searchbar[oneoffui] {
   -moz-binding: url("chrome://browser/content/search/search.xml#searchbar-flare") !important;
 }
 
 #PopupAutoCompleteRichResult {
   -moz-binding: url("chrome://browser/content/urlbarBindings.xml#urlbar-rich-result-popup");
 }
 
+#PopupAutoCompleteRichResult.showSearchSuggestionsNotification {
+  transition: height 100ms;
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] {
+  visibility: collapse;
+  transition: margin-top 100ms;
+}
+
+#PopupAutoCompleteRichResult.showSearchSuggestionsNotification > hbox[anonid="search-suggestions-notification"] {
+  visibility: visible;
+}
+
+#PopupAutoCompleteRichResult > richlistbox {
+  transition: height 100ms;
+}
+
+#PopupAutoCompleteRichResult.showSearchSuggestionsNotification > richlistbox {
+  transition: none;
+}
+
 #urlbar[pageproxystate="invalid"] > #urlbar-icons > .urlbar-icon,
 #urlbar[pageproxystate="invalid"][focused="true"] > #urlbar-go-button ~ toolbarbutton,
 #urlbar[pageproxystate="valid"] > #urlbar-go-button,
 #urlbar:not([focused="true"]) > #urlbar-go-button {
   visibility: collapse;
 }
 
 #urlbar[pageproxystate="invalid"] > #identity-box > #identity-icon-labels {
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -453,16 +453,17 @@ skip-if = e10s # Bug 1100700 - test reli
 [browser_urlbarAutoFillTrimURLs.js]
 [browser_urlbarCopying.js]
 [browser_urlbarDelete.js]
 [browser_urlbarEnter.js]
 [browser_urlbarEnterAfterMouseOver.js]
 skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
 [browser_urlbarRevert.js]
 [browser_urlbarSearchSingleWordNotification.js]
+[browser_urlbarSearchSuggestionsNotification.js]
 [browser_urlbarStop.js]
 [browser_urlbarTrimURLs.js]
 [browser_urlbar_autoFill_backspaced.js]
 [browser_urlbar_search_healthreport.js]
 [browser_urlbar_searchsettings.js]
 [browser_utilityOverlay.js]
 [browser_visibleFindSelection.js]
 [browser_visibleLabel.js]
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_urlbarSearchSuggestionsNotification.js
@@ -0,0 +1,192 @@
+const SUGGEST_ALL_PREF = "browser.search.suggest.enabled";
+const SUGGEST_URLBAR_PREF = "browser.urlbar.suggest.searches";
+const CHOICE_PREF = "browser.urlbar.userMadeSearchSuggestionsChoice";
+const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+
+// Must run first.
+add_task(function* prepare() {
+  let engine = yield promiseNewEngine(TEST_ENGINE_BASENAME);
+  let oldCurrentEngine = Services.search.currentEngine;
+  Services.search.currentEngine = engine;
+  registerCleanupFunction(function () {
+    Services.search.currentEngine = oldCurrentEngine;
+    Services.prefs.clearUserPref(SUGGEST_ALL_PREF);
+    Services.prefs.clearUserPref(SUGGEST_URLBAR_PREF);
+
+    // Disable the notification for future tests so it doesn't interfere with
+    // them.  clearUserPref() won't work because by default the pref is false.
+    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(function* focus_allSuggestionsDisabled() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, false);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+  yield promiseAutocompleteResultPopup("foo");
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(false);
+});
+
+add_task(function* focus_noChoiceMade() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(true);
+  gURLBar.blur();
+  Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+  gURLBar.focus();
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open again");
+  assertVisible(true);
+});
+
+add_task(function* dismissWithoutResults() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(true);
+  Assert.equal(gURLBar.popup._matchCount, 0, "popup should have no results");
+  let disableButton = document.getAnonymousElementByAttribute(
+    gURLBar.popup, "anonid", "search-suggestions-notification-disable"
+  );
+  let transitionPromise = promiseTransition();
+  disableButton.click();
+  yield transitionPromise;
+  Assert.ok(!gURLBar.popup.popupOpen, "popup should be closed");
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(!gURLBar.popup.popupOpen, "popup should remain closed");
+  yield promiseAutocompleteResultPopup("foo");
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(false);
+});
+
+add_task(function* dismissWithResults() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(true);
+  yield promiseAutocompleteResultPopup("foo");
+  Assert.ok(gURLBar.popup._matchCount > 0, "popup should have results");
+  let disableButton = document.getAnonymousElementByAttribute(
+    gURLBar.popup, "anonid", "search-suggestions-notification-disable"
+  );
+  let transitionPromise = promiseTransition();
+  disableButton.click();
+  yield transitionPromise;
+  Assert.ok(gURLBar.popup.popupOpen, "popup should remain open");
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(!gURLBar.popup.popupOpen, "popup should remain closed");
+  yield promiseAutocompleteResultPopup("foo");
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(false);
+});
+
+add_task(function* disable() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  gURLBar.blur();
+  gURLBar.focus();
+  Assert.ok(gURLBar.popup.popupOpen, "popup should be open");
+  assertVisible(true);
+  let disableButton = document.getAnonymousElementByAttribute(
+    gURLBar.popup, "anonid", "search-suggestions-notification-disable"
+  );
+  let transitionPromise = promiseTransition();
+  disableButton.click();
+  yield transitionPromise;
+  gURLBar.blur();
+  yield promiseAutocompleteResultPopup("foo");
+  assertSuggestionsPresent(false);
+});
+
+add_task(function* enable() {
+  Services.prefs.setBoolPref(SUGGEST_ALL_PREF, true);
+  Services.prefs.setBoolPref(SUGGEST_URLBAR_PREF, false);
+  Services.prefs.setBoolPref(CHOICE_PREF, false);
+  gURLBar.blur();
+  gURLBar.focus();
+  yield promiseAutocompleteResultPopup("foo");
+  assertVisible(true);
+  assertSuggestionsPresent(false);
+  let enableButton = document.getAnonymousElementByAttribute(
+    gURLBar.popup, "anonid", "search-suggestions-notification-enable"
+  );
+  let searchPromise = promiseSearchComplete();
+  enableButton.click();
+  yield searchPromise;
+  // Clicking Yes should trigger a new search so that suggestions appear
+  // immediately.
+  assertSuggestionsPresent(true);
+  gURLBar.blur();
+  gURLBar.focus();
+  // Suggestions should still be present in a new search of course.
+  yield promiseAutocompleteResultPopup("bar");
+  assertSuggestionsPresent(true);
+});
+
+function assertSuggestionsPresent(expectedPresent) {
+  let controller = gURLBar.popup.input.controller;
+  let matchCount = controller.matchCount;
+  let actualPresent = false;
+  for (let i = 0; i < matchCount; i++) {
+    let url = controller.getValueAt(i);
+    let [, type, paramStr] = url.match(/^moz-action:([^,]+),(.*)$/);
+    let params = {};
+    try {
+      params = JSON.parse(paramStr);
+    } catch (err) {}
+    let isSuggestion = type == "searchengine" && "searchSuggestion" in params;
+    actualPresent = actualPresent || isSuggestion;
+  }
+  Assert.equal(actualPresent, expectedPresent);
+}
+
+function assertVisible(visible) {
+  let style =
+    window.getComputedStyle(gURLBar.popup.searchSuggestionsNotification);
+  Assert.equal(style.visibility, visible ? "visible" : "collapse");
+}
+
+function promiseNewEngine(basename) {
+  return new Promise((resolve, reject) => {
+    info("Waiting for engine to be added: " + basename);
+    let url = getRootDirectory(gTestPath) + basename;
+    Services.search.addEngine(url, Ci.nsISearchEngine.TYPE_MOZSEARCH, "",
+                              false, {
+      onSuccess: function (engine) {
+        info("Search engine added: " + basename);
+        registerCleanupFunction(() => Services.search.removeEngine(engine));
+        resolve(engine);
+      },
+      onError: function (errCode) {
+        Assert.ok(false, "addEngine failed with error code " + errCode);
+        reject();
+      },
+    });
+  });
+}
+
+function promiseTransition() {
+  return new Promise(resolve => {
+    gURLBar.popup.addEventListener("transitionend", function onEnd() {
+      gURLBar.popup.removeEventListener("transitionend", onEnd, true);
+      // The urlbar needs to handle the transitionend first, but that happens
+      // naturally since promises are resolved at the end of the current tick.
+      resolve();
+    }, true);
+  });
+}
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -61,16 +61,18 @@ file, You can obtain one at http://mozil
 
         this._prefs.addObserver("", this, false);
         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._userMadeSearchSuggestionsChoice =
+          this._prefs.getBoolPref("userMadeSearchSuggestionsChoice");
         this._ignoreNextSelect = false;
 
         this.inputField.controllers.insertControllerAt(0, this._copyCutController);
         this.inputField.addEventListener("paste", this, false);
         this.inputField.addEventListener("mousedown", this, false);
         this.inputField.addEventListener("mousemove", this, false);
         this.inputField.addEventListener("mouseout", this, false);
         this.inputField.addEventListener("overflow", this, false);
@@ -655,16 +657,20 @@ file, You can obtain one at http://mozil
                 this.completeDefaultIndex = this._prefs.getBoolPref(aData);
                 break;
               case "delay":
                 this.timeout = this._prefs.getIntPref(aData);
                 break;
               case "formatting.enabled":
                 this._formattingEnabled = this._prefs.getBoolPref(aData);
                 break;
+              case "userMadeSearchSuggestionsChoice":
+                this._userMadeSearchSuggestionsChoice =
+                  this._prefs.getBoolPref(aData);
+                break;
               case "trimURLs":
                 this._mayTrimURLs = this._prefs.getBoolPref(aData);
                 break;
               case "unifiedcomplete":
                 let useUnifiedComplete = false;
                 try {
                   useUnifiedComplete = this._prefs.getBoolPref(aData);
                 } catch (ex) {}
@@ -908,16 +914,35 @@ file, You can obtain one at http://mozil
           if (Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete") &&
               this.popup.selectedIndex == 0) {
             return this.mController.handleText();
           }
           return this.mController.handleDelete();
         ]]></body>
       </method>
 
+      <field name="_userMadeSearchSuggestionsChoice"><![CDATA[
+        false
+      ]]></field>
+
+      <method name="_maybeShowSearchSuggestionsNotification">
+        <body><![CDATA[
+          let showNotification =
+            !this._userMadeSearchSuggestionsChoice &&
+            // 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.getBoolPref("unifiedcomplete");
+          if (showNotification) {
+            this.popup.showSearchSuggestionsNotification(this, this);
+          }
+        ]]></body>
+      </method>
+
     </implementation>
 
     <handlers>
       <handler event="keydown"><![CDATA[
         if ((event.keyCode === KeyEvent.DOM_VK_ALT ||
              event.keyCode === KeyEvent.DOM_VK_SHIFT) &&
             this.popup.selectedIndex >= 0 &&
             !this._noActionsKeys.has(event.keyCode)) {
@@ -938,16 +963,17 @@ file, You can obtain one at http://mozil
             this._clearNoActions();
         }
       ]]></handler>
 
       <handler event="focus"><![CDATA[
         if (event.originalTarget == this.inputField) {
           this._hideURLTooltip();
           this.formatValue();
+          this._maybeShowSearchSuggestionsNotification();
         }
       ]]></handler>
 
       <handler event="blur"><![CDATA[
         if (event.originalTarget == this.inputField) {
           this._clearNoActions();
           this.formatValue();
         }
@@ -1469,25 +1495,81 @@ file, You can obtain one at http://mozil
   <binding id="addengine-icon" extends="xul:box">
     <content>
       <xul:image class="addengine-icon" xbl:inherits="src"/>
       <xul:image class="addengine-badge"/>
     </content>
   </binding>
 
   <binding id="urlbar-rich-result-popup" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-rich-result-popup">
+
+    <content ignorekeys="true" level="top" consumeoutsideclicks="never">
+      <xul:hbox anonid="search-suggestions-notification" align="center">
+        <xul:description flex="1">
+          &urlbar.searchSuggestionsNotification.question;
+          <xul:label anonid="search-suggestions-notification-learn-more"
+                     class="text-link"
+                     value="&urlbar.searchSuggestionsNotification.learnMore;"/>
+        </xul:description>
+        <xul:button anonid="search-suggestions-notification-disable"
+                    label="&urlbar.searchSuggestionsNotification.disable;"
+                    onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(false);"/>
+        <xul:button anonid="search-suggestions-notification-enable"
+                    label="&urlbar.searchSuggestionsNotification.enable;"
+                    onclick="document.getBindingParent(this).dismissSearchSuggestionsNotification(true);"/>
+      </xul:hbox>
+      <xul:richlistbox anonid="richlistbox" class="autocomplete-richlistbox"
+                       flex="1"/>
+      <xul:hbox anonid="footer">
+        <children/>
+      </xul:hbox>
+    </content>
+
     <implementation>
       <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>
 
+      <field name="searchSuggestionsNotification" readonly="true">
+        document.getAnonymousElementByAttribute(
+          this, "anonid", "search-suggestions-notification"
+        );
+      </field>
+
+      <field name="searchSuggestionsNotificationLearnMoreLink" readonly="true">
+        document.getAnonymousElementByAttribute(
+          this, "anonid", "search-suggestions-notification-learn-more"
+        );
+      </field>
+
+      <field name="footer" readonly="true">
+        document.getAnonymousElementByAttribute(this, "anonid", "footer");
+      </field>
+
+      <method name="dismissSearchSuggestionsNotification">
+        <parameter name="enableSuggestions"/>
+        <body><![CDATA[
+          Services.prefs.setBoolPref(
+            "browser.urlbar.userMadeSearchSuggestionsChoice", true
+          );
+          Services.prefs.setBoolPref(
+            "browser.urlbar.suggest.searches", enableSuggestions
+          );
+          this._hideSearchSuggestionsNotification(true);
+          if (enableSuggestions && this.input.textValue) {
+            // Start a new search so that suggestions appear immediately.
+            this.input.controller.startSearch(this.input.textValue);
+          }
+        ]]></body>
+      </method>
+
       <!-- Override this so that when UnifiedComplete is enabled, navigating
            between items results in an item always being selected. This is
            contrary to the old behaviour (UnifiedComplete disabled) where
            if you navigate beyond either end of the list, no item will be
            selected. -->
       <method name="getNextIndex">
         <parameter name="reverse"/>
         <parameter name="amount"/>
@@ -1544,19 +1626,112 @@ 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;
 
-          // this method is defined on the base binding
+          // The popup may already be open if it's showing the search
+          // suggestions notification.  In that case, its footer visibility
+          // needs to be updated.
+          if (this.popupOpen) {
+            this._updateFooterVisibility();
+          }
+
           this._openAutocompletePopup(aInput, aElement);
-        ]]></body>
+          ]]>
+        </body>
+      </method>
+
+      <method name="_updateFooterVisibility">
+        <body>
+          <![CDATA[
+          this.footer.collapsed = this._matchCount == 0;
+          ]]>
+        </body>
+      </method>
+
+      <method name="showSearchSuggestionsNotification">
+        <parameter name="aInput"/>
+        <parameter name="aElement"/>
+        <body>
+          <![CDATA[
+          // Set the learn-more link href.
+          let link = this.searchSuggestionsNotificationLearnMoreLink;
+          if (!link.hasAttribute("href")) {
+            let url = Services.urlFormatter.formatURL(
+              Services.prefs.getCharPref("app.support.baseURL") + "suggestions"
+            );
+            link.setAttribute("href", url);
+          }
+
+          // 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
+          // around that problem by disabling the listbox animation.
+          this.richlistbox.flex = 0;
+          this.setAttribute("dontanimate", "true");
+
+          this.classList.add("showSearchSuggestionsNotification");
+          this._updateFooterVisibility();
+          this.openAutocompletePopup(aInput, aElement);
+          ]]>
+        </body>
+      </method>
+
+      <method name="_hideSearchSuggestionsNotification">
+        <parameter name="animate"/>
+        <body>
+          <![CDATA[
+          if (animate) {
+            this._hideSearchSuggestionsNotificationWithAnimation();
+            return;
+          }
+          this.classList.remove("showSearchSuggestionsNotification");
+          this.richlistbox.flex = 1;
+          this.removeAttribute("dontanimate");
+          if (this._matchCount) {
+            // Update popup height.
+            this._invalidate();
+          } else {
+            this.closePopup();
+          }
+          ]]>
+        </body>
+      </method>
+
+      <method name="_hideSearchSuggestionsNotificationWithAnimation">
+        <body>
+          <![CDATA[
+          let notificationHeight = this.searchSuggestionsNotification
+                                       .getBoundingClientRect()
+                                       .height;
+          this.searchSuggestionsNotification.style.marginTop =
+            "-" + notificationHeight + "px";
+
+          let popupHeightPx =
+            (this.getBoundingClientRect().height - notificationHeight) + "px";
+          this.style.height = popupHeightPx;
+
+          let onTransitionEnd = () => {
+            this.removeEventListener("transitionend", onTransitionEnd, true);
+            this.searchSuggestionsNotification.style.marginTop = "0px";
+            this.style.removeProperty("height");
+            this._hideSearchSuggestionsNotification(false);
+          };
+          this.addEventListener("transitionend", onTransitionEnd, true);
+          ]]>
+        </body>
       </method>
 
       <method name="onPopupClick">
         <parameter name="aEvent"/>
         <body>
           <![CDATA[
           // Ignore right-clicks
           if (aEvent.button == 2)
@@ -1664,16 +1839,25 @@ file, You can obtain one at http://mozil
         // When the user selects one of matches, stop the search to avoid
         // changing the underlying result unexpectedly.
         if (!this._ignoreNextSelect && this.selectedIndex >= 0) {
           let controller = this.view.QueryInterface(Components.interfaces.nsIAutoCompleteController);
           controller.stopSearch();
         }
       ]]></handler>
 
+      <handler event="mousedown"><![CDATA[
+        // Required to make the xul:label.text-link elements in the search
+        // suggestions notification work correctly when clicked on Linux.
+        // This is copied from the mousedown handler in
+        // browser-search-autocomplete-result-popup, which apparently had a
+        // similar problem.
+        event.preventDefault();
+      ]]></handler>
+
     </handlers>
   </binding>
 
   <binding id="addon-progress-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
     <implementation>
       <constructor><![CDATA[
         if (!this.notification)
           return;
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -390,16 +390,21 @@ These should match what Safari and other
 <!ENTITY customizeMenu.addMoreItems.label "Add More Items…">
 <!ENTITY customizeMenu.addMoreItems.accesskey "A">
 
 <!ENTITY openCmd.commandkey           "l">
 <!ENTITY urlbar.placeholder2          "Search or enter address">
 <!ENTITY urlbar.accesskey             "d">
 <!ENTITY urlbar.switchToTab.label     "Switch to tab:">
 
+<!ENTITY urlbar.searchSuggestionsNotification.question "Would you like to improve your search experience with suggestions?">
+<!ENTITY urlbar.searchSuggestionsNotification.learnMore "Learn more…">
+<!ENTITY urlbar.searchSuggestionsNotification.disable "No">
+<!ENTITY urlbar.searchSuggestionsNotification.enable "Yes">
+
 <!-- 
   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)
--- a/browser/themes/linux/browser.css
+++ b/browser/themes/linux/browser.css
@@ -957,19 +957,17 @@ toolbarbutton[constrain-size="true"][cui
 
 .urlbar-display {
   margin-top: 0;
   margin-bottom: 0;
   -moz-margin-start: 0;
   color: GrayText;
 }
 
-#PopupAutoCompleteRichResult > richlistbox {
-  transition: height 100ms;
-}
+%include ../shared/urlbarSearchSuggestionsNotification.inc.css
 
 #search-container {
   min-width: calc(54px + 11ch);
 }
 
 /* identity box */
 
 #identity-box:-moz-locale-dir(ltr) {
@@ -1131,17 +1129,19 @@ richlistitem[type~="action"][actiontype=
 
 .ac-result-type-tag,
 .autocomplete-treebody::-moz-tree-image(tag, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/tag.png");
   width: 16px;
   height: 16px;
 }
 
-.ac-comment {
+.ac-comment,
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description,
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button {
   font-size: 1.05em;
 }
 
 .ac-extra > .ac-comment {
   font-size: inherit;
 }
 
 .ac-url-text,
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -91,16 +91,17 @@ browser.jar:
   skin/classic/browser/search-pref.png                      (../shared/search/search-pref.png)
   skin/classic/browser/search-indicator.png                 (../shared/search/search-indicator.png)
   skin/classic/browser/search-engine-placeholder.png        (../shared/search/search-engine-placeholder.png)
   skin/classic/browser/badge-add-engine.png                 (../shared/search/badge-add-engine.png)
   skin/classic/browser/search-indicator-badge-add.png       (../shared/search/search-indicator-badge-add.png)
   skin/classic/browser/search-history-icon.svg              (../shared/search/history-icon.svg)
   skin/classic/browser/search-indicator-magnifying-glass.svg   (../shared/search/search-indicator-magnifying-glass.svg)
   skin/classic/browser/search-arrow-go.svg                  (../shared/search/search-arrow-go.svg)
+  skin/classic/browser/info.svg                             (../shared/info.svg)
   skin/classic/browser/Security-broken.png
   skin/classic/browser/setDesktopBackground.css
   skin/classic/browser/slowStartup-16.png
   skin/classic/browser/theme-switcher-icon.png              (../shared/theme-switcher-icon.png)
   skin/classic/browser/Toolbar.png
   skin/classic/browser/Toolbar-inverted.png
   skin/classic/browser/Toolbar-small.png
   skin/classic/browser/undoCloseTab.png                        (../shared/undoCloseTab.png)
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -1787,24 +1787,22 @@ toolbarbutton[constrain-size="true"][cui
 }
 
 .urlbar-display {
   margin-top: 0;
   margin-bottom: 0;
   color: GrayText;
 }
 
-#PopupAutoCompleteRichResult > richlistbox {
-  transition: height 100ms;
-}
-
 #PopupAutoCompleteRichResult {
   margin-top: 2px;
 }
 
+%include ../shared/urlbarSearchSuggestionsNotification.inc.css
+
 /* ----- AUTOCOMPLETE ----- */
 
 #treecolAutoCompleteImage {
   max-width: 36px;
 }
 
 .ac-result-type-bookmark,
 .autocomplete-treebody::-moz-tree-image(bookmark, treecolAutoCompleteImage) {
@@ -1840,18 +1838,18 @@ richlistitem[selected="true"][current="t
 }
 
 .ac-extra > .ac-comment {
   font-size: inherit;
 }
 
 .ac-url-text,
 .ac-action-text {
+  font: message-box;
   color: -moz-nativehyperlinktext;
-  font: message-box;
 }
 
 richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-icon {
   list-style-image: url("chrome://browser/skin/actionicon-tab.png");
   -moz-image-region: rect(0, 16px, 11px, 0);
   padding: 0 3px;
 }
 
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -122,16 +122,17 @@ browser.jar:
   skin/classic/browser/search-engine-placeholder@2x.png        (../shared/search/search-engine-placeholder@2x.png)
   skin/classic/browser/badge-add-engine.png                    (../shared/search/badge-add-engine.png)
   skin/classic/browser/badge-add-engine@2x.png                 (../shared/search/badge-add-engine@2x.png)
   skin/classic/browser/search-indicator-badge-add.png          (../shared/search/search-indicator-badge-add.png)
   skin/classic/browser/search-indicator-badge-add@2x.png       (../shared/search/search-indicator-badge-add@2x.png)
   skin/classic/browser/search-history-icon.svg                 (../shared/search/history-icon.svg)
   skin/classic/browser/search-indicator-magnifying-glass.svg   (../shared/search/search-indicator-magnifying-glass.svg)
   skin/classic/browser/search-arrow-go.svg                     (../shared/search/search-arrow-go.svg)
+  skin/classic/browser/info.svg                                (../shared/info.svg)
   skin/classic/browser/slowStartup-16.png
   skin/classic/browser/theme-switcher-icon.png                 (../shared/theme-switcher-icon.png)
   skin/classic/browser/theme-switcher-icon@2x.png              (../shared/theme-switcher-icon@2x.png)
   skin/classic/browser/Toolbar.png
   skin/classic/browser/Toolbar@2x.png
   skin/classic/browser/Toolbar-inverted.png
   skin/classic/browser/Toolbar-inverted@2x.png
   skin/classic/browser/toolbarbutton-dropmarker.png
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/info.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
+  <circle fill="#00a1f2" cx="8" cy="8" r="8" />
+  <circle fill="#fff" cx="8" cy="4" r="1.25" />
+  <rect x="7" y="7" width="2" height="6" rx="1" ry="1" fill="#fff" />
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/urlbarSearchSuggestionsNotification.inc.css
@@ -0,0 +1,55 @@
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] {
+  border-bottom: 1px solid hsla(210, 4%, 10%, 0.14);
+  background-color: hsla(210, 4%, 10%, 0.07);
+  padding: 6px 0;
+  -moz-padding-start: 44px;
+  -moz-padding-end: 6px;
+  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) {
+  background-position: right 20px center;
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description {
+  margin: 0;
+  padding: 0;
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description > label.text-link {
+  -moz-margin-start: 0;
+}
+
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button {
+  -moz-appearance: none;
+  -moz-user-focus: ignore;
+  min-width: 80px;
+  border-radius: 3px;
+  padding: 4px 16px;
+  margin: 0;
+  -moz-margin-start: 10px;
+}
+
+#PopupAutoCompleteRichResult > hbox[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 {
+  background-color: hsl(210, 0%, 84%);
+}
+
+#PopupAutoCompleteRichResult > hbox[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 {
+  background-color: hsl(93, 82%, 40%);
+}
--- a/browser/themes/windows/browser.css
+++ b/browser/themes/windows/browser.css
@@ -1424,19 +1424,17 @@ html|*.urlbar-input:-moz-lwtheme::-moz-p
 
 .urlbar-display {
   margin-top: 0;
   margin-bottom: 0;
   -moz-margin-start: 0;
   color: GrayText;
 }
 
-#PopupAutoCompleteRichResult > richlistbox {
-  transition: height 100ms;
-}
+%include ../shared/urlbarSearchSuggestionsNotification.inc.css
 
 #search-container {
   min-width: calc(54px + 11ch);
 }
 
 /* identity box */
 
 #identity-box:-moz-locale-dir(ltr) {
@@ -1559,27 +1557,30 @@ richlistitem[type~="action"][actiontype=
 
 .ac-result-type-tag,
 .autocomplete-treebody::-moz-tree-image(tag, treecolAutoCompleteImage) {
   list-style-image: url("chrome://browser/skin/places/tag.png");
   width: 16px;
   height: 16px;
 }
 
-.ac-comment {
+.ac-comment,
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > description,
+#PopupAutoCompleteRichResult > hbox[anonid="search-suggestions-notification"] > button {
   font-size: 1.06em;
 }
 
-.ac-extra > .ac-comment {
+.ac-extra > .ac-comment,
+.ac-url-text,
+.ac-action-text {
   font-size: 1em;
 }
 
 .ac-url-text,
 .ac-action-text {
-  font-size: 1em;
   color: -moz-nativehyperlinktext;
 }
 
 @media (-moz-os-version: windows-xp) and (-moz-windows-default-theme) {
   .ac-url-text:not([selected="true"]),
   .ac-action-text:not([selected="true"]) {
     color: #008800;
   }
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -119,16 +119,17 @@ browser.jar:
         skin/classic/browser/search-engine-placeholder@2x.png        (../shared/search/search-engine-placeholder@2x.png)
         skin/classic/browser/badge-add-engine.png                    (../shared/search/badge-add-engine.png)
         skin/classic/browser/badge-add-engine@2x.png                 (../shared/search/badge-add-engine@2x.png)
         skin/classic/browser/search-indicator-badge-add.png          (../shared/search/search-indicator-badge-add.png)
         skin/classic/browser/search-indicator-badge-add@2x.png       (../shared/search/search-indicator-badge-add@2x.png)
         skin/classic/browser/search-history-icon.svg                 (../shared/search/history-icon.svg)
         skin/classic/browser/search-indicator-magnifying-glass.svg   (../shared/search/search-indicator-magnifying-glass.svg)
         skin/classic/browser/search-arrow-go.svg                     (../shared/search/search-arrow-go.svg)
+        skin/classic/browser/info.svg                                (../shared/info.svg)
         skin/classic/browser/setDesktopBackground.css
         skin/classic/browser/slowStartup-16.png
         skin/classic/browser/theme-switcher-icon.png                 (../shared/theme-switcher-icon.png)
         skin/classic/browser/Toolbar.png
         skin/classic/browser/Toolbar@2x.png
         skin/classic/browser/Toolbar-aero.png
         skin/classic/browser/Toolbar-aero@2x.png
         skin/classic/browser/Toolbar-inverted.png
--- a/testing/marionette/client/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/client/marionette/tests/unit/unit-tests.ini
@@ -142,16 +142,17 @@ browser = false
 b2g = false
 [test_set_window_size.py]
 b2g = false
 skip-if = os == "linux" # Bug 1085717
 [test_with_using_context.py]
 
 [test_modal_dialogs.py]
 b2g = false
+skip-if = true # Disabled so bug 959567 can land
 [test_key_actions.py]
 [test_mouse_action.py]
 b2g = false
 [test_teardown_context_preserved.py]
 b2g = false
 [test_file_upload.py]
 b2g = false
 skip-if = os == "win" # http://bugs.python.org/issue14574
--- a/testing/profiles/prefs_general.js
+++ b/testing/profiles/prefs_general.js
@@ -330,9 +330,13 @@ user_pref("dom.serviceWorkers.periodic-u
 
 // Enable speech synth test service, and disable built in platform services.
 user_pref("media.webspeech.synth.test", true);
 
 // Turn off search suggestions in the location bar so as not to trigger network
 // connections.
 user_pref("browser.urlbar.suggest.searches", false);
 
+// Turn off the location bar search suggestions opt-in.  It interferes with
+// tests that don't expect it to be there.
+user_pref("browser.urlbar.userMadeSearchSuggestionsChoice", true);
+
 user_pref("dom.audiochannel.mutedByDefault", false);
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -31,17 +31,17 @@ const PREF_RESTRICT_SWITCHTAB =     [ "r
 const PREF_RESTRICT_SEARCHES =      [ "restrict.searces",       "$" ];
 const PREF_MATCH_TITLE =            [ "match.title",            "#" ];
 const PREF_MATCH_URL =              [ "match.url",              "@" ];
 
 const PREF_SUGGEST_HISTORY =        [ "suggest.history",        true ];
 const PREF_SUGGEST_BOOKMARK =       [ "suggest.bookmark",       true ];
 const PREF_SUGGEST_OPENPAGE =       [ "suggest.openpage",       true ];
 const PREF_SUGGEST_HISTORY_ONLYTYPED = [ "suggest.history.onlyTyped", false ];
-const PREF_SUGGEST_SEARCHES =       [ "suggest.searches",       true ];
+const PREF_SUGGEST_SEARCHES =       [ "suggest.searches",       false ];
 
 const PREF_MAX_CHARS_FOR_SUGGEST =  [ "maxCharsForSearchSuggestions", 20];
 
 // Match type constants.
 // These indicate what type of search function we should be using.
 const MATCH_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE;
 const MATCH_BOUNDARY_ANYWHERE = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY_ANYWHERE;
 const MATCH_BOUNDARY = Ci.mozIPlacesAutoComplete.MATCH_BOUNDARY;
--- a/toolkit/components/places/tests/unifiedcomplete/test_enabled.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_enabled.js
@@ -54,11 +54,15 @@ add_task(function* test_sync_enabled() {
   }
   Assert.equal(Services.prefs.getBoolPref("browser.urlbar.autocomplete.enabled"), true);
 
   // Disable autocoplete again, then re-enable it and check suggest prefs
   // have been reset.
   Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", false);
   Services.prefs.setBoolPref("browser.urlbar.autocomplete.enabled", true);
   for (let type of types.filter(t => t != "history")) {
-    Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), true);
+    if (type == "searches") {
+      Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), false);
+    } else {
+      Assert.equal(Services.prefs.getBoolPref("browser.urlbar.suggest." + type), true);
+    }
   }
 });
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1073,35 +1073,27 @@ extends="chrome://global/content/binding
           this._invalidate();
           ]]>
         </body>
       </method>
 
       <method name="_invalidate">
         <body>
           <![CDATA[
-          // To get a fixed height for the popup, instead of the default
-          // behavior that grows and shrinks it on result change, a consumer
-          // can set the height attribute. In such a case, instead of adjusting
-          // the richlistbox height, we just need to collapse unused items.
-          if (!this.hasAttribute("height")) {
-            // collapsed if no matches
-            this.richlistbox.collapsed = (this._matchCount == 0);
+          // collapsed if no matches
+          this.richlistbox.collapsed = (this._matchCount == 0);
 
-            // Update the richlistbox height.
-            if (this._adjustHeightTimeout) {
-              clearTimeout(this._adjustHeightTimeout);
-            }
-            if (this._shrinkTimeout) {
-              clearTimeout(this._shrinkTimeout);
-            }
-            this._adjustHeightTimeout = setTimeout(() => this.adjustHeight(), 0);
-          } else {
-            this._collapseUnusedItems();
+          // Update the richlistbox height.
+          if (this._adjustHeightTimeout) {
+            clearTimeout(this._adjustHeightTimeout);
           }
+          if (this._shrinkTimeout) {
+            clearTimeout(this._shrinkTimeout);
+          }
+          this._adjustHeightTimeout = setTimeout(() => this.adjustHeight(), 0);
 
           this._currentIndex = 0;
           if (this._appendResultTimeout) {
             clearTimeout(this._appendResultTimeout);
           }
           this._appendCurrentResult();
         ]]>
         </body>
@@ -1138,48 +1130,59 @@ extends="chrome://global/content/binding
 
       <method name="adjustHeight">
         <body>
           <![CDATA[
           // Figure out how many rows to show
           let rows = this.richlistbox.childNodes;
           let numRows = Math.min(this._matchCount, this.maxRows, rows.length);
 
+          this.removeAttribute("height");
+
           // Default the height to 0 if we have no rows to show
           let height = 0;
           if (numRows) {
             if (!this._rowHeight) {
               let firstRowRect = rows[0].getBoundingClientRect();
               this._rowHeight = firstRowRect.height;
-              this._rlbAnimated = !!window.getComputedStyle(this.richlistbox).transitionProperty;
+
+              let transition =
+                window.getComputedStyle(this.richlistbox).transitionProperty;
+              this._rlbAnimated = transition && transition != "none";
 
               // Set a fixed max-height to avoid flicker when growing the panel.
               this.richlistbox.style.maxHeight = (this._rowHeight * this.maxRows) + "px";
             }
 
             // Calculate the height to have the first row to last row shown
             height = this._rowHeight * numRows;
           }
 
+          let animate = this._rlbAnimated &&
+                        this.getAttribute("dontanimate") != "true";
           let currentHeight = this.richlistbox.getBoundingClientRect().height;
           if (height > currentHeight) {
             // Grow immediately.
-            this.richlistbox.style.height = height + "px";
+            if (animate) {
+              this.richlistbox.removeAttribute("height");
+              this.richlistbox.style.height = height + "px";
+            } else {
+              this.richlistbox.style.removeProperty("height");
+              this.richlistbox.height = height;
+            }
           } else {
             // Delay shrinking to avoid flicker.
             this._shrinkTimeout = setTimeout(() => {
-              this.richlistbox.style.height = height + "px";
-              if (this._rlbAnimated) {
-                let onTransitionEnd = () => {
-                  this.removeEventListener("transitionend", onTransitionEnd, true);
-                  this._collapseUnusedItems();
-                };
-                this.addEventListener("transitionend", onTransitionEnd, true);
+              this._collapseUnusedItems();
+              if (animate) {
+                this.richlistbox.removeAttribute("height");
+                this.richlistbox.style.height = height + "px";
               } else {
-                this._collapseUnusedItems();
+                this.richlistbox.style.removeProperty("height");
+                this.richlistbox.height = height;
               }
             }, this.mInput.shrinkDelay);
           }
           ]]>
         </body>
       </method>
 
       <method name="_getImageURLForResolution">