Merge m-c to b2ginbound, a=merge
authorWes Kocher <wkocher@mozilla.com>
Mon, 20 Jul 2015 16:37:20 -0700
changeset 253775 75c1398494d586c33f21f0e74131f32ea977259e
parent 253774 0ab3fa748d651b8f973de132bfb6cc0b00b42906 (current diff)
parent 253765 3a4bfa5d2d026f7d3fbfd0f87663b87b5caa9344 (diff)
child 253776 5c8399bc9a32bc88882d3ba6d48c1aabb078945b
push id29079
push usercbook@mozilla.com
push dateTue, 21 Jul 2015 14:50:24 +0000
treeherdermozilla-central@27d4a5eb76e4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone42.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to b2ginbound, a=merge
browser/base/content/searchSuggestionUI.css
browser/base/content/searchSuggestionUI.js
browser/base/content/test/general/browser_searchSuggestionUI.js
browser/base/content/test/general/searchSuggestionUI.html
browser/base/content/test/general/searchSuggestionUI.js
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1123386 requires clobber for switching to use a new compiler
+Bug 1171344 requires a clobber due to renaming of a test file.
--- a/accessible/ipc/DocAccessibleParent.cpp
+++ b/accessible/ipc/DocAccessibleParent.cpp
@@ -173,30 +173,41 @@ DocAccessibleParent::RecvTextChangeEvent
   ProxyTextChangeEvent(target, aStr, aStart, aLen, aIsInsert, aFromUser);
 
   return true;
 }
 
 bool
 DocAccessibleParent::RecvBindChildDoc(PDocAccessibleParent* aChildDoc, const uint64_t& aID)
 {
+  // One document should never directly be the child of another.
+  // We should always have at least an outer doc accessible in between.
+  MOZ_ASSERT(aID);
+  if (!aID)
+    return false;
+
   auto childDoc = static_cast<DocAccessibleParent*>(aChildDoc);
-  DebugOnly<bool> result = AddChildDoc(childDoc, aID, false);
+  bool result = AddChildDoc(childDoc, aID, false);
   MOZ_ASSERT(result);
-  return true;
+  return result;
 }
 
 bool
 DocAccessibleParent::AddChildDoc(DocAccessibleParent* aChildDoc,
                                  uint64_t aParentID, bool aCreating)
 {
-  ProxyAccessible* outerDoc = mAccessibles.GetEntry(aParentID)->mProxy;
-  if (!outerDoc)
+  // We do not use GetAccessible here because we want to be sure to not get the
+  // document it self.
+  ProxyEntry* e = mAccessibles.GetEntry(aParentID);
+  if (!e)
     return false;
 
+  ProxyAccessible* outerDoc = e->mProxy;
+  MOZ_ASSERT(outerDoc);
+
   aChildDoc->mParent = outerDoc;
   outerDoc->SetChildDoc(aChildDoc);
   mChildDocs.AppendElement(aChildDoc);
   aChildDoc->mParentDoc = this;
 
   if (aCreating) {
     ProxyCreated(aChildDoc, 0);
   }
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -329,17 +329,16 @@ pref("media.eme.apiVisible", true);
 
 // The default number of decoded video frames that are enqueued in
 // MediaDecoderReader's mVideoQueue.
 pref("media.video-queue.default-size", 3);
 
 // optimize images' memory usage
 pref("image.downscale-during-decode.enabled", true);
 pref("image.mem.allow_locking_in_content_processes", true);
-pref("image.decode.retry-on-alloc-failure", true);
 // Limit the surface cache to 1/8 of main memory or 128MB, whichever is smaller.
 // Almost everything that was factored into 'max_decoded_image_kb' is now stored
 // in the surface cache.  1/8 of main memory is 32MB on a 256MB device, which is
 // about the same as the old 'max_decoded_image_kb'.
 pref("image.mem.surfacecache.max_size_kb", 131072);  // 128MB
 pref("image.mem.surfacecache.size_factor", 8);  // 1/8 of main memory
 pref("image.mem.surfacecache.discard_factor", 2);  // Discard 1/2 of the surface cache at a time.
 pref("image.mem.surfacecache.min_expiration_ms", 86400000); // 24h, we rely on the out of memory hook
--- a/browser/base/content/abouthome/aboutHome.css
+++ b/browser/base/content/abouthome/aboutHome.css
@@ -44,130 +44,112 @@ a {
   width: 192px;
   margin: 22px auto 31px;
   background-image: url("chrome://branding/content/about-logo.png");
   background-size: 192px auto;
   background-position: center center;
   background-repeat: no-repeat;
 }
 
-#searchForm,
+#searchIconAndTextContainer,
 #snippets {
   width: 470px;
 }
 
-#searchForm {
-  display: -moz-box;
-}
-
-#searchLogoContainer {
+#searchIconAndTextContainer {
   display: -moz-box;
-  -moz-box-align: center;
-  padding-top: 2px;
-  -moz-padding-end: 8px;
-}
-
-#searchLogoContainer[hidden] {
-  display: none;
-}
-
-#searchEngineLogo {
-  display: inline-block;
-  height: 28px;
-  width: 70px;
-  min-width: 70px;
+  height: 36px;
+  position: relative;
 }
 
 #searchIcon {
-  border: 1px solid transparent;
-  -moz-margin-end: 5px;
-  height: 38px;
-  width: 38px;
-  background: url("chrome://browser/skin/magnifier.png") center center no-repeat;
-  background-size: 26px;
-}
-
-#searchIcon[active],
-#searchIcon:hover {
-  background-color: #e9e9e9;
-  border-color: rgb(226, 227, 229);
-  border-radius: 2.5px;
-}
-
-html[searchUIConfiguration="oldsearchui"] #searchIcon {
-  display: none;
-}
-
-html:not([searchUIConfiguration="oldsearchui"]) #searchText::-moz-placeholder {
-  color: transparent;
-}
-
-html:not([searchUIConfiguration="oldsearchui"]) #searchLogoContainer {
-  display: none;
+  border: 1px transparent;
+  padding: 0;
+  margin: 0;
+  width: 36px;
+  height: 36px;
+  background: url("chrome://browser/skin/search-indicator-magnifying-glass.svg") center center no-repeat;
+  position: absolute;
 }
 
 #searchText {
+  margin-left: 0;
   -moz-box-flex: 1;
-  padding: 6px 8px;
+  padding-top: 6px;
+  padding-bottom: 6px;
+  padding-left: 34px;
+  padding-right: 8px;
   background: hsla(0,0%,100%,.9) padding-box;
   border: 1px solid;
+  border-radius: 2px 0 0 2px;
   border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
   box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset,
               0 0 2px hsla(210,65%,9%,.1) inset,
               0 1px 0 hsla(0,0%,100%,.2);
-  border-radius: 2.5px 0 0 2.5px;
   color: inherit;
 }
 
 #searchText:-moz-dir(rtl) {
-  border-radius: 0 2.5px 2.5px 0;
+  border-radius: 0 2px 2px 0;
 }
 
+#searchText[aria-expanded="true"] {
+  border-radius: 2px 0 0 0;
+}
+
+#searchText[aria-expanded="true"]:-moz-dir(rtl) {
+  border-radius: 0 2px 0 0;
+}
+
+#searchText[keepfocus],
 #searchText:focus,
 #searchText[autofocus] {
   border-color: hsla(206,100%,60%,.6) hsla(206,76%,52%,.6) hsla(204,100%,40%,.6);
 }
 
 #searchSubmit {
   -moz-margin-start: -1px;
-  background: linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
-  padding: 0 9px;
+  background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go") center center no-repeat, linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
+  padding: 0;
   border: 1px solid;
   border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+  border-radius: 0 2px 2px 0;
   -moz-border-start: 1px solid transparent;
-  border-radius: 0 2.5px 2.5px 0;
   box-shadow: 0 0 2px hsla(0,0%,100%,.5) inset,
               0 1px 0 hsla(0,0%,100%,.2);
   color: inherit;
   cursor: pointer;
   transition-property: background-color, border-color, box-shadow;
   transition-duration: 150ms;
+  width: 50px;
 }
 
 #searchSubmit:-moz-dir(rtl) {
-  border-radius: 2.5px 0 0 2.5px;
+  border-radius: 2px 0 0 2px;
 }
 
 #searchText:focus + #searchSubmit,
+#searchText[keepfocus] + #searchSubmit,
 #searchText + #searchSubmit:hover,
 #searchText[autofocus] + #searchSubmit {
   border-color: #59b5fc #45a3e7 #3294d5;
   color: white;
 }
 
 #searchText:focus + #searchSubmit,
+#searchText[keepfocus] + #searchSubmit,
 #searchText[autofocus] + #searchSubmit {
-  background-image: linear-gradient(#4cb1ff, #1793e5);
+  background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted") center center no-repeat, linear-gradient(#4cb1ff, #1793e5);
   box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
               0 0 0 1px hsla(0,0%,100%,.1) inset,
               0 1px 0 hsla(210,54%,20%,.03);
 }
 
 #searchText + #searchSubmit:hover {
-  background-image: linear-gradient(#66bdff, #0d9eff);
+  background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted") center center no-repeat, linear-gradient(#66bdff, #0d9eff);
   box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
               0 0 0 1px hsla(0,0%,100%,.1) inset,
               0 1px 0 hsla(210,54%,20%,.03),
               0 0 4px hsla(206,100%,20%,.2);
 }
 
 #searchText + #searchSubmit:hover:active {
   box-shadow: 0 1px 1px hsla(211,79%,6%,.1) inset,
@@ -175,29 +157,29 @@ html:not([searchUIConfiguration="oldsear
   transition-duration: 0ms;
 }
 
 #defaultSnippet1,
 #defaultSnippet2,
 #rightsSnippet {
   display: block;
   min-height: 38px;
-  background: 30px center no-repeat;
+  background: 0 center no-repeat;
   padding: 6px 0;
-  -moz-padding-start: 79px;
+  -moz-padding-start: 49px;
 }
 
 #rightsSnippet[hidden] {
   display: none;
 }
 
 #defaultSnippet1:-moz-dir(rtl),
 #defaultSnippet2:-moz-dir(rtl),
 #rightsSnippet:-moz-dir(rtl) {
-  background-position: right 30px center;
+  background-position: right 0 center;
 }
 
 #defaultSnippet1 {
   background-image: url("chrome://browser/content/abouthome/snippet1.png");
 }
 
 #defaultSnippet2 {
   background-image: url("chrome://browser/content/abouthome/snippet2.png");
@@ -242,17 +224,17 @@ body[narrow] #launcher[session] {
   padding: 14px 6px;
   min-width: 88px;
   max-width: 176px;
   max-height: 85px;
   vertical-align: top;
   white-space: normal;
   background: transparent padding-box;
   border: 1px solid transparent;
-  border-radius: 2.5px;
+  border-radius: 2px;
   color: #525c66;
   font-size: 75%;
   cursor: pointer;
   transition-property: background-color, border-color, box-shadow;
   transition-duration: 150ms;
 }
 
 body[narrow] #launcher[session] > .launchButton {
@@ -391,20 +373,16 @@ body[narrow] #restorePreviousSession::be
  * At resolutions above 1dppx, prefer downscaling the 2x Retina graphics
  * rather than upscaling the original-size ones (bug 818940).
  */
 @media not all and (max-resolution: 1dppx) {
   #brandLogo {
     background-image: url("chrome://branding/content/about-logo@2x.png");
   }
 
-  #searchIcon {
-    background-image: url("chrome://browser/skin/magnifier@2x.png");
-  }
-
   #defaultSnippet1,
   #defaultSnippet2,
   #rightsSnippet {
     background-size: 40px;
   }
 
   #defaultSnippet1 {
     background-image: url("chrome://browser/content/abouthome/snippet1@2x.png");
--- a/browser/base/content/abouthome/aboutHome.js
+++ b/browser/base/content/abouthome/aboutHome.js
@@ -1,146 +1,14 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const SEARCH_ENGINES = {
-  "Google": {
-    // This is the "2x" image designed for OS X retina resolution, Windows at 192dpi, etc.;
-    // it will be scaled down as necessary on lower-dpi displays.
-    // This needs to be defined in a single line to keep the JS parser from creating many
-    // intermediate strings in memory.  See bug 986672.
-    image: "data:image/png;base64,\
-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"
-  }
-};
-
 // The process of adding a new default snippet involves:
 //   * add a new entity to aboutHome.dtd
 //   * add a <span/> for it in aboutHome.xhtml
 //   * add an entry here in the proper ordering (based on spans)
 // The <a/> part of the snippet will be linked to the corresponding url.
 const DEFAULT_SNIPPETS_URLS = [
   "https://www.mozilla.org/firefox/features/?utm_source=snippet&utm_medium=snippet&utm_campaign=default+feature+snippet"
 , "https://addons.mozilla.org/firefox/?utm_source=snippet&utm_medium=snippet&utm_campaign=addons"
@@ -153,32 +21,32 @@ const DATABASE_NAME = "abouthome";
 const DATABASE_VERSION = 1;
 const DATABASE_STORAGE = "persistent";
 const SNIPPETS_OBJECTSTORE_NAME = "snippets";
 
 // This global tracks if the page has been set up before, to prevent double inits
 let gInitialized = false;
 let gObserver = new MutationObserver(function (mutations) {
   for (let mutation of mutations) {
-    if (mutation.attributeName == "searchEngineName") {
-      setupSearchEngine();
+    if (mutation.attributeName == "snippetsVersion") {
       if (!gInitialized) {
         ensureSnippetsMapThen(loadSnippets);
         gInitialized = true;
       }
       return;
     }
   }
 });
 
 window.addEventListener("pageshow", function () {
   // Delay search engine setup, cause browser.js::BrowserOnAboutPageLoad runs
   // later and may use asynchronous getters.
   window.gObserver.observe(document.documentElement, { attributes: true });
   fitToWidth();
+  setupSearch();
   window.addEventListener("resize", fitToWidth);
 
   // Ask chrome to update snippets.
   var event = new CustomEvent("AboutHomeLoad", {bubbles:true});
   document.dispatchEvent(event);
 });
 
 window.addEventListener("pagehide", function() {
@@ -295,94 +163,38 @@ function ensureSnippetsMapThen(aCallback
 
       setTimeout(invokeCallbacks, 0);
     }
   }
 }
 
 function onSearchSubmit(aEvent)
 {
-  let searchText = document.getElementById("searchText");
-  let searchTerms = searchText.value;
-  let engineName = document.documentElement.getAttribute("searchEngineName");
-
-  if (engineName && searchTerms.length > 0) {
-    // Send an event that will perform a search and Firefox Health Report will
-    // record that a search from about:home has occurred.
-    let eventData = {
-      engineName: engineName,
-      searchTerms: searchTerms,
-      originalEvent: {
-        target: {
-          ownerDocument: null
-        },
-        shiftKey: aEvent.shiftKey,
-        ctrlKey: aEvent.ctrlKey,
-        metaKey: aEvent.metaKey,
-        altKey: aEvent.altKey,
-        button: aEvent.button,
-      },
-    };
-
-    if (searchText.hasAttribute("selection-index")) {
-      eventData.selection = {
-        index: searchText.getAttribute("selection-index"),
-        kind: searchText.getAttribute("selection-kind")
-      };
-    }
-
-    eventData = JSON.stringify(eventData);
-
-    let event = new CustomEvent("AboutHomeSearchEvent", {detail: eventData});
-    document.dispatchEvent(event);
-  }
-
-  gSearchSuggestionController.addInputValueToFormHistory();
-
-  if (aEvent) {
-    aEvent.preventDefault();
-  }
+  gContentSearchController.search(aEvent);
 }
 
 
-let gSearchSuggestionController;
+let gContentSearchController;
 
-function setupSearchEngine()
+function setupSearch()
 {
   // The "autofocus" attribute doesn't focus the form element
   // immediately when the element is first drawn, so the
   // attribute is also used for styling when the page first loads.
   let searchText = document.getElementById("searchText");
   searchText.addEventListener("blur", function searchText_onBlur() {
     searchText.removeEventListener("blur", searchText_onBlur);
     searchText.removeAttribute("autofocus");
   });
- 
-  let searchEngineName = document.documentElement.getAttribute("searchEngineName");
-  let searchEngineInfo = SEARCH_ENGINES[searchEngineName];
-  let logoElt = document.getElementById("searchEngineLogo");
 
-  // Add search engine logo.
-  if (searchEngineInfo && searchEngineInfo.image) {
-    logoElt.parentNode.hidden = false;
-    logoElt.src = searchEngineInfo.image;
-    logoElt.alt = searchEngineName;
-    searchText.placeholder = "";
+  if (!gContentSearchController) {
+    gContentSearchController =
+      new ContentSearchUIController(searchText, searchText.parentNode,
+                                    "abouthome", "homepage");
   }
-  else {
-    logoElt.parentNode.hidden = true;
-    searchText.placeholder = searchEngineName;
-  }
-
-  if (!gSearchSuggestionController) {
-    gSearchSuggestionController =
-      new SearchSuggestionUIController(searchText, searchText.parentNode,
-                                       onSearchSubmit);
-  }
-  gSearchSuggestionController.engineName = searchEngineName;
 }
 
 /**
  * Inform the test harness that we're done loading the page.
  */
 function loadCompleted()
 {
   var event = new CustomEvent("AboutHomeLoadSnippetsCompleted", {bubbles:true});
--- a/browser/base/content/abouthome/aboutHome.xhtml
+++ b/browser/base/content/abouthome/aboutHome.xhtml
@@ -19,39 +19,37 @@
 
 <html xmlns="http://www.w3.org/1999/xhtml">
   <head>
     <title>&abouthome.pageTitle;</title>
 
     <link rel="icon" type="image/png" id="favicon"
           href="chrome://branding/content/icon32.png"/>
     <link rel="stylesheet" type="text/css" media="all"
-          href="chrome://browser/content/searchSuggestionUI.css"/>
+          href="chrome://browser/content/contentSearchUI.css"/>
     <link rel="stylesheet" type="text/css" media="all" defer="defer"
           href="chrome://browser/content/abouthome/aboutHome.css"/>
 
     <script type="text/javascript;version=1.8"
             src="chrome://browser/content/abouthome/aboutHome.js"/>
     <script type="text/javascript;version=1.8"
-            src="chrome://browser/content/searchSuggestionUI.js"/>
+            src="chrome://browser/content/contentSearchUI.js"/>
   </head>
 
   <body dir="&locale.dir;">
     <div class="spacer"/>
     <div id="topSection">
       <div id="brandLogo"></div>
 
-      <div id="searchContainer">
-        <form name="searchForm" id="searchForm" onsubmit="onSearchSubmit(event)">
-          <div id="searchLogoContainer" hidden="true"><img id="searchEngineLogo"/></div>
-          <button id="searchIcon" type="button" />
-          <input type="text" name="q" value="" id="searchText" maxlength="256"
-                 autofocus="autofocus" dir="auto"/>
-          <input id="searchSubmit" type="submit" value="&abouthome.searchEngineButton.label;"/>
-        </form>
+      <div id="searchIconAndTextContainer">
+        <div id="searchIcon"/>
+        <input type="text" name="q" value="" id="searchText" maxlength="256"
+               aria-label="&contentSearchInput.label;" autofocus="autofocus" dir="auto"/>
+        <input id="searchSubmit" type="button" value="" onclick="onSearchSubmit(event)"
+               aria-label="&contentSearchSubmit.label;"/>
       </div>
 
       <div id="snippetContainer">
         <div id="defaultSnippets" hidden="true">
           <span id="defaultSnippet1">&abouthome.defaultSnippet1.v1;</span>
           <span id="defaultSnippet2">&abouthome.defaultSnippet2.v1;</span>
         </div>
         <span id="rightsSnippet" hidden="true">&abouthome.rightsSnippet;</span>
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -3476,19 +3476,18 @@ const BrowserSearch = {
       }
       return;
     }
 #endif
     let openSearchPageIfFieldIsNotActive = function(aSearchBar) {
       if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) {
         let url = gBrowser.currentURI.spec.toLowerCase();
         let mm = gBrowser.selectedBrowser.messageManager;
-        if (url === "about:home") {
-          AboutHome.focusInput(mm);
-        } else if (url === "about:newtab" && NewTabUtils.allPages.enabled) {
+        if (url === "about:home" ||
+            (url === "about:newtab" && NewTabUtils.allPages.enabled)) {
           ContentSearch.focusInput(mm);
         } else {
           openUILinkIn("about:home", "current");
         }
       }
     };
 
     let searchBar = this.searchBar;
rename from browser/base/content/searchSuggestionUI.css
rename to browser/base/content/contentSearchUI.css
--- a/browser/base/content/searchSuggestionUI.css
+++ b/browser/base/content/contentSearchUI.css
@@ -1,44 +1,152 @@
 /* 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/. */
 
-.searchSuggestionTable {
+.contentSearchSuggestionTable {
   background-color: hsla(0,0%,100%,.99);
-  border: 1px solid;
-  border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+  border: 1px solid hsla(0, 0%, 0%, .2);
+  border-top: none;
+  box-shadow: 0 5px 10px hsla(0, 0%, 0%, .1);
+  position: absolute;
+  left: 0;
+  z-index: 1001;
+  -moz-user-select: none;
+  cursor: default;
+}
+
+.contentSearchSuggestionsList {
+  border-bottom: 1px solid hsl(0, 0%, 92%);
+  width: 100%;
+  height: 100%;
+}
+
+.contentSearchSuggestionTable,
+.contentSearchSuggestionsList {
   border-spacing: 0;
-  border-top: 0;
-  box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset,
-              0 0 2px hsla(210,65%,9%,.1) inset,
-              0 1px 0 hsla(0,0%,100%,.2);
   overflow: hidden;
   padding: 0;
-  position: absolute;
+  margin: 0;
   text-align: start;
-  z-index: 1001;
 }
 
-.searchSuggestionRow {
-  cursor: default;
+.contentSearchHeaderRow,
+.contentSearchSuggestionRow {
   margin: 0;
   max-width: inherit;
   padding: 0;
 }
 
-.searchSuggestionRow.formHistory + .searchSuggestionRow.remote > td {
-  border-top: 1px solid GrayText;
+.contentSearchHeaderRow > td > img,
+.contentSearchSuggestionRow > td > .historyIcon {
+  margin-right: 8px;
+  margin-bottom: -3px;
+}
+
+.contentSearchSuggestionTable .historyIcon {
+  width: 16px;
+  height: 16px;
+  display: inline-block;
+  background-image: url("chrome://browser/skin/search-history-icon.svg#search-history-icon");
+}
+
+.contentSearchSuggestionRow.selected > td > .historyIcon {
+  background-image: url("chrome://browser/skin/search-history-icon.svg#search-history-icon-active");
 }
 
-.searchSuggestionRow.selected {
-  background-color: hsl(210,100%,40%);
-  color: hsl(0,0%,100%);
+.contentSearchHeader > img {
+  height: 16px;
+  width: 16px;
+  margin: 0;
+  padding: 0;
 }
 
-.searchSuggestionEntry {
+.contentSearchSuggestionRow.remote > td > .historyIcon {
+  visibility: hidden;
+}
+
+.contentSearchSuggestionRow.selected {
+  background-color: Highlight;
+  color: HighlightText;
+}
+
+.contentSearchHeader,
+.contentSearchSuggestionEntry {
   margin: 0;
   max-width: inherit;
   overflow: hidden;
-  padding: 6px 8px;
+  padding: 4px 10px;
   text-overflow: ellipsis;
   white-space: nowrap;
+  font-size: 75%;
 }
+
+.contentSearchHeader {
+  background-color: hsl(0, 0%, 97%);
+  color: #666;
+  border-bottom: 1px solid hsl(0, 0%, 92%);
+}
+
+.contentSearchSuggestionsContainer {
+  margin: 0;
+  padding: 0;
+  border-spacing: 0;
+  width: 100%;
+}
+
+.contentSearchSearchWithHeaderSearchText {
+  white-space: pre;
+  font-weight: bold;
+}
+
+.contentSearchOneOffItem {
+  -moz-appearance: none;
+  height: 32px;
+  margin: 0;
+  padding: 0;
+  border: none;
+  background: none;
+  background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAWCAYAAAABxvaqAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH3gofECQNNVW2/AAAABBJREFUGFdjOHPmzH8GehEA/KpKg9YTf4AAAAAASUVORK5CYII=');
+  background-repeat: no-repeat;
+  background-position: right center;
+}
+
+.contentSearchOneOffItem > img {
+  width: 16px;
+  height: 16px;
+  margin-bottom: -2px;
+}
+
+.contentSearchOneOffItem:not(.last-row) {
+  border-bottom: 1px solid hsl(0, 0%, 92%);
+}
+
+.contentSearchOneOffItem.end-of-row {
+  background-image: none;
+}
+
+.contentSearchOneOffItem.selected {
+  background-color: Highlight;
+  background-image: none;
+}
+
+.contentSearchOneOffsTable {
+  width: 100%;
+}
+
+.contentSearchSettingsButton {
+  margin: 0;
+  padding: 0;
+  height: 32px;
+  border: none;
+  border-top: 1px solid hsla(0, 0%, 0%, .08);
+  text-align: center;
+  width: 100%;
+}
+
+.contentSearchSettingsButton.selected {
+  background-color: hsl(0, 0%, 90%);
+}
+
+.contentSearchSettingsButton:active {
+  background-color: hsl(0, 0%, 85%);
+}
rename from browser/base/content/searchSuggestionUI.js
rename to browser/base/content/contentSearchUI.js
--- a/browser/base/content/searchSuggestionUI.js
+++ b/browser/base/content/contentSearchUI.js
@@ -1,19 +1,20 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-this.SearchSuggestionUIController = (function () {
+this.ContentSearchUIController = (function () {
 
 const MAX_DISPLAYED_SUGGESTIONS = 6;
 const SUGGESTION_ID_PREFIX = "searchSuggestion";
-const CSS_URI = "chrome://browser/content/searchSuggestionUI.css";
+const ONE_OFF_ID_PREFIX = "oneOff";
+const CSS_URI = "chrome://browser/content/contentSearchUI.css";
 
 const HTML_NS = "http://www.w3.org/1999/xhtml";
 
 /**
  * Creates a new object that manages search suggestions and their UI for a text
  * box.
  *
  * The UI consists of an html:table that's inserted into the DOM after the given
@@ -24,267 +25,381 @@ const HTML_NS = "http://www.w3.org/1999/
  *        Assumed to be an html:input.  xul:textbox is untested but might work.
  * @param tableParent
  *        The suggestion table is appended as a child to this element.  Since
  *        the table is absolutely positioned and its top and left values are set
  *        to be relative to the top and left of the page, either the parent and
  *        all its ancestors should not be positioned elements (i.e., their
  *        positions should be "static"), or the parent's position should be the
  *        top left of the page.
- * @param onClick
- *        A function that's called when a search suggestion is clicked.  Ideally
- *        we could call submit() on inputElement's ancestor form, but that
- *        doesn't trigger submit listeners. The function is passed one argument,
- *        the click event.
+ * @param healthReportKey
+ *        This will be sent with the search data for FHR to record the search.
+ * @param searchPurpose
+ *        Sent with search data, see nsISearchEngine.getSubmission.
  * @param idPrefix
  *        The IDs of elements created by the object will be prefixed with this
  *        string.
  */
-function SearchSuggestionUIController(inputElement, tableParent, onClick=null,
-                                      idPrefix="") {
+function ContentSearchUIController(inputElement, tableParent, healthReportKey,
+                                   searchPurpose, idPrefix="") {
   this.input = inputElement;
-  this.onClick = onClick;
   this._idPrefix = idPrefix;
+  this._healthReportKey = healthReportKey;
+  this._searchPurpose = searchPurpose;
 
   let tableID = idPrefix + "searchSuggestionTable";
   this.input.autocomplete = "off";
   this.input.setAttribute("aria-autocomplete", "true");
   this.input.setAttribute("aria-controls", tableID);
   tableParent.appendChild(this._makeTable(tableID));
 
   this.input.addEventListener("keypress", this);
   this.input.addEventListener("input", this);
   this.input.addEventListener("focus", this);
   this.input.addEventListener("blur", this);
   window.addEventListener("ContentSearchService", this);
 
   this._stickyInputValue = "";
   this._hideSuggestions();
 
-  this._ignoreInputEvent = false;
+  this._getSearchEngines();
+  this._getStrings();
 }
 
-SearchSuggestionUIController.prototype = {
+ContentSearchUIController.prototype = {
 
   // The timeout (ms) of the remote suggestions.  Corresponds to
   // SearchSuggestionController.remoteTimeout.  Uses
   // SearchSuggestionController's default timeout if falsey.
   remoteTimeout: undefined,
+  _oneOffButtons: [],
+  // Setting up the one off buttons causes an uninterruptible reflow. If we
+  // receive the list of engines while the newtab page is loading, this reflow
+  // may regress performance - so we set this flag and only set up the buttons
+  // if it's set when the suggestions table is actually opened.
+  _pendingOneOffRefresh: undefined,
 
-  get engineName() {
-    return this._engineName;
+  get defaultEngine() {
+    return this._defaultEngine;
   },
 
-  set engineName(val) {
-    this._engineName = val;
+  set defaultEngine(val) {
+    this._defaultEngine = val;
+    this._updateDefaultEngineHeader();
+
     if (val && document.activeElement == this.input) {
       this._speculativeConnect();
     }
   },
 
+  get engines() {
+    return this._engines;
+  },
+
+  set engines(val) {
+    this._engines = val;
+    if (!this._table.hidden) {
+      this._setUpOneOffButtons();
+      return;
+    }
+    this._pendingOneOffRefresh = true;
+  },
+
+  // The selectedIndex is the index of the element with the "selected" class in
+  // the list obtained by concatenating the suggestion rows, one-off buttons, and
+  // search settings button.
   get selectedIndex() {
-    for (let i = 0; i < this._table.children.length; i++) {
-      let row = this._table.children[i];
-      if (row.classList.contains("selected")) {
+    let allElts = [...this._suggestionsList.children,
+                   ...this._oneOffButtons,
+                   document.getElementById("contentSearchSettingsButton")];
+    for (let i = 0; i < allElts.length; ++i) {
+      let elt = allElts[i];
+      if (elt.classList.contains("selected")) {
         return i;
       }
     }
     return -1;
   },
 
   set selectedIndex(idx) {
     // Update the table's rows, and the input when there is a selection.
     this._table.removeAttribute("aria-activedescendant");
-    for (let i = 0; i < this._table.children.length; i++) {
-      let row = this._table.children[i];
+    this.input.removeAttribute("aria-activedescendant");
+
+    let allElts = [...this._suggestionsList.children,
+                   ...this._oneOffButtons,
+                   document.getElementById("contentSearchSettingsButton")];
+    for (let i = 0; i < allElts.length; ++i) {
+      let elt = allElts[i];
+      let ariaSelectedElt = i < this.numSuggestions ? elt.firstChild : elt;
       if (i == idx) {
-        row.classList.add("selected");
-        row.firstChild.setAttribute("aria-selected", "true");
-        this._table.setAttribute("aria-activedescendant", row.firstChild.id);
+        elt.classList.add("selected");
+        ariaSelectedElt.setAttribute("aria-selected", "true");
+        this.input.setAttribute("aria-activedescendant", ariaSelectedElt.id);
       }
       else {
-        row.classList.remove("selected");
-        row.firstChild.setAttribute("aria-selected", "false");
+        elt.classList.remove("selected");
+        ariaSelectedElt.setAttribute("aria-selected", "false");
       }
     }
   },
 
+  get selectedEngineName() {
+    let selectedElt = this._table.querySelector(".selected");
+    if (selectedElt && selectedElt.engineName) {
+      return selectedElt.engineName;
+    }
+    return this.defaultEngine.name;
+  },
+
   get numSuggestions() {
-    return this._table.children.length;
+    return this._suggestionsList.children.length;
   },
 
   selectAndUpdateInput: function (idx) {
     this.selectedIndex = idx;
-    this.input.value = idx >= 0 ? this.suggestionAtIndex(idx) :
-                       this._stickyInputValue;
+    let newValue = this.suggestionAtIndex(idx) || this._stickyInputValue;
+    // Setting the input value when the value has not changed commits the current
+    // IME composition, which we don't want to do.
+    if (this.input.value != newValue) {
+      this.input.value = newValue;
+    }
+    this._updateSearchWithHeader();
   },
 
   suggestionAtIndex: function (idx) {
-    let row = this._table.children[idx];
+    let row = this._suggestionsList.children[idx];
     return row ? row.textContent : null;
   },
 
   deleteSuggestionAtIndex: function (idx) {
     // Only form history suggestions can be deleted.
     if (this.isFormHistorySuggestionAtIndex(idx)) {
       let suggestionStr = this.suggestionAtIndex(idx);
       this._sendMsg("RemoveFormHistoryEntry", suggestionStr);
-      this._table.children[idx].remove();
+      this._suggestionsList.children[idx].remove();
       this.selectAndUpdateInput(-1);
     }
   },
 
   isFormHistorySuggestionAtIndex: function (idx) {
-    let row = this._table.children[idx];
+    let row = this._suggestionsList.children[idx];
     return row && row.classList.contains("formHistory");
   },
 
   addInputValueToFormHistory: function () {
     this._sendMsg("AddFormHistoryEntry", this.input.value);
   },
 
   handleEvent: function (event) {
     this["_on" + event.type[0].toUpperCase() + event.type.substr(1)](event);
   },
 
-  _onInput: function (event) {
-    if (this._ignoreInputEvent) {
+  _onCommand: function(aEvent) {
+    if (this.selectedIndex == this.numSuggestions + this._oneOffButtons.length) {
+      // Settings button was selected.
+      this._sendMsg("ManageEngines");
       return;
     }
-    if (this.input.value) {
-      this._getSuggestions();
+
+    this.search(aEvent);
+
+    if (aEvent) {
+      aEvent.preventDefault();
+    }
+  },
+
+  search: function (aEvent) {
+    if (!this.defaultEngine) {
+      return; // Not initialized yet.
+    }
+
+    let searchText = this.input;
+    let searchTerms;
+    if (this._table.hidden ||
+        aEvent.originalTarget.id == "contentSearchDefaultEngineHeader") {
+      searchTerms = searchText.value;
     }
     else {
+      searchTerms = this.suggestionAtIndex(this.selectedIndex) || searchText.value;
+    }
+    // Send an event that will perform a search and Firefox Health Report will
+    // record that a search from the healthReportKey passed to the constructor.
+    let eventData = {
+      engineName: this.selectedEngineName,
+      searchString: searchTerms,
+      healthReportKey: this._healthReportKey,
+      searchPurpose: this._searchPurpose,
+      originalEvent: {
+        shiftKey: aEvent.shiftKey,
+        ctrlKey: aEvent.ctrlKey,
+        metaKey: aEvent.metaKey,
+        altKey: aEvent.altKey,
+        button: aEvent.button,
+      },
+    };
+
+    if (this.suggestionAtIndex(this.selectedIndex)) {
+      eventData.selection = {
+        index: this.selectedIndex,
+        kind: aEvent instanceof MouseEvent ? "mouse" :
+              aEvent instanceof KeyboardEvent ? "key" : undefined,
+      };
+    }
+
+    this._sendMsg("Search", eventData);
+    this.addInputValueToFormHistory();
+  },
+
+  _onInput: function () {
+    if (!this.input.value) {
       this._stickyInputValue = "";
       this._hideSuggestions();
     }
-    this.selectedIndex = -1;
+    else if (this.input.value != this._stickyInputValue) {
+      // Only fetch new suggestions if the input value has changed.
+      this._getSuggestions();
+      this.selectAndUpdateInput(-1);
+    }
+    this._updateSearchWithHeader();
   },
 
   _onKeypress: function (event) {
     let selectedIndexDelta = 0;
     switch (event.keyCode) {
     case event.DOM_VK_UP:
-      if (this.numSuggestions) {
+      if (!this._table.hidden) {
         selectedIndexDelta = -1;
       }
       break;
     case event.DOM_VK_DOWN:
-      if (this.numSuggestions) {
-        selectedIndexDelta = 1;
+      if (this._table.hidden) {
+        this._getSuggestions();
       }
       else {
-        this._getSuggestions();
+        selectedIndexDelta = 1;
       }
       break;
     case event.DOM_VK_RIGHT:
       // Allow normal caret movement until the caret is at the end of the input.
       if (this.input.selectionStart != this.input.selectionEnd ||
           this.input.selectionEnd != this.input.value.length) {
         return;
       }
-      // else, fall through
-    case event.DOM_VK_RETURN:
-      if (this.selectedIndex >= 0) {
+      if (this.numSuggestions && this.selectedIndex >= 0 &&
+          this.selectedIndex < this.numSuggestions) {
         this.input.value = this.suggestionAtIndex(this.selectedIndex);
         this.input.setAttribute("selection-index", this.selectedIndex);
         this.input.setAttribute("selection-kind", "key");
       } else {
         // If we didn't select anything, make sure to remove the attributes
         // in case they were populated last time.
         this.input.removeAttribute("selection-index");
         this.input.removeAttribute("selection-kind");
       }
       this._stickyInputValue = this.input.value;
       this._hideSuggestions();
       break;
+    case event.DOM_VK_RETURN:
+      this._onCommand(event);
+      break;
     case event.DOM_VK_DELETE:
       if (this.selectedIndex >= 0) {
         this.deleteSuggestionAtIndex(this.selectedIndex);
       }
       break;
+    case event.DOM_VK_ESCAPE:
+      if (!this._table.hidden) {
+        this._hideSuggestions();
+      }
     default:
       return;
     }
 
     if (selectedIndexDelta) {
       // Update the selection.
       let newSelectedIndex = this.selectedIndex + selectedIndexDelta;
       if (newSelectedIndex < -1) {
-        newSelectedIndex = this.numSuggestions - 1;
+        newSelectedIndex = this.numSuggestions + this._oneOffButtons.length;
       }
-      else if (this.numSuggestions <= newSelectedIndex) {
+      else if (this.numSuggestions + this._oneOffButtons.length < newSelectedIndex) {
         newSelectedIndex = -1;
       }
       this.selectAndUpdateInput(newSelectedIndex);
 
       // Prevent the input's caret from moving.
       event.preventDefault();
     }
   },
 
   _onFocus: function () {
+    if (this._mousedown) {
+      return;
+    }
+    // When the input box loses focus to something in our table, we refocus it
+    // immediately. This causes the focus highlight to flicker, so we set a
+    // custom attribute which consumers should use for focus highlighting. This
+    // attribute is removed only when we do not immediately refocus the input
+    // box, thus eliminating flicker.
+    this.input.setAttribute("keepfocus", "true");
     this._speculativeConnect();
   },
 
   _onBlur: function () {
+    if (this._mousedown) {
+      // At this point, this.input has lost focus, but a new element has not yet
+      // received it. If we re-focus this.input directly, the new element will
+      // steal focus immediately, so we queue it instead.
+      setTimeout(() => this.input.focus(), 0);
+      return;
+    }
+    this.input.removeAttribute("keepfocus");
     this._hideSuggestions();
   },
 
   _onMousemove: function (event) {
-    this.selectedIndex = this._indexOfTableRowOrDescendent(event.target);
+    this.selectedIndex = this._indexOfTableItem(event.target);
   },
 
-  _onMousedown: function (event) {
+  _onMouseup: function (event) {
     if (event.button == 2) {
       return;
     }
-    let idx = this._indexOfTableRowOrDescendent(event.target);
-    let suggestion = this.suggestionAtIndex(idx);
-    this._stickyInputValue = suggestion;
+    this._onCommand(event);
+  },
 
-    // Setting value commits composition string forcibly.  While IME commits
-    // composition, this needs to ignore input event at committed composition
-    // string which will be overwritten by the suggestion.
-    this._ignoreInputEvent = true;
-    this.input.value = suggestion;
-    this._ignoreInputEvent = false;
-    this.input.setAttribute("selection-index", idx);
-    this.input.setAttribute("selection-kind", "mouse");
-    this._hideSuggestions();
-    if (this.onClick) {
-      this.onClick.call(null, event);
-    }
+  _onClick: function (event) {
+    this._onMouseup(event);
   },
 
   _onContentSearchService: function (event) {
     let methodName = "_onMsg" + event.detail.type;
     if (methodName in this) {
       this[methodName](event.detail.data);
     }
   },
 
+  _onMsgFocusInput: function (event) {
+    this.input.focus();
+  },
+
   _onMsgSuggestions: function (suggestions) {
     // Ignore the suggestions if their search string or engine doesn't match
     // ours.  Due to the async nature of message passing, this can easily happen
     // when the user types quickly.
     if (this._stickyInputValue != suggestions.searchString ||
-        this.engineName != suggestions.engineName) {
+        this.defaultEngine.name != suggestions.engineName) {
       return;
     }
 
-    // Empty the table.
-    while (this._table.firstElementChild) {
-      this._table.firstElementChild.remove();
-    }
+    this._clearSuggestionRows();
 
     // Position and size the table.
-    let { left, bottom } = this.input.getBoundingClientRect();
-    this._table.style.left = (left + window.scrollX) + "px";
-    this._table.style.top = (bottom + window.scrollY) + "px";
+    let { left } = this.input.getBoundingClientRect();
+    this._table.style.top = this.input.offsetHeight + "px";
     this._table.style.minWidth = this.input.offsetWidth + "px";
     this._table.style.maxWidth = (window.innerWidth - left - 40) + "px";
 
     // Add the suggestions to the table.
     let searchWords =
       new Set(suggestions.searchString.trim().toLowerCase().split(/\s+/));
     for (let i = 0; i < MAX_DISPLAYED_SUGGESTIONS; i++) {
       let type, idx;
@@ -295,41 +410,116 @@ SearchSuggestionUIController.prototype =
         let j = i - suggestions.formHistory.length;
         if (j < suggestions.remote.length) {
           [type, idx] = ["remote", j];
         }
         else {
           break;
         }
       }
-      this._table.appendChild(this._makeTableRow(type, suggestions[type][idx],
-                                                 i, searchWords));
+      this._suggestionsList.appendChild(
+        this._makeTableRow(type, suggestions[type][idx], i, searchWords));
     }
 
-    this._table.hidden = false;
-    this.input.setAttribute("aria-expanded", "true");
+    if (this._table.hidden) {
+      this.selectedIndex = -1;
+      if (this._pendingOneOffRefresh) {
+        this._setUpOneOffButtons();
+        delete this._pendingOneOffRefresh;
+      }
+      this._table.hidden = false;
+      this.input.setAttribute("aria-expanded", "true");
+    }
+  },
+
+  _onMsgState: function (state) {
+    this.defaultEngine = {
+      name: state.currentEngine.name,
+      icon: this._getFaviconURIFromBuffer(state.currentEngine.iconBuffer),
+    };
+    this.engines = state.engines;
+  },
+
+  _onMsgCurrentState: function (state) {
+    this._onMsgState(state);
+  },
+
+  _onMsgCurrentEngine: function (engine) {
+    this.defaultEngine = {
+      name: engine.name,
+      icon: this._getFaviconURIFromBuffer(engine.iconBuffer),
+    };
+    if (!this._table.hidden) {
+      this._setUpOneOffButtons();
+      return;
+    }
+    this._pendingOneOffRefresh = true;
+  },
+
+  _onMsgStrings: function (strings) {
+    this._strings = strings;
+    this._updateDefaultEngineHeader();
+    this._updateSearchWithHeader();
+  },
+
+  _updateDefaultEngineHeader: function () {
+    let header = document.getElementById("contentSearchDefaultEngineHeader");
+    if (this.defaultEngine.icon) {
+      header.firstChild.setAttribute("src", this.defaultEngine.icon);
+    }
+    if (!this._strings) {
+      return;
+    }
+    while (header.firstChild.nextSibling) {
+      header.firstChild.nextSibling.remove();
+    }
+    header.appendChild(document.createTextNode(
+      this._strings.searchHeader.replace("%S", this.defaultEngine.name)));
+  },
+
+  _updateSearchWithHeader: function () {
+    if (!this._strings) {
+      return;
+    }
+    let searchWithHeader = document.getElementById("contentSearchSearchWithHeader");
+    while (searchWithHeader.firstChild) {
+      searchWithHeader.firstChild.remove();
+    }
+    if (this.input.value) {
+      searchWithHeader.appendChild(document.createTextNode(this._strings.searchFor));
+      let span = document.createElementNS(HTML_NS, "span");
+      span.setAttribute("class", "contentSearchSearchWithHeaderSearchText");
+      span.appendChild(document.createTextNode(" " + this.input.value + " "));
+      searchWithHeader.appendChild(span);
+      searchWithHeader.appendChild(document.createTextNode(this._strings.searchWith));
+      return;
+    }
+    searchWithHeader.appendChild(document.createTextNode(this._strings.searchWithHeader));
   },
 
   _speculativeConnect: function () {
-    if (this.engineName) {
-      this._sendMsg("SpeculativeConnect", this.engineName);
+    if (this.defaultEngine) {
+      this._sendMsg("SpeculativeConnect", this.defaultEngine.name);
     }
   },
 
   _makeTableRow: function (type, suggestionStr, currentRow, searchWords) {
     let row = document.createElementNS(HTML_NS, "tr");
     row.dir = "auto";
-    row.classList.add("searchSuggestionRow");
+    row.classList.add("contentSearchSuggestionRow");
     row.classList.add(type);
     row.setAttribute("role", "presentation");
     row.addEventListener("mousemove", this);
-    row.addEventListener("mousedown", this);
+    row.addEventListener("mouseup", this);
 
     let entry = document.createElementNS(HTML_NS, "td");
-    entry.classList.add("searchSuggestionEntry");
+    let img = document.createElementNS(HTML_NS, "div");
+    img.setAttribute("class", "historyIcon");
+    entry.appendChild(img);
+    entry.classList.add("contentSearchSuggestionEntry");
     entry.setAttribute("role", "option");
     entry.id = this._idPrefix + SUGGESTION_ID_PREFIX + currentRow;
     entry.setAttribute("aria-selected", "false");
 
     let suggestionWords = suggestionStr.trim().toLowerCase().split(/\s+/);
     for (let i = 0; i < suggestionWords.length; i++) {
       let word = suggestionWords[i];
       let wordSpan = document.createElementNS(HTML_NS, "span");
@@ -342,59 +532,221 @@ SearchSuggestionUIController.prototype =
         entry.appendChild(document.createTextNode(" "));
       }
     }
 
     row.appendChild(entry);
     return row;
   },
 
+  // Converts favicon array buffer into data URI of the right size and dpi.
+  _getFaviconURIFromBuffer: function (buffer) {
+    let blob = new Blob([buffer]);
+    let dpiSize = Math.round(16 * window.devicePixelRatio);
+    let sizeStr = dpiSize + "," + dpiSize;
+    return URL.createObjectURL(blob) + "#-moz-resolution=" + sizeStr;
+  },
+
+  _getSearchEngines: function () {
+    this._sendMsg("GetState");
+  },
+
+  _getStrings: function () {
+    this._sendMsg("GetStrings");
+  },
+
   _getSuggestions: function () {
     this._stickyInputValue = this.input.value;
-    if (this.engineName) {
+    if (this.defaultEngine) {
       this._sendMsg("GetSuggestions", {
-        engineName: this.engineName,
+        engineName: this.defaultEngine.name,
         searchString: this.input.value,
         remoteTimeout: this.remoteTimeout,
       });
     }
   },
 
+  _clearSuggestionRows: function() {
+    while (this._suggestionsList.firstElementChild) {
+      this._suggestionsList.firstElementChild.remove();
+    }
+  },
+
   _hideSuggestions: function () {
     this.input.setAttribute("aria-expanded", "false");
     this._table.hidden = true;
-    while (this._table.firstElementChild) {
-      this._table.firstElementChild.remove();
-    }
-    this.selectAndUpdateInput(-1);
   },
 
-  _indexOfTableRowOrDescendent: function (row) {
-    while (row && row.localName != "tr") {
-      row = row.parentNode;
+  _indexOfTableItem: function (elt) {
+    if (elt.classList.contains("contentSearchOneOffItem")) {
+      return this.numSuggestions + this._oneOffButtons.indexOf(elt);
     }
-    if (!row) {
+    if (elt.classList.contains("contentSearchSettingsButton")) {
+      return this.numSuggestions + this._oneOffButtons.length;
+    }
+    while (elt && elt.localName != "tr") {
+      elt = elt.parentNode;
+    }
+    if (!elt) {
       throw new Error("Element is not a row");
     }
-    return row.rowIndex;
+    return elt.rowIndex;
   },
 
   _makeTable: function (id) {
     this._table = document.createElementNS(HTML_NS, "table");
     this._table.id = id;
     this._table.hidden = true;
-    this._table.classList.add("searchSuggestionTable");
-    this._table.setAttribute("role", "listbox");
+    this._table.classList.add("contentSearchSuggestionTable");
+    this._table.setAttribute("role", "presentation");
+
+    // When the search input box loses focus, we want to immediately give focus
+    // back to it if the blur was because the user clicked somewhere in the table.
+    // onBlur uses the _mousedown flag to detect this.
+    this._table.addEventListener("mousedown", () => { this._mousedown = true; });
+    document.addEventListener("mouseup", () => { delete this._mousedown; });
+
+    // Deselect the selected element on mouseout if it wasn't a suggestion.
+    this._table.addEventListener("mouseout", () => {
+      if (this.selectedIndex >= this.numSuggestions) {
+        this.selectAndUpdateInput(-1);
+      }
+    });
+
+    // If a search is loaded in the same tab, ensure the suggestions dropdown
+    // is hidden immediately when the page starts loading and not when it first
+    // appears, in order to provide timely feedback to the user.
+    window.addEventListener("beforeunload", () => { this._hideSuggestions(); });
+
+    let headerRow = document.createElementNS(HTML_NS, "tr");
+    let header = document.createElementNS(HTML_NS, "td");
+    headerRow.setAttribute("class", "contentSearchHeaderRow");
+    header.setAttribute("class", "contentSearchHeader");
+    let img = document.createElementNS(HTML_NS, "img");
+    img.setAttribute("src", "chrome://browser/skin/search-engine-placeholder.png");
+    header.appendChild(img);
+    header.id = "contentSearchDefaultEngineHeader";
+    headerRow.appendChild(header);
+    headerRow.addEventListener("click", this);
+    this._table.appendChild(headerRow);
+
+    let row = document.createElementNS(HTML_NS, "tr");
+    row.setAttribute("class", "contentSearchSuggestionsContainer");
+    let cell = document.createElementNS(HTML_NS, "td");
+    cell.setAttribute("class", "contentSearchSuggestionsContainer");
+    this._suggestionsList = document.createElementNS(HTML_NS, "table");
+    this._suggestionsList.setAttribute("class", "contentSearchSuggestionsList");
+    cell.appendChild(this._suggestionsList);
+    row.appendChild(cell);
+    this._table.appendChild(row);
+    this._suggestionsList.setAttribute("role", "listbox");
+
+    this._oneOffsTable = document.createElementNS(HTML_NS, "table");
+    this._oneOffsTable.setAttribute("class", "contentSearchOneOffsTable");
+    this._oneOffsTable.classList.add("contentSearchSuggestionsContainer");
+    this._oneOffsTable.setAttribute("role", "group");
+    this._table.appendChild(this._oneOffsTable);
+
+    headerRow = document.createElementNS(HTML_NS, "tr");
+    header = document.createElementNS(HTML_NS, "td");
+    headerRow.setAttribute("class", "contentSearchHeaderRow");
+    header.setAttribute("class", "contentSearchHeader");
+    headerRow.appendChild(header);
+    header.id = "contentSearchSearchWithHeader";
+    this._oneOffsTable.appendChild(headerRow);
+
+    let button = document.createElementNS(HTML_NS, "button");
+    button.appendChild(document.createTextNode("Change Search Settings"));
+    button.setAttribute("class", "contentSearchSettingsButton");
+    button.classList.add("contentSearchHeaderRow");
+    button.classList.add("contentSearchHeader");
+    button.id = "contentSearchSettingsButton";
+    button.addEventListener("click", this);
+    button.addEventListener("mousemove", this);
+    this._table.appendChild(button);
+
     return this._table;
   },
 
+  _setUpOneOffButtons: function () {
+    // Sometimes we receive a CurrentEngine message from the ContentSearch service
+    // before we've received a State message - i.e. before we have our engines.
+    if (!this._engines) {
+      return;
+    }
+
+    while (this._oneOffsTable.firstChild.nextSibling) {
+      this._oneOffsTable.firstChild.nextSibling.remove();
+    }
+
+    this._oneOffButtons = [];
+
+    let engines = this._engines.filter(aEngine => aEngine.name != this.defaultEngine.name);
+    if (!engines.length) {
+      this._oneOffsTable.hidden = true;
+      return;
+    }
+
+    const kDefaultButtonWidth = 49; // 48px + 1px border.
+    let rowWidth = this.input.offsetWidth - 2; // 2px border.
+    let enginesPerRow = Math.floor(rowWidth / kDefaultButtonWidth);
+    let buttonWidth = Math.floor(rowWidth / enginesPerRow);
+
+    let row = document.createElementNS(HTML_NS, "tr");
+    let cell = document.createElementNS(HTML_NS, "td");
+    row.setAttribute("class", "contentSearchSuggestionsContainer");
+    cell.setAttribute("class", "contentSearchSuggestionsContainer");
+
+    for (let i = 0; i < engines.length; ++i) {
+      let engine = engines[i];
+      if (i > 0 && i % enginesPerRow == 0) {
+        row.appendChild(cell);
+        this._oneOffsTable.appendChild(row);
+        row = document.createElementNS(HTML_NS, "tr");
+        cell = document.createElementNS(HTML_NS, "td");
+        row.setAttribute("class", "contentSearchSuggestionsContainer");
+        cell.setAttribute("class", "contentSearchSuggestionsContainer");
+      }
+      let button = document.createElementNS(HTML_NS, "button");
+      button.setAttribute("class", "contentSearchOneOffItem");
+      let img = document.createElementNS(HTML_NS, "img");
+      let uri = "chrome://browser/skin/search-engine-placeholder.png";
+      if (engine.iconBuffer) {
+        uri = this._getFaviconURIFromBuffer(engine.iconBuffer);
+      }
+      img.setAttribute("src", uri);
+      button.appendChild(img);
+      button.style.width = buttonWidth + "px";
+      button.setAttribute("title", engine.name);
+
+      button.engineName = engine.name;
+      button.addEventListener("click", this);
+      button.addEventListener("mousemove", this);
+
+      if (engines.length - i <= enginesPerRow - (i % enginesPerRow)) {
+        button.classList.add("last-row");
+      }
+
+      if ((i + 1) % enginesPerRow == 0) {
+        button.classList.add("end-of-row");
+      }
+
+      button.id = ONE_OFF_ID_PREFIX + i;
+      cell.appendChild(button);
+      this._oneOffButtons.push(button);
+    }
+    row.appendChild(cell);
+    this._oneOffsTable.appendChild(row);
+    this._oneOffsTable.hidden = false;
+  },
+
   _sendMsg: function (type, data=null) {
     dispatchEvent(new CustomEvent("ContentSearchClient", {
       detail: {
         type: type,
         data: data,
       },
     }));
   },
 };
 
-return SearchSuggestionUIController;
+return ContentSearchUIController;
 })();
--- a/browser/base/content/newtab/newTab.css
+++ b/browser/base/content/newtab/newTab.css
@@ -325,144 +325,116 @@ input[type=button] {
   background-color: #fff;
   opacity: 0.01;
 }
 
 /* SEARCH */
 #newtab-search-container {
   display: -moz-box;
   position: relative;
-  -moz-box-align: center;
   -moz-box-pack: center;
 }
 
 #newtab-search-container[page-disabled] {
   opacity: 0;
   pointer-events: none;
 }
 
 #newtab-search-form {
   display: -moz-box;
+  position: relative;
+  height: 36px;
   -moz-box-flex: 1;
-  -moz-box-orient: horizontal;
-  -moz-box-align: center;
-  height: 44px; /* 32 + 6 logo top "padding" + 6 logo bottom "padding" */
-  margin: 26px 20px 10px; /* top: 32 - 6 search form top "padding", bottom: 32 - 16 tiles top margin - 6 logo bottom "padding" */
   max-width: 600px; /* 2 * (290 cell width + 10 cell margin) */
 }
 
-#newtab-search-logo {
-  display: -moz-box;
-  width: 38px;
-  height: 38px; /* 26 image height + 6 top "padding" + 6 bottom "padding" */
-  border: 1px solid transparent;
-  -moz-margin-end: 8px;
-  background-repeat: no-repeat;
-  background-position: center;
-  background-image: url(chrome://global/skin/icons/autocomplete-search.svg#search-icon);
-  background-size: 26px 26px;
-}
-
-#newtab-search-logo.magnifier {
-  width: 38px; /* 26 image width + 6 left "padding" + 6 right "padding" */
-  -moz-margin-end: 5px;
-  background-size: 26px;
-  background-image: url("chrome://browser/skin/magnifier.png");
-}
-
-@media not all and (max-resolution: 1dppx) {
-  #newtab-search-logo.magnifier {
-    background-image: url("chrome://browser/skin/magnifier@2x.png");
-  }
-}
-
-#newtab-search-logo[type="logo"] {
-  background-size: 65px 26px;
-  width: 77px; /* 65 image width + 6 left "padding" + 6 right "padding" */
-}
-
-#newtab-search-logo[type="favicon"] {
-  background-size: 16px 16px;
-}
-
-#newtab-search-logo[hidden] {
-  display: none;
-}
-
-#newtab-search-logo[active],
-#newtab-search-logo:hover {
-  background-color: #e9e9e9;
-  border: 1px solid rgb(226, 227, 229);
-  border-radius: 2.5px;
+#newtab-search-icon {
+  border: 1px transparent;
+  padding: 0;
+  margin: 0;
+  width: 36px;
+  height: 36px;
+  background: url("chrome://browser/skin/search-indicator-magnifying-glass.svg") center center no-repeat;
+  position: absolute;
 }
 
 #newtab-search-text {
-  height: 38px; /* same height as #newtab-search-logo */
   -moz-box-flex: 1;
-
-  padding: 0 8px;
+  padding-top: 6px;
+  padding-bottom: 6px;
+  padding-left: 34px;
+  padding-right: 8px;
   background: hsla(0,0%,100%,.9) padding-box;
   border: 1px solid;
+  border-spacing: 0;
+  border-radius: 2px 0 0 2px;
   border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
   box-shadow: 0 1px 0 hsla(210,65%,9%,.02) inset,
               0 0 2px hsla(210,65%,9%,.1) inset,
               0 1px 0 hsla(0,0%,100%,.2);
-  border-radius: 2.5px 0 0 2.5px;
   color: inherit;
 }
 
 #newtab-search-text:-moz-dir(rtl) {
-  border-radius: 0 2.5px 2.5px 0;
+  border-radius: 0 2px 2px 0;
 }
 
+#newtab-search-text[aria-expanded="true"] {
+  border-radius: 2px 0 0 0;
+}
+
+#newtab-search-text[aria-expanded="true"]:-moz-dir(rtl) {
+  border-radius: 0 2px 0 0;
+}
+
+#newtab-search-text[keepfocus],
 #newtab-search-text:focus,
 #newtab-search-text[autofocus] {
   border-color: hsla(206,100%,60%,.6) hsla(206,76%,52%,.6) hsla(204,100%,40%,.6);
 }
 
 #newtab-search-submit {
-  height: 38px; /* same height as #newtab-search-logo */
-  font-size: 13px !important;
-
   -moz-margin-start: -1px;
-  background: linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
-  padding: 0 9px;
+  background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go") center center no-repeat, linear-gradient(hsla(0,0%,100%,.8), hsla(0,0%,100%,.1)) padding-box;
+  padding: 0;
   border: 1px solid;
   border-color: hsla(210,54%,20%,.15) hsla(210,54%,20%,.17) hsla(210,54%,20%,.2);
+  border-radius: 0 2px 2px 0;
   -moz-border-start: 1px solid transparent;
-  border-radius: 0 2.5px 2.5px 0;
   box-shadow: 0 0 2px hsla(0,0%,100%,.5) inset,
               0 1px 0 hsla(0,0%,100%,.2);
   color: inherit;
   cursor: pointer;
   transition-property: background-color, border-color, box-shadow;
   transition-duration: 150ms;
+  width: 50px;
 }
 
 #newtab-search-submit:-moz-dir(rtl) {
-  border-radius: 2.5px 0 0 2.5px;
+  border-radius: 2px 0 0 2px;
 }
 
 #newtab-search-text:focus + #newtab-search-submit,
 #newtab-search-text + #newtab-search-submit:hover,
 #newtab-search-text[autofocus] + #newtab-search-submit {
   border-color: #59b5fc #45a3e7 #3294d5;
   color: white;
 }
 
 #newtab-search-text:focus + #newtab-search-submit,
+#newtab-search-text[keepfocus] + #newtab-search-submit,
 #newtab-search-text[autofocus] + #newtab-search-submit {
-  background-image: linear-gradient(#4cb1ff, #1793e5);
+  background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted") center center no-repeat, linear-gradient(#4cb1ff, #1793e5);
   box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
               0 0 0 1px hsla(0,0%,100%,.1) inset,
               0 1px 0 hsla(210,54%,20%,.03);
 }
 
 #newtab-search-text + #newtab-search-submit:hover {
-  background-image: linear-gradient(#66bdff, #0d9eff);
+  background: url("chrome://browser/skin/search-arrow-go.svg#search-arrow-go-inverted") center center no-repeat, linear-gradient(#4cb1ff, #1793e5);
   box-shadow: 0 1px 0 hsla(0,0%,100%,.2) inset,
               0 0 0 1px hsla(0,0%,100%,.1) inset,
               0 1px 0 hsla(210,54%,20%,.03),
               0 0 4px hsla(206,100%,20%,.2);
 }
 
 #newtab-search-text + #newtab-search-submit:hover:active {
   box-shadow: 0 1px 1px hsla(211,79%,6%,.1) inset,
@@ -539,48 +511,35 @@ input[type=button] {
   display: table-cell;
   border-top: none;
 }
 
 #newtab-customize-title > label {
   cursor: default;
 }
 
-#newtab-customize-panel > .panel-arrowcontainer > .panel-arrowcontent,
-#newtab-search-panel > .panel-arrowcontainer > .panel-arrowcontent {
+#newtab-customize-panel > .panel-arrowcontainer > .panel-arrowcontent {
   padding: 0;
 }
 
-.newtab-customize-panel-item,
-.newtab-search-panel-engine,
-#newtab-search-manage {
+.newtab-customize-panel-item {
   line-height: 25px;
   padding: 15px;
   -moz-padding-start: 40px;
   font-size: 14px;
   cursor: pointer;
   max-width: 300px;
 }
 
-.newtab-customize-panel-item:not(:first-child),
-.newtab-search-panel-engine {
+.newtab-customize-panel-item:not(:first-child) {
   border-top: 1px solid threedshadow;
 }
 
-.newtab-search-panel-engine > image {
-  -moz-margin-end: 8px;
-  width: 16px;
-  height: 16px;
-  list-style-image: url("chrome://mozapps/skin/places/defaultFavicon.png");
-}
-
 .newtab-customize-panel-subitem > label,
 .newtab-customize-panel-item > label,
-.newtab-search-panel-engine > label,
-#newtab-search-manage > label,
 .newtab-customize-complex-option {
   padding: 0;
   margin: 0;
   cursor: pointer;
 }
 
 .newtab-customize-panel-item,
 .newtab-customize-complex-option {
@@ -620,18 +579,17 @@ input[type=button] {
   background-position: 15px 15px;
   color: #171F26;
 }
 
 .newtab-customize-complex-option:hover > .selectable:not([selected]) + .newtab-customize-panel-subitem {
   background-color: #FFFFFF;
 }
 
-.newtab-customize-panel-item[selected],
-.newtab-search-panel-engine[selected] {
+.newtab-customize-panel-item[selected] {
   background: url("chrome://global/skin/menu/shared-menu-check-active.svg") no-repeat transparent;
   background-size: 16px 16px;
   background-position: 15px 15px;
   color: black;
   font-weight: 600;
 }
 
 .newtab-customize-panel-subitem > .checkbox {
@@ -666,17 +624,17 @@ input[type=button] {
 .newtab-customize-panel-superitem {
   line-height: 20px;
   border-bottom: medium none !important;
   padding: 15px 15px 10px 15px;
   -moz-padding-start: 40px;
   border-top: 1px solid threedshadow;
 }
 
-.searchSuggestionTable {
+.contentSearchSuggestionTable {
   font: message-box;
   font-size: 16px;
 }
 
 /**
  * Onboarding styling
  */
 
--- a/browser/base/content/newtab/newTab.xul
+++ b/browser/base/content/newtab/newTab.xul
@@ -1,39 +1,30 @@
 <?xml version="1.0" encoding="UTF-8"?>
 
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
-<?xml-stylesheet href="chrome://browser/content/searchSuggestionUI.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/contentSearchUI.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/content/newtab/newTab.css" type="text/css"?>
 <?xml-stylesheet href="chrome://browser/skin/newtab/newTab.css" type="text/css"?>
 
 <!DOCTYPE window [
   <!ENTITY % newTabDTD SYSTEM "chrome://browser/locale/newTab.dtd">
   %newTabDTD;
-  <!ENTITY % searchBarDTD SYSTEM "chrome://browser/locale/searchbar.dtd">
-  %searchBarDTD;
   <!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
   %browserDTD;
 ]>
 
 <xul:window id="newtab-window" xmlns="http://www.w3.org/1999/xhtml"
             xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
             title="&newtab.pageTitle;">
 
-  <xul:panel id="newtab-search-panel" orient="vertical" type="arrow"
-             noautohide="true" hidden="true">
-    <xul:hbox id="newtab-search-manage">
-      <xul:label>&changeSearchSettings.button;</xul:label>
-    </xul:hbox>
-  </xul:panel>
-
   <div class="newtab-customize-panel-container">
     <div id="newtab-customize-panel" orient="vertical">
         <div id="newtab-customize-panel-anchor"></div>
         <div id="newtab-customize-title" class="newtab-customize-panel-item">
             <label>&newtab.customize.cog.title2;</label>
         </div>
 
         <div class="newtab-customize-complex-option">
@@ -103,23 +94,23 @@
                       class="newtab-undo-button" />
           <xul:toolbarbutton id="newtab-undo-close-button" tabindex="-1"
                              class="close-icon tabbable"
                              tooltiptext="&newtab.undo.closeTooltip;" />
         </div>
       </div>
 
       <div id="newtab-search-container">
-        <form id="newtab-search-form" name="searchForm">
-          <div id="newtab-search-logo"/>
+        <div id="newtab-search-form">
+          <div id="newtab-search-icon"/>
           <input type="text" name="q" value="" id="newtab-search-text"
-                 maxlength="256" dir="auto"/>
-          <input id="newtab-search-submit" type="submit"
-                 value="&searchEndCap.label;"/>
-        </form>
+                 aria-label="&contentSearchInput.label;" maxlength="256" dir="auto"/>
+          <input id="newtab-search-submit" type="button" value=""
+                 aria-label="&contentSearchSubmit.label;"/>
+        </div>
       </div>
 
       <div id="newtab-horizontal-margin">
         <div class="newtab-side-margin"/>
 
         <div id="newtab-grid">
         </div>
 
@@ -128,12 +119,12 @@
 
       <div id="newtab-margin-bottom"/>
 
     </div>
     <input id="newtab-customize-button" type="button" title="&newtab.customize.title;"/>
   </div>
 
   <xul:script type="text/javascript;version=1.8"
-              src="chrome://browser/content/searchSuggestionUI.js"/>
+              src="chrome://browser/content/contentSearchUI.js"/>
   <xul:script type="text/javascript;version=1.8"
               src="chrome://browser/content/newtab/newTab.js"/>
 </xul:window>
--- a/browser/base/content/newtab/search.js
+++ b/browser/base/content/newtab/search.js
@@ -1,261 +1,15 @@
 #ifdef 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/. */
 #endif
 
 let gSearch = {
-
-  currentEngineName: null,
-
-  get useNewUI() {
-    let newUI = Services.prefs.getBoolPref("browser.search.showOneOffButtons");
-    delete this.useNewUI;
-    this.useNewUI = newUI;
-    return newUI;
-  },
-
   init: function () {
-    for (let idSuffix of this._nodeIDSuffixes) {
-      this._nodes[idSuffix] =
-        document.getElementById("newtab-search-" + idSuffix);
-    }
-
-    if (this.useNewUI) {
-      this._nodes.logo.classList.add("magnifier");
-    }
-
-    window.addEventListener("ContentSearchService", this);
-    this._send("GetState");
-  },
-
-  showPanel: function () {
-    let panel = this._nodes.panel;
-    let logo = this._nodes.logo;
-    panel.hidden = false;
-    panel.openPopup(logo);
-    logo.setAttribute("active", "true");
-    panel.addEventListener("popuphidden", function onHidden() {
-      panel.removeEventListener("popuphidden", onHidden);
-      panel.hidden = true;
-      logo.removeAttribute("active");
-    });
-  },
-
-  search: function (event) {
-    if (event) {
-      event.preventDefault();
-    }
-    let searchText = this._nodes.text;
-    let searchStr = searchText.value;
-    if (this.currentEngineName && searchStr.length) {
-      let useNewTab = event && event.button == 1;
-      let eventData = {
-        engineName: this.currentEngineName,
-        searchString: searchStr,
-        whence: "newtab",
-        originalEvent: {
-          target: {
-            ownerDocument: null
-          },
-          shiftKey: event.shiftKey,
-          ctrlKey: event.ctrlKey,
-          metaKey: event.metaKey,
-          altKey: event.altKey,
-          button: event.button,
-        },
-      }
-
-      if (searchText.hasAttribute("selection-index")) {
-        eventData.selection = {
-          index: searchText.getAttribute("selection-index"),
-          kind: searchText.getAttribute("selection-kind")
-        };
-      }
-
-      this._send("Search", eventData);
-    }
-    this._suggestionController.addInputValueToFormHistory();
-  },
-
-  manageEngines: function () {
-    this._nodes.panel.hidePopup();
-    this._send("ManageEngines");
-  },
-
-  handleEvent: function (event) {
-    let methodName = "on" + event.detail.type;
-    if (this.hasOwnProperty(methodName)) {
-      this[methodName](event.detail.data);
-    }
-  },
-
-  onState: function (data) {
-    this._newEngines = data.engines;
-    this._setCurrentEngine(data.currentEngine);
-    this._initWhenInitalStateReceived();
-  },
-
-  onCurrentState: function (data) {
-    if (this._initialStateReceived) {
-      this._newEngines = data.engines;
-      this._setCurrentEngine(data.currentEngine);
-    }
-  },
-
-  onCurrentEngine: function (engineName) {
-    if (this._initialStateReceived) {
-      this._nodes.panel.hidePopup();
-      this._setCurrentEngine(engineName);
-    }
-  },
-
-  onFocusInput: function () {
-    this._nodes.text.focus();
-  },
-
-  _nodeIDSuffixes: [
-    "form",
-    "logo",
-    "manage",
-    "panel",
-    "text",
-  ],
-
-  _nodes: {},
-
-  _initWhenInitalStateReceived: function () {
-    this._nodes.form.addEventListener("submit", e => this.search(e));
-    this._nodes.logo.addEventListener("click", e => this.showPanel());
-    this._nodes.manage.addEventListener("click", e => this.manageEngines());
-    this._nodes.panel.addEventListener("popupshowing", e => this._setUpPanel());
-    this._initialStateReceived = true;
-    this._initWhenInitalStateReceived = function () {};
-  },
-
-  _send: function (type, data=null) {
-    window.dispatchEvent(new CustomEvent("ContentSearchClient", {
-      detail: {
-        type: type,
-        data: data,
-      },
-    }));
-  },
-
-  _setUpPanel: function () {
-    // The new search UI only contains the "manage" engine entry in the panel
-    if (this.useNewUI) {
-      return;
-    }
-
-    // Build the panel if necessary.
-    if (this._newEngines) {
-      this._buildPanel(this._newEngines);
-      delete this._newEngines;
-    }
-
-    // Set the selected states of the engines.
-    let panel = this._nodes.panel;
-    for (let box of panel.childNodes) {
-      if (box.getAttribute("engine") == this.currentEngineName) {
-        box.setAttribute("selected", "true");
-      }
-      else {
-        box.removeAttribute("selected");
-      }
-    }
-  },
-
-  _buildPanel: function (engines) {
-    let panel = this._nodes.panel;
-
-    // Empty the panel except for the Manage Engines row.
-    let i = 0;
-    while (i < panel.childNodes.length) {
-      let node = panel.childNodes[i];
-      if (node != this._nodes.manage) {
-        panel.removeChild(node);
-      }
-      else {
-        i++;
-      }
-    }
-
-    // Add all the engines.
-    for (let engine of engines) {
-      panel.insertBefore(this._makePanelEngine(panel, engine),
-                         this._nodes.manage);
-    }
-  },
-
-  // Converts favicon array buffer into data URI of the right size and dpi.
-  _getFaviconURIFromBuffer: function (buffer) {
-    let blob = new Blob([buffer]);
-    let dpiSize = Math.round(16 * window.devicePixelRatio);
-    let sizeStr = dpiSize + "," + dpiSize;
-    return URL.createObjectURL(blob) + "#-moz-resolution=" + sizeStr;
-  },
-
-  _makePanelEngine: function (panel, engine) {
-    let box = document.createElementNS(XUL_NAMESPACE, "hbox");
-    box.className = "newtab-search-panel-engine";
-    box.setAttribute("engine", engine.name);
-
-    box.addEventListener("click", () => {
-      this._send("SetCurrentEngine", engine.name);
-      panel.hidePopup();
-      this._nodes.text.focus();
-    });
-
-    let image = document.createElementNS(XUL_NAMESPACE, "image");
-    if (engine.iconBuffer) {
-      let uri = this._getFaviconURIFromBuffer(engine.iconBuffer);
-      image.setAttribute("src", uri);
-    }
-    box.appendChild(image);
-
-    let label = document.createElementNS(XUL_NAMESPACE, "label");
-    label.setAttribute("value", engine.name);
-    box.appendChild(label);
-
-    return box;
-  },
-
-  _setCurrentEngine: function (engine) {
-    this.currentEngineName = engine.name;
-
-    if (!this.useNewUI) {
-      let type = "";
-      let uri;
-      let logoBuf = window.devicePixelRatio >= 2 ?
-                    engine.logo2xBuffer || engine.logoBuffer :
-                    engine.logoBuffer || engine.logo2xBuffer;
-      if (logoBuf) {
-        uri = URL.createObjectURL(new Blob([logoBuf]));
-        type = "logo";
-      }
-      else if (engine.iconBuffer) {
-        uri = this._getFaviconURIFromBuffer(engine.iconBuffer);
-        type = "favicon";
-      }
-      this._nodes.logo.setAttribute("type", type);
-
-      if (uri) {
-        this._nodes.logo.style.backgroundImage = "url(" + uri + ")";
-      }
-      else {
-        this._nodes.logo.style.backgroundImage = "";
-      }
-      this._nodes.text.placeholder = engine.placeholder;
-    }
-
-    // Set up the suggestion controller.
-    if (!this._suggestionController) {
-      let parent = document.getElementById("newtab-scrollbox");
-      this._suggestionController =
-        new SearchSuggestionUIController(this._nodes.text, parent,
-                                         event => this.search(event));
-    }
-    this._suggestionController.engineName = engine.name;
+    document.getElementById("newtab-search-submit")
+            .addEventListener("click", e => this._contentSearchController.search(e));
+    let textbox = document.getElementById("newtab-search-text");
+    this._contentSearchController =
+      new ContentSearchUIController(textbox, textbox.parentNode, "newtab", "newtab");
   },
 };
--- a/browser/base/content/tab-content.js
+++ b/browser/base/content/tab-content.js
@@ -109,22 +109,16 @@ let AboutHomeListener = {
   handleEvent: function(aEvent) {
     if (!this.isAboutHome) {
       return;
     }
     switch (aEvent.type) {
       case "AboutHomeLoad":
         this.onPageLoad();
         break;
-      case "AboutHomeSearchEvent":
-        this.onSearch(aEvent);
-        break;
-      case "AboutHomeSearchPanel":
-        this.onOpenSearchPanel(aEvent);
-        break;
       case "click":
         this.onClick(aEvent);
         break;
       case "pagehide":
         this.onPageHide(aEvent);
         break;
     }
   },
@@ -132,57 +126,49 @@ let AboutHomeListener = {
   receiveMessage: function(aMessage) {
     if (!this.isAboutHome) {
       return;
     }
     switch (aMessage.name) {
       case "AboutHome:Update":
         this.onUpdate(aMessage.data);
         break;
-      case "AboutHome:FocusInput":
-        this.onFocusInput();
-        break;
     }
   },
 
   onUpdate: function(aData) {
     let doc = content.document;
     if (aData.showRestoreLastSession && !PrivateBrowsingUtils.isContentWindowPrivate(content))
       doc.getElementById("launcher").setAttribute("session", "true");
 
     // Inject search engine and snippets URL.
     let docElt = doc.documentElement;
-    // set the following attributes BEFORE searchEngineName, which triggers to
-    // show the snippets when it's set.
+    // Set snippetsVersion last, which triggers to show the snippets when it's set.
     docElt.setAttribute("snippetsURL", aData.snippetsURL);
     if (aData.showKnowYourRights)
       docElt.setAttribute("showKnowYourRights", "true");
     docElt.setAttribute("snippetsVersion", aData.snippetsVersion);
-    docElt.setAttribute("searchEngineName", aData.defaultEngineName);
   },
 
   onPageLoad: function() {
     let doc = content.document;
     if (doc.documentElement.hasAttribute("hasBrowserHandlers")) {
       return;
     }
 
     doc.documentElement.setAttribute("hasBrowserHandlers", "true");
     addMessageListener("AboutHome:Update", this);
-    addMessageListener("AboutHome:FocusInput", this);
     addEventListener("click", this, true);
     addEventListener("pagehide", this, true);
 
     if (!Services.prefs.getBoolPref("browser.search.showOneOffButtons")) {
       doc.documentElement.setAttribute("searchUIConfiguration", "oldsearchui");
     }
 
     sendAsyncMessage("AboutHome:RequestUpdate");
-    doc.addEventListener("AboutHomeSearchEvent", this, true, true);
-    doc.addEventListener("AboutHomeSearchPanel", this, true, true);
   },
 
   onClick: function(aEvent) {
     if (!aEvent.isTrusted || // Don't trust synthetic events
         aEvent.button == 2 || aEvent.target.localName != "button") {
       return;
     }
 
@@ -223,49 +209,30 @@ let AboutHomeListener = {
 
       case "sync":
         sendAsyncMessage("AboutHome:Sync");
         break;
 
       case "settings":
         sendAsyncMessage("AboutHome:Settings");
         break;
-
-      case "searchIcon":
-        sendAsyncMessage("AboutHome:OpenSearchPanel", null, { anchor: originalTarget });
-        break;
     }
   },
 
   onPageHide: function(aEvent) {
     if (aEvent.target.defaultView.frameElement) {
       return;
     }
     removeMessageListener("AboutHome:Update", this);
     removeEventListener("click", this, true);
     removeEventListener("pagehide", this, true);
     if (aEvent.target.documentElement) {
       aEvent.target.documentElement.removeAttribute("hasBrowserHandlers");
     }
   },
-
-  onSearch: function(aEvent) {
-    sendAsyncMessage("AboutHome:Search", { searchData: aEvent.detail });
-  },
-
-  onOpenSearchPanel: function(aEvent) {
-    sendAsyncMessage("AboutHome:OpenSearchPanel");
-  },
-
-  onFocusInput: function () {
-    let searchInput = content.document.getElementById("searchText");
-    if (searchInput) {
-      searchInput.focus();
-    }
-  },
 };
 AboutHomeListener.init(this);
 
 let AboutPrivateBrowsingListener = {
   init(chromeGlobal) {
     chromeGlobal.addEventListener("AboutPrivateBrowsingOpenWindow", this,
                                   false, true);
     chromeGlobal.addEventListener("AboutPrivateBrowsingEnableTrackingProtection", this,
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -75,16 +75,17 @@ support-files =
   parsingTestHelpers.jsm
   pinning_headers.sjs
   pinning_reports.sjs
   popup_blocker.html
   print_postdata.sjs
   redirect_bug623155.sjs
   searchSuggestionEngine.sjs
   searchSuggestionEngine.xml
+  searchSuggestionEngine2.xml
   subtst_contextmenu.html
   test-mixedcontent-securityerrors.html
   test_bug435035.html
   test_bug462673.html
   test_bug628179.html
   test_bug839103.html
   test_bug959531.html
   test_process_flags_chrome.html
@@ -365,20 +366,20 @@ skip-if = buildapp == 'mulet' || e10s # 
 [browser_save_private_link_perwindowpb.js]
 skip-if = buildapp == 'mulet' || e10s # e10s: Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly
 [browser_save_link_when_window_navigates.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly
 [browser_save_video.js]
 skip-if = buildapp == 'mulet'
 [browser_save_video_frame.js]
 [browser_scope.js]
-[browser_searchSuggestionUI.js]
+[browser_contentSearchUI.js]
 support-files =
-  searchSuggestionUI.html
-  searchSuggestionUI.js
+  contentSearchUI.html
+  contentSearchUI.js
 [browser_selectpopup.js]
 run-if = e10s
 [browser_selectTabAtIndex.js]
 [browser_ssl_error_reports.js]
 [browser_star_hsts.js]
 [browser_subframe_favicons_not_used.js]
 [browser_syncui.js]
 [browser_tabDrop.js]
--- a/browser/base/content/test/general/browser_aboutHome.js
+++ b/browser/base/content/test/general/browser_aboutHome.js
@@ -96,53 +96,45 @@ let gTests = [
       // Health Report disabled, or no SearchesProvider.
       return Promise.resolve();
     }
 
     let engine = yield promiseNewEngine("searchSuggestionEngine.xml");
     // Make this actually work in healthreport by giving it an ID:
     engine.wrappedJSObject._identifier = 'org.mozilla.testsearchsuggestions';
 
-    let promise = promiseBrowserAttributes(gBrowser.selectedTab);
+    let p = promiseContentSearchChange(engine.name);
     Services.search.currentEngine = engine;
-    yield promise;
+    yield p;
 
     let numSearchesBefore = 0;
     let searchEventDeferred = Promise.defer();
     let doc = gBrowser.contentDocument;
-    let engineName = doc.documentElement.getAttribute("searchEngineName");
+    let engineName = gBrowser.contentWindow.wrappedJSObject.gContentSearchController.defaultEngine.name;
     is(engine.name, engineName, "Engine name in DOM should match engine we just added");
-    let mm = gBrowser.selectedBrowser.messageManager;
-
-    mm.loadFrameScript(TEST_CONTENT_HELPER, false);
-
-    mm.addMessageListener("AboutHomeTest:CheckRecordedSearch", function (msg) {
-      let data = JSON.parse(msg.data);
-      is(data.engineName, engineName, "Detail is search engine name");
-
-      getNumberOfSearches(engineName).then(num => {
-        is(num, numSearchesBefore + 1, "One more search recorded.");
-        searchEventDeferred.resolve();
-      });
-    });
 
     // Get the current number of recorded searches.
     let searchStr = "a search";
     getNumberOfSearches(engineName).then(num => {
       numSearchesBefore = num;
 
       info("Perform a search.");
       doc.getElementById("searchText").value = searchStr;
       doc.getElementById("searchSubmit").click();
     });
 
     let expectedURL = Services.search.currentEngine.
                       getSubmission(searchStr, null, "homepage").
                       uri.spec;
-    let loadPromise = waitForDocLoadAndStopIt(expectedURL);
+    let loadPromise = waitForDocLoadAndStopIt(expectedURL).then(() => {
+      getNumberOfSearches(engineName).then(num => {
+        is(num, numSearchesBefore + 1, "One more search recorded.");
+        searchEventDeferred.resolve();
+      });
+    });
 
     try {
       yield Promise.all([searchEventDeferred.promise, loadPromise]);
     } catch (ex) {
       Cu.reportError(ex);
       ok(false, "An error occurred waiting for the search to be performed: " + ex);
     } finally {
       try {
@@ -231,67 +223,58 @@ let gTests = [
 
     Services.prefs.clearUserPref("browser.rights.override");
   }
 },
 
 {
   desc: "Check POST search engine support",
   setup: function() {},
-  run: function()
+  run: function* ()
   {
     let deferred = Promise.defer();
     let currEngine = Services.search.defaultEngine;
-    let searchObserver = function search_observer(aSubject, aTopic, aData) {
+    let searchObserver = Task.async(function* search_observer(aSubject, aTopic, aData) {
       let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
       info("Observer: " + aData + " for " + engine.name);
 
       if (aData != "engine-added")
         return;
 
       if (engine.name != "POST Search")
         return;
 
       // Ready to execute the tests!
       let needle = "Search for something awesome.";
       let document = gBrowser.selectedBrowser.contentDocument;
       let searchText = document.getElementById("searchText");
 
-      // We're about to change the search engine. Once the change has
-      // propagated to the about:home content, we want to perform a search.
-      let mutationObserver = new MutationObserver(function (mutations) {
-        for (let mutation of mutations) {
-          if (mutation.attributeName == "searchEngineName") {
-            searchText.value = needle;
-            searchText.focus();
-            EventUtils.synthesizeKey("VK_RETURN", {});
-          }
-        }
-      });
-      mutationObserver.observe(document.documentElement, { attributes: true });
+      let p = promiseContentSearchChange(engine.name);
+      Services.search.defaultEngine = engine;
+      yield p;
 
-      // Change the search engine, triggering the observer above.
-      Services.search.defaultEngine = engine;
+      searchText.value = needle;
+      searchText.focus();
+      EventUtils.synthesizeKey("VK_RETURN", {});
 
       registerCleanupFunction(function() {
-        mutationObserver.disconnect();
         Services.search.removeEngine(engine);
         Services.search.defaultEngine = currEngine;
       });
 
 
       // When the search results load, check them for correctness.
       waitForLoad(function() {
         let loadedText = gBrowser.contentDocument.body.textContent;
         ok(loadedText, "search page loaded");
         is(loadedText, "searchterms=" + escape(needle.replace(/\s/g, "+")),
            "Search text should arrive correctly");
         deferred.resolve();
       });
-    };
+    });
     Services.obs.addObserver(searchObserver, "browser-search-engine-modified", false);
     registerCleanupFunction(function () {
       Services.obs.removeObserver(searchObserver, "browser-search-engine-modified");
     });
     Services.search.addEngine("http://test:80/browser/browser/base/content/test/general/POSTSearchEngine.xml",
                               Ci.nsISearchEngine.DATA_XML, null, false);
     return deferred.promise;
   }
@@ -330,31 +313,30 @@ let gTests = [
     });
 
     browser.loadURI("https://example.com/browser/browser/base/content/test/general/test_bug959531.html");
     return deferred.promise;
   }
 },
 
 {
-  // See browser_searchSuggestionUI.js for comprehensive content search
-  // suggestion UI tests.
+  // See browser_contentSearchUI.js for comprehensive content search UI tests.
   desc: "Search suggestion smoke test",
   setup: function() {},
   run: function()
   {
     return Task.spawn(function* () {
       // Add a test engine that provides suggestions and switch to it.
       let engine = yield promiseNewEngine("searchSuggestionEngine.xml");
-      let promise = promiseBrowserAttributes(gBrowser.selectedTab);
+      let p = promiseContentSearchChange(engine.name);
       Services.search.currentEngine = engine;
-      yield promise;
+      yield p;
 
       // Avoid intermittent failures.
-      gBrowser.contentWindow.wrappedJSObject.gSearchSuggestionController.remoteTimeout = 5000;
+      gBrowser.contentWindow.wrappedJSObject.gContentSearchController.remoteTimeout = 5000;
 
       // Type an X in the search input.
       let input = gBrowser.contentDocument.getElementById("searchText");
       input.focus();
       EventUtils.synthesizeKey("x", {});
 
       // Wait for the search suggestions to become visible.
       let table =
@@ -396,19 +378,21 @@ let gTests = [
           string: "x",
           clauses: [
             { length: 1, attr: EventUtils.COMPOSITION_ATTR_RAW_CLAUSE }
           ]
         },
         caret: { start: 1, length: 0 }
       }, gBrowser.contentWindow);
 
+      let searchController =
+        gBrowser.contentWindow.wrappedJSObject.gContentSearchController;
+
       // Wait for the search suggestions to become visible.
-      let table =
-        gBrowser.contentDocument.getElementById("searchSuggestionTable");
+      let table = searchController._suggestionsList;
       let deferred = Promise.defer();
       let observer = new MutationObserver(() => {
         if (input.getAttribute("aria-expanded") == "true") {
           observer.disconnect();
           ok(!table.hidden, "Search suggestion table unhidden");
           deferred.resolve();
         }
       });
@@ -419,19 +403,24 @@ let gTests = [
       yield deferred.promise;
 
       // Click the second suggestion.
       let expectedURL = Services.search.currentEngine.
                         getSubmission("xbar", null, "homepage").
                         uri.spec;
       let loadPromise = waitForDocLoadAndStopIt(expectedURL);
       let row = table.children[1];
-      EventUtils.sendMouseEvent({ type: "mousedown" }, row, gBrowser.contentWindow);
+      // ContentSearchUIController looks at the current selectedIndex when
+      // performing a search. Synthesizing the mouse event on the suggestion
+      // doesn't actually mouseover the suggestion and trigger it to be flagged
+      // as selected, so we manually select it first.
+      searchController.selectedIndex = 1;
+      EventUtils.synthesizeMouseAtCenter(row, {button: 0}, gBrowser.contentWindow);
       yield loadPromise;
-      ok(input.value == "xbar", "Suggestion is selected");
+      ok(input.value == "x", "Input value did not change");
     });
   }
 },
 {
   desc: "Cmd+k should focus the search box in the page when the search box in the toolbar is absent",
   setup: function () {
     // Remove the search bar from toolbar
     CustomizableUI.removeWidgetFromArea("search-container");
@@ -472,36 +461,16 @@ let gTests = [
   run: Task.async(function* () {
     let syncButton = gBrowser.selectedBrowser.contentDocument.getElementById("sync");
     yield EventUtils.synthesizeMouseAtCenter(syncButton, {}, gBrowser.contentWindow);
 
     yield promiseTabLoadEvent(gBrowser.selectedTab, null, "load");
     is(gBrowser.currentURI.spec, "about:accounts?entrypoint=abouthome",
       "Entry point should be `abouthome`.");
   })
-},
-{
-  desc: "Clicking the icon should open the popup",
-  setup: function () {},
-  run: Task.async(function* () {
-    let doc = gBrowser.selectedBrowser.contentDocument;
-    let searchIcon = doc.getElementById("searchIcon");
-    let panel = window.document.getElementById("abouthome-search-panel");
-
-    info("Waiting for popup to open");
-    EventUtils.synthesizeMouseAtCenter(searchIcon, {}, gBrowser.selectedBrowser.contentWindow);
-    yield promiseWaitForEvent(panel, "popupshown");
-    info("Saw popup open");
-
-    let promise = promisePrefsOpen();
-    let item = window.document.getElementById("abouthome-search-panel-manage");
-    EventUtils.synthesizeMouseAtCenter(item, {});
-
-    yield promise;
-  })
 }
 
 ];
 
 function test()
 {
   waitForExplicitFinish();
   requestLongerTimeout(2);
@@ -572,48 +541,16 @@ function promiseSetupSnippetsMap(aTab, a
       aSetupFn(aSnippetsMap);
       deferred.resolve(aSnippetsMap);
     });
   }, true, true);
   return deferred.promise;
 }
 
 /**
- * Waits for the attributes being set by browser.js.
- *
- * @param aTab
- *        The tab containing about:home.
- * @return {Promise} resolved when the attributes are ready.
- */
-function promiseBrowserAttributes(aTab)
-{
-  let deferred = Promise.defer();
-
-  let docElt = aTab.linkedBrowser.contentDocument.documentElement;
-  let observer = new MutationObserver(function (mutations) {
-    for (let mutation of mutations) {
-      info("Got attribute mutation: " + mutation.attributeName +
-                                    " from " + mutation.oldValue);
-      // Now we just have to wait for the last attribute.
-      if (mutation.attributeName == "searchEngineName") {
-        info("Remove attributes observer");
-        observer.disconnect();
-        // Must be sure to continue after the page mutation observer.
-        executeSoon(function() deferred.resolve());
-        break;
-      }
-    }
-  });
-  info("Add attributes observer");
-  observer.observe(docElt, { attributes: true });
-
-  return deferred.promise;
-}
-
-/**
  * Retrieves the number of about:home searches recorded for the current day.
  *
  * @param aEngineName
  *        name of the setup search engine.
  *
  * @return {Promise} Returns a promise resolving to the number of searches.
  */
 function getNumberOfSearches(aEngineName) {
@@ -706,16 +643,28 @@ let promisePrefsOpen = Task.async(functi
             });
           });
         }
       });
     });
   }
 });
 
+function promiseContentSearchChange(newEngineName) {
+  return new Promise(resolve => {
+    content.addEventListener("ContentSearchService", function listener(aEvent) {
+      if (aEvent.detail.type == "CurrentState" &&
+          gBrowser.contentWindow.wrappedJSObject.gContentSearchController.defaultEngine.name == newEngineName) {
+        content.removeEventListener("ContentSearchService", listener);
+        resolve();
+      }
+    });
+  });
+}
+
 function promiseNewEngine(basename) {
   info("Waiting for engine to be added: " + basename);
   let addDeferred = Promise.defer();
   let url = getRootDirectory(gTestPath) + basename;
   Services.search.addEngine(url, Ci.nsISearchEngine.TYPE_MOZSEARCH, "", false, {
     onSuccess: function (engine) {
       info("Search engine added: " + basename);
       registerCleanupFunction(() => {
rename from browser/base/content/test/general/browser_searchSuggestionUI.js
rename to browser/base/content/test/general/browser_contentSearchUI.js
--- a/browser/base/content/test/general/browser_searchSuggestionUI.js
+++ b/browser/base/content/test/general/browser_contentSearchUI.js
@@ -1,16 +1,18 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-const TEST_PAGE_BASENAME = "searchSuggestionUI.html";
-const TEST_CONTENT_SCRIPT_BASENAME = "searchSuggestionUI.js";
+const TEST_PAGE_BASENAME = "contentSearchUI.html";
+const TEST_CONTENT_SCRIPT_BASENAME = "contentSearchUI.js";
+const TEST_ENGINE_PREFIX = "browser_searchSuggestionEngine";
 const TEST_ENGINE_BASENAME = "searchSuggestionEngine.xml";
+const TEST_ENGINE_2_BASENAME = "searchSuggestionEngine2.xml";
 
-const TEST_MSG = "SearchSuggestionUIControllerTest";
+const TEST_MSG = "ContentSearchUIControllerTest";
 
 add_task(function* emptyInput() {
   yield setUp();
 
   let state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
   state = yield msg("key", "VK_BACK_SPACE");
@@ -26,233 +28,406 @@ add_task(function* blur() {
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
   state = yield msg("blur");
   checkState(state, "x", [], -1);
 
   yield msg("reset");
 });
 
-add_task(function* arrowKeys() {
+add_task(function* upDownKeys() {
   yield setUp();
 
   let state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
   // Cycle down the suggestions starting from no selection.
   state = yield msg("key", "VK_DOWN");
   checkState(state, "xfoo", ["xfoo", "xbar"], 0);
 
   state = yield msg("key", "VK_DOWN");
   checkState(state, "xbar", ["xfoo", "xbar"], 1);
 
   state = yield msg("key", "VK_DOWN");
+  checkState(state, "x", ["xfoo", "xbar"], 2);
+
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "x", ["xfoo", "xbar"], 3);
+
+  state = yield msg("key", "VK_DOWN");
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
   // Cycle up starting from no selection.
   state = yield msg("key", "VK_UP");
+  checkState(state, "x", ["xfoo", "xbar"], 3);
+
+  state = yield msg("key", "VK_UP");
+  checkState(state, "x", ["xfoo", "xbar"], 2);
+
+  state = yield msg("key", "VK_UP");
   checkState(state, "xbar", ["xfoo", "xbar"], 1);
 
   state = yield msg("key", "VK_UP");
   checkState(state, "xfoo", ["xfoo", "xbar"], 0);
 
   state = yield msg("key", "VK_UP");
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
   yield msg("reset");
 });
 
-// The right arrow and return key function the same.
-function rightArrowOrReturn(keyName) {
-  return function* rightArrowOrReturnTest() {
-    yield setUp();
+add_task(function* rightLeftKeys() {
+  yield setUp();
+
+  let state = yield msg("key", { key: "x", waitForSuggestions: true });
+  checkState(state, "x", ["xfoo", "xbar"], -1);
 
-    let state = yield msg("key", { key: "x", waitForSuggestions: true });
-    checkState(state, "x", ["xfoo", "xbar"], -1);
+  state = yield msg("key", "VK_LEFT");
+  checkState(state, "x", ["xfoo", "xbar"], -1);
+
+  state = yield msg("key", "VK_LEFT");
+  checkState(state, "x", ["xfoo", "xbar"], -1);
 
-    state = yield msg("key", "VK_DOWN");
-    checkState(state, "xfoo", ["xfoo", "xbar"], 0);
+  state = yield msg("key", "VK_RIGHT");
+  checkState(state, "x", ["xfoo", "xbar"], -1);
+
+  state = yield msg("key", "VK_RIGHT");
+  checkState(state, "x", [], -1);
 
-    // This should make the xfoo suggestion sticky.  To make sure it sticks,
-    // trigger suggestions again and cycle through them by pressing Down until
-    // nothing is selected again.
-    state = yield msg("key", keyName);
-    checkState(state, "xfoo", [], -1);
+  state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+  checkState(state, "x", ["xfoo", "xbar"], -1);
+
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xfoo", ["xfoo", "xbar"], 0);
 
-    state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
-    checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
+  // This should make the xfoo suggestion sticky.  To make sure it sticks,
+  // trigger suggestions again and cycle through them by pressing Down until
+  // nothing is selected again.
+  state = yield msg("key", "VK_RIGHT");
+  checkState(state, "xfoo", [], 0);
 
-    state = yield msg("key", "VK_DOWN");
-    checkState(state, "xfoofoo", ["xfoofoo", "xfoobar"], 0);
+  state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+  checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
 
-    state = yield msg("key", "VK_DOWN");
-    checkState(state, "xfoobar", ["xfoofoo", "xfoobar"], 1);
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xfoofoo", ["xfoofoo", "xfoobar"], 0);
 
-    state = yield msg("key", "VK_DOWN");
-    checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xfoobar", ["xfoofoo", "xfoobar"], 1);
+
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xfoo", ["xfoofoo", "xfoobar"], 2);
 
-    yield msg("reset");
-  };
-}
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xfoo", ["xfoofoo", "xfoobar"], 3);
 
-add_task(rightArrowOrReturn("VK_RIGHT"));
-add_task(rightArrowOrReturn("VK_RETURN"));
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
+
+  yield msg("reset");
+});
 
 add_task(function* mouse() {
   yield setUp();
 
   let state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
-  // Mouse over the first suggestion.
-  state = yield msg("mousemove", 0);
-  checkState(state, "x", ["xfoo", "xbar"], 0);
-
-  // Mouse over the second suggestion.
-  state = yield msg("mousemove", 1);
-  checkState(state, "x", ["xfoo", "xbar"], 1);
+  for (let i = 0; i < 4; ++i) {
+    state = yield msg("mousemove", i);
+    checkState(state, "x", ["xfoo", "xbar"], i);
+  }
 
-  // Click the second suggestion.  This should make it sticky.  To make sure it
-  // sticks, trigger suggestions again and cycle through them by pressing Down
-  // until nothing is selected again.
-  state = yield msg("mousedown", 1);
-  checkState(state, "xbar", [], -1);
-
-  state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
-  checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);
-
-  state = yield msg("key", "VK_DOWN");
-  checkState(state, "xbarfoo", ["xbarfoo", "xbarbar"], 0);
-
-  state = yield msg("key", "VK_DOWN");
-  checkState(state, "xbarbar", ["xbarfoo", "xbarbar"], 1);
-
-  state = yield msg("key", "VK_DOWN");
-  checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);
+  state = yield msg("mousemove", -1);
+  checkState(state, "x", ["xfoo", "xbar"], -1);
 
   yield msg("reset");
 });
 
 add_task(function* formHistory() {
   yield setUp();
 
   // Type an X and add it to form history.
   let state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
-  yield msg("addInputValueToFormHistory");
-
   // Wait for Satchel to say it's been added to form history.
   let deferred = Promise.defer();
   Services.obs.addObserver(function onAdd(subj, topic, data) {
     if (data == "formhistory-add") {
       Services.obs.removeObserver(onAdd, "satchel-storage-changed");
       executeSoon(() => deferred.resolve());
     }
   }, "satchel-storage-changed", false);
-  yield deferred.promise;
+  yield Promise.all([msg("addInputValueToFormHistory"), deferred.promise]);
 
   // Reset the input.
   state = yield msg("reset");
   checkState(state, "", [], -1);
 
   // Type an X again.  The form history entry should appear.
   state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
              -1);
 
   // Select the form history entry and delete it.
   state = yield msg("key", "VK_DOWN");
   checkState(state, "x", [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
              0);
 
-  state = yield msg("key", "VK_DELETE");
-  checkState(state, "x", ["xfoo", "xbar"], -1);
-
   // Wait for Satchel.
   deferred = Promise.defer();
   Services.obs.addObserver(function onRemove(subj, topic, data) {
     if (data == "formhistory-remove") {
       Services.obs.removeObserver(onRemove, "satchel-storage-changed");
       executeSoon(() => deferred.resolve());
     }
   }, "satchel-storage-changed", false);
+
+  state = yield msg("key", "VK_DELETE");
+  checkState(state, "x", ["xfoo", "xbar"], -1);
+
   yield deferred.promise;
 
   // Reset the input.
   state = yield msg("reset");
   checkState(state, "", [], -1);
 
   // Type an X again.  The form history entry should still be gone.
   state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
   yield msg("reset");
 });
 
-add_task(function* composition() {
+add_task(function* search() {
+  yield setUp();
+
+  let modifiers = {};
+  ["altKey", "ctrlKey", "metaKey", "shiftKey"].forEach(k => modifiers[k] = true);
+
+  // Test typing a query and pressing enter.
+  let p = msg("waitForSearch");
+  yield msg("key", { key: "x", waitForSuggestions: true });
+  yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
+  let mesg = yield p;
+  let eventData = {
+    engineName: TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME,
+    searchString: "x",
+    healthReportKey: "test",
+    searchPurpose: "test",
+    originalEvent: modifiers,
+  };
+  SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+  yield promiseTab();
+  yield setUp();
+
+  // Test typing a query, then selecting a suggestion and pressing enter.
+  p = msg("waitForSearch");
+  yield msg("key", { key: "x", waitForSuggestions: true });
+  yield msg("key", "VK_DOWN");
+  yield msg("key", "VK_DOWN");
+  yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
+  mesg = yield p;
+  eventData.searchString = "xfoo";
+  eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME;
+  eventData.selection = {
+    index: 1,
+    kind: "key",
+  }
+  SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+  yield promiseTab();
   yield setUp();
 
+  // Test typing a query, then selecting a one-off button and pressing enter.
+  p = msg("waitForSearch");
+  yield msg("key", { key: "x", waitForSuggestions: true });
+  yield msg("key", "VK_UP");
+  yield msg("key", "VK_UP");
+  yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
+  mesg = yield p;
+  delete eventData.selection;
+  eventData.searchString = "x";
+  eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_2_BASENAME;
+  SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+  yield promiseTab();
+  yield setUp();
+
+  // Test typing a query and clicking the search engine header.
+  p = msg("waitForSearch");
+  modifiers.button = 0;
+  yield msg("key", { key: "x", waitForSuggestions: true });
+  yield msg("mousemove", -1);
+  yield msg("click", { eltIdx: -1, modifiers: modifiers });
+  mesg = yield p;
+  eventData.originalEvent = modifiers;
+  eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME;
+  SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+  yield promiseTab();
+  yield setUp();
+
+  // Test typing a query and then clicking a suggestion.
+  yield msg("key", { key: "x", waitForSuggestions: true });
+  p = msg("waitForSearch");
+  yield msg("mousemove", 1);
+  yield msg("click", { eltIdx: 1, modifiers: modifiers });
+  mesg = yield p;
+  eventData.searchString = "xfoo";
+  eventData.selection = {
+    index: 1,
+    kind: "mouse",
+  };
+  SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+  yield promiseTab();
+  yield setUp();
+
+  // Test typing a query and then clicking a one-off button.
+  yield msg("key", { key: "x", waitForSuggestions: true });
+  p = msg("waitForSearch");
+  yield msg("mousemove", 3);
+  yield msg("click", { eltIdx: 3, modifiers: modifiers });
+  mesg = yield p;
+  eventData.searchString = "x";
+  eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_2_BASENAME;
+  delete eventData.selection;
+  SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+  yield promiseTab();
+  yield setUp();
+
+  // Test searching when using IME composition.
   let state = yield msg("startComposition", { data: "" });
   checkState(state, "", [], -1);
   state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
-  checkState(state, "x", ["xfoo", "xbar"], -1);
+  checkState(state, "x", [{ str: "x", type: "formHistory" },
+                          { str: "xfoo", type: "formHistory" }, "xbar"], -1);
+  yield msg("commitComposition");
+  delete modifiers.button;
+  p = msg("waitForSearch");
+  yield msg("key", { key: "VK_RETURN", modifiers: modifiers });
+  mesg = yield p;
+  eventData.originalEvent = modifiers;
+  eventData.engineName = TEST_ENGINE_PREFIX + " " + TEST_ENGINE_BASENAME;
+  SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+  yield promiseTab();
+  yield setUp();
+
+  state = yield msg("startComposition", { data: "" });
+  checkState(state, "", [], -1);
+  state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
+  checkState(state, "x", [{ str: "x", type: "formHistory" },
+                          { str: "xfoo", type: "formHistory" }, "xbar"], -1);
 
   // Mouse over the first suggestion.
   state = yield msg("mousemove", 0);
-  checkState(state, "x", ["xfoo", "xbar"], 0);
+  checkState(state, "x", [{ str: "x", type: "formHistory" },
+                          { str: "xfoo", type: "formHistory" }, "xbar"], 0);
 
   // Mouse over the second suggestion.
   state = yield msg("mousemove", 1);
-  checkState(state, "x", ["xfoo", "xbar"], 1);
+  checkState(state, "x", [{ str: "x", type: "formHistory" },
+                          { str: "xfoo", type: "formHistory" }, "xbar"], 1);
 
-  // Click the second suggestion.  This should make it sticky.  To make sure it
-  // sticks, trigger suggestions again and cycle through them by pressing Down
-  // until nothing is selected again.
-  state = yield msg("mousedown", 1);
+  modifiers.button = 0;
+  let currentTab = gBrowser.selectedTab;
+  p = msg("waitForSearch");
+  yield msg("click", { eltIdx: 1, modifiers: modifiers });
+  mesg = yield p;
+  eventData.searchString = "xfoo";
+  eventData.originalEvent = modifiers;
+  eventData.selection = {
+    index: 1,
+    kind: "mouse",
+  };
+  SimpleTest.isDeeply(eventData, mesg, "Search event data");
 
-  checkState(state, "xbar", [], -1);
+  yield promiseTab();
+  yield setUp();
 
-  state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
-  checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);
-
-  state = yield msg("key", "VK_DOWN");
-  checkState(state, "xbarfoo", ["xbarfoo", "xbarbar"], 0);
+  // Remove form history entries.
+  // Wait for Satchel.
+  let deferred = Promise.defer();
+  let historyCount = 2;
+  Services.obs.addObserver(function onRemove(subj, topic, data) {
+    if (data == "formhistory-remove") {
+      if (--historyCount) {
+        return;
+      }
+      Services.obs.removeObserver(onRemove, "satchel-storage-changed");
+      executeSoon(() => deferred.resolve());
+    }
+  }, "satchel-storage-changed", false);
 
-  state = yield msg("key", "VK_DOWN");
-  checkState(state, "xbarbar", ["xbarfoo", "xbarbar"], 1);
+  yield msg("key", { key: "x", waitForSuggestions: true });
+  yield msg("key", "VK_DOWN");
+  yield msg("key", "VK_DOWN");
+  yield msg("key", "VK_DELETE");
+  yield msg("key", "VK_DOWN");
+  yield msg("key", "VK_DELETE");
+  yield deferred.promise;
+
+  yield msg("reset");
+  state = yield msg("key", { key: "x", waitForSuggestions: true });
+  checkState(state, "x", ["xfoo", "xbar"], -1);
 
-  state = yield msg("key", "VK_DOWN");
-  checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);
+  yield promiseTab();
+  yield setUp();
+  yield msg("reset");
+});
+
+add_task(function* settings() {
+  yield setUp();
+  yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+  yield msg("key", "VK_UP");
+  let p = msg("waitForSearchSettings");
+  yield msg("key", "VK_RETURN");
+  yield p;
 
   yield msg("reset");
 });
 
-
 let gDidInitialSetUp = false;
 
-function setUp() {
+function setUp(aNoEngine) {
   return Task.spawn(function* () {
     if (!gDidInitialSetUp) {
-      yield promiseNewEngine(TEST_ENGINE_BASENAME);
+      Cu.import("resource:///modules/ContentSearch.jsm");
+      let originalOnMessageSearch = ContentSearch._onMessageSearch;
+      let originalOnMessageManageEngines = ContentSearch._onMessageManageEngines;
+      ContentSearch._onMessageSearch = () => {};
+      ContentSearch._onMessageManageEngines = () => {};
+      registerCleanupFunction(() => {
+        ContentSearch._onMessageSearch = originalOnMessageSearch;
+        ContentSearch._onMessageManageEngines = originalOnMessageManageEngines;
+      });
+      yield setUpEngines();
       yield promiseTab();
       gDidInitialSetUp = true;
     }
     yield msg("focus");
   });
 }
 
 function msg(type, data=null) {
   gMsgMan.sendAsyncMessage(TEST_MSG, {
     type: type,
     data: data,
   });
   let deferred = Promise.defer();
   gMsgMan.addMessageListener(TEST_MSG, function onMsg(msg) {
+    if (msg.data.type != type) {
+      return;
+    }
     gMsgMan.removeMessageListener(TEST_MSG, onMsg);
-    deferred.resolve(msg.data);
+    deferred.resolve(msg.data.data);
   });
   return deferred.promise;
 }
 
 function checkState(actualState, expectedInputVal, expectedSuggestions,
                     expectedSelectedIdx) {
   expectedSuggestions = expectedSuggestions.map(sugg => {
     return typeof(sugg) == "object" ? sugg : {
@@ -264,27 +439,16 @@ function checkState(actualState, expecte
   let expectedState = {
     selectedIndex: expectedSelectedIdx,
     numSuggestions: expectedSuggestions.length,
     suggestionAtIndex: expectedSuggestions.map(s => s.str),
     isFormHistorySuggestionAtIndex:
       expectedSuggestions.map(s => s.type == "formHistory"),
 
     tableHidden: expectedSuggestions.length == 0,
-    tableChildrenLength: expectedSuggestions.length,
-    tableChildren: expectedSuggestions.map((s, i) => {
-      let expectedClasses = new Set([s.type]);
-      if (i == expectedSelectedIdx) {
-        expectedClasses.add("selected");
-      }
-      return {
-        textContent: s.str,
-        classes: expectedClasses,
-      };
-    }),
 
     inputValue: expectedInputVal,
     ariaExpanded: expectedSuggestions.length == 0 ? "false" : "true",
   };
 
   SimpleTest.isDeeply(actualState, expectedState, "State");
 }
 
@@ -301,17 +465,17 @@ function promiseTab() {
     gMsgMan = tab.linkedBrowser.messageManager;
     gMsgMan.sendAsyncMessage("ContentSearch", {
       type: "AddToWhitelist",
       data: [pageURL],
     });
     promiseMsg("ContentSearch", "AddToWhitelistAck", gMsgMan).then(() => {
       let jsURL = getRootDirectory(gTestPath) + TEST_CONTENT_SCRIPT_BASENAME;
       gMsgMan.loadFrameScript(jsURL, false);
-      deferred.resolve();
+      deferred.resolve(msg("init"));
     });
   }, true, true);
   openUILinkIn(pageURL, "current");
   return deferred.promise;
 }
 
 function promiseMsg(name, type, msgMan) {
   let deferred = Promise.defer();
@@ -321,24 +485,44 @@ function promiseMsg(name, type, msgMan) 
     if (msg.data.type == type) {
       msgMan.removeMessageListener(name, onMsg);
       deferred.resolve(msg);
     }
   });
   return deferred.promise;
 }
 
+function setUpEngines() {
+  return Task.spawn(function* () {
+    info("Removing default search engines");
+    let currentEngineName = Services.search.currentEngine.name;
+    let currentEngines = Services.search.getVisibleEngines();
+    info("Adding test search engines");
+    let engine1 = yield promiseNewEngine(TEST_ENGINE_BASENAME);
+    let engine2 = yield promiseNewEngine(TEST_ENGINE_2_BASENAME);
+    Services.search.currentEngine = engine1;
+    for (let engine of currentEngines) {
+      Services.search.removeEngine(engine);
+    }
+    registerCleanupFunction(() => {
+      Services.search.restoreDefaultEngines();
+      Services.search.removeEngine(engine1);
+      Services.search.removeEngine(engine2);
+      Services.search.currentEngine = Services.search.getEngineByName(currentEngineName);
+    });
+  });
+}
+
 function promiseNewEngine(basename) {
   info("Waiting for engine to be added: " + basename);
   let addDeferred = Promise.defer();
   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));
       addDeferred.resolve(engine);
     },
     onError: function (errCode) {
       ok(false, "addEngine failed with error code " + errCode);
       addDeferred.reject();
     },
   });
   return addDeferred.promise;
rename from browser/base/content/test/general/searchSuggestionUI.html
rename to browser/base/content/test/general/contentSearchUI.html
--- a/browser/base/content/test/general/searchSuggestionUI.html
+++ b/browser/base/content/test/general/contentSearchUI.html
@@ -4,17 +4,18 @@
 
 <html>
 <head>
 <meta charset="utf-8">
 <script type="application/javascript;version=1.8"
         src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js">
 </script>
 <script type="application/javascript;version=1.8"
-        src="chrome://browser/content/searchSuggestionUI.js">
+        src="chrome://browser/content/contentSearchUI.js">
 </script>
+<link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css"/>
 </head>
 <body>
 
-<input>
+<div id="container" style="position: relative;"><input type="text" value=""/></div>
 
 </body>
 </html>
rename from browser/base/content/test/general/searchSuggestionUI.js
rename to browser/base/content/test/general/contentSearchUI.js
--- a/browser/base/content/test/general/searchSuggestionUI.js
+++ b/browser/base/content/test/general/contentSearchUI.js
@@ -1,158 +1,189 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 (function () {
 
-const TEST_MSG = "SearchSuggestionUIControllerTest";
+const TEST_MSG = "ContentSearchUIControllerTest";
 const ENGINE_NAME = "browser_searchSuggestionEngine searchSuggestionEngine.xml";
-
-let input = content.document.querySelector("input");
-let gController =
-  new content.SearchSuggestionUIController(input, input.parentNode);
-gController.engineName = ENGINE_NAME;
-gController.remoteTimeout = 5000;
+var gController;
 
 addMessageListener(TEST_MSG, msg => {
   messageHandlers[msg.data.type](msg.data.data);
 });
 
 let messageHandlers = {
 
+  init: function() {
+    Services.search.currentEngine = Services.search.getEngineByName(ENGINE_NAME);
+    let input = content.document.querySelector("input");
+    gController =
+      new content.ContentSearchUIController(input, input.parentNode, "test", "test");
+    content.addEventListener("ContentSearchService", function listener(aEvent) {
+      if (aEvent.detail.type == "State" &&
+          gController.defaultEngine.name == ENGINE_NAME) {
+        content.removeEventListener("ContentSearchService", listener);
+        ack("init");
+      }
+    });
+    gController.remoteTimeout = 5000;
+  },
+
   key: function (arg) {
     let keyName = typeof(arg) == "string" ? arg : arg.key;
-    content.synthesizeKey(keyName, {});
+    content.synthesizeKey(keyName, arg.modifiers || {});
     let wait = arg.waitForSuggestions ? waitForSuggestions : cb => cb();
-    wait(ack);
+    wait(ack.bind(null, "key"));
   },
 
   startComposition: function (arg) {
     content.synthesizeComposition({ type: "compositionstart", data: "" });
-    ack();
+    ack("startComposition");
   },
 
   changeComposition: function (arg) {
     let data = typeof(arg) == "string" ? arg : arg.data;
     content.synthesizeCompositionChange({
       composition: {
         string: data,
         clauses: [
           { length: data.length, attr: content.COMPOSITION_ATTR_RAW_CLAUSE }
         ]
       },
       caret: { start: data.length, length: 0 }
     });
     let wait = arg.waitForSuggestions ? waitForSuggestions : cb => cb();
-    wait(ack);
+    wait(ack.bind(null, "changeComposition"));
+  },
+
+  commitComposition: function () {
+    content.synthesizeComposition({ type: "compositioncommitasis" });
+    ack("commitComposition");
   },
 
   focus: function () {
     gController.input.focus();
-    ack();
+    ack("focus");
   },
 
   blur: function () {
     gController.input.blur();
-    ack();
+    ack("blur");
+  },
+
+  waitForSearch: function () {
+    waitForContentSearchEvent("Search", aData => ack("waitForSearch", aData));
+  },
+
+  waitForSearchSettings: function () {
+    waitForContentSearchEvent("ManageEngines",
+                              aData => ack("waitForSearchSettings", aData));
   },
 
-  mousemove: function (suggestionIdx) {
-    // Copied from widget/tests/test_panel_mouse_coords.xul and
-    // browser/base/content/test/newtab/head.js
-    let row = gController._table.children[suggestionIdx];
-    let rect = row.getBoundingClientRect();
-    let left = content.mozInnerScreenX + rect.left;
-    let x = left + rect.width / 2;
-    let y = content.mozInnerScreenY + rect.top + rect.height / 2;
-
-    let utils = content.SpecialPowers.getDOMWindowUtils(content);
-    let scale = utils.screenPixelsPerCSSPixel;
-
-    let widgetToolkit = content.SpecialPowers.
-                        Cc["@mozilla.org/xre/app-info;1"].
-                        getService(content.SpecialPowers.Ci.nsIXULRuntime).
-                        widgetToolkit;
-    let nativeMsg = widgetToolkit == "cocoa" ? 5 : // NSMouseMoved
-                    widgetToolkit == "windows" ? 1 : // MOUSEEVENTF_MOVE
-                    3; // GDK_MOTION_NOTIFY
-
-    row.addEventListener("mousemove", function onMove() {
-      row.removeEventListener("mousemove", onMove);
-      ack();
-    });
-    utils.sendNativeMouseEvent(x * scale, y * scale, nativeMsg, 0, null);
+  mousemove: function (itemIndex) {
+    let row;
+    if (itemIndex == -1) {
+      row = gController._table.firstChild;
+    }
+    else {
+      let allElts = [...gController._suggestionsList.children,
+                     ...gController._oneOffButtons,
+                     content.document.getElementById("contentSearchSettingsButton")];
+      row = allElts[itemIndex];
+    }
+    let event = {
+      type: "mousemove",
+      clickcount: 0,
+    }
+    content.synthesizeMouseAtCenter(row, event);
+    ack("mousemove");
   },
 
-  mousedown: function (suggestionIdx) {
-    gController.onClick = () => {
-      gController.onClick = null;
-      ack();
-    };
-    let row = gController._table.children[suggestionIdx];
-    content.sendMouseEvent({ type: "mousedown" }, row);
+  click: function (arg) {
+    let eltIdx = typeof(arg) == "object" ? arg.eltIdx : arg;
+    let row;
+    if (eltIdx == -1) {
+      row = gController._table.firstChild;
+    }
+    else {
+      let allElts = [...gController._suggestionsList.children,
+                     ...gController._oneOffButtons,
+                     content.document.getElementById("contentSearchSettingsButton")];
+      row = allElts[eltIdx];
+    }
+    let event = arg.modifiers || {};
+    // synthesizeMouseAtCenter defaults to sending a mousedown followed by a
+    // mouseup if the event type is not specified.
+    content.synthesizeMouseAtCenter(row, event);
+    ack("click");
   },
 
   addInputValueToFormHistory: function () {
     gController.addInputValueToFormHistory();
-    ack();
+    ack("addInputValueToFormHistory");
   },
 
   reset: function () {
     // Reset both the input and suggestions by select all + delete.
     gController.input.focus();
     content.synthesizeKey("a", { accelKey: true });
     content.synthesizeKey("VK_DELETE", {});
-    ack();
+    ack("reset");
   },
 };
 
-function ack() {
-  sendAsyncMessage(TEST_MSG, currentState());
+function ack(aType, aData) {
+  sendAsyncMessage(TEST_MSG, { type: aType, data: aData || currentState() });
 }
 
 function waitForSuggestions(cb) {
   let observer = new content.MutationObserver(() => {
     if (gController.input.getAttribute("aria-expanded") == "true") {
       observer.disconnect();
       cb();
     }
   });
   observer.observe(gController.input, {
     attributes: true,
     attributeFilter: ["aria-expanded"],
   });
 }
 
+function waitForContentSearchEvent(messageType, cb) {
+  let mm = content.SpecialPowers.Cc["@mozilla.org/globalmessagemanager;1"].
+    getService(content.SpecialPowers.Ci.nsIMessageListenerManager);
+  mm.addMessageListener("ContentSearch", function listener(aMsg) {
+    if (aMsg.data.type != messageType) {
+      return;
+    }
+    mm.removeMessageListener("ContentSearch", listener);
+    cb(aMsg.data.data);
+  });
+}
+
 function currentState() {
   let state = {
     selectedIndex: gController.selectedIndex,
-    numSuggestions: gController.numSuggestions,
+    numSuggestions: gController._table.hidden ? 0 : gController.numSuggestions,
     suggestionAtIndex: [],
     isFormHistorySuggestionAtIndex: [],
 
     tableHidden: gController._table.hidden,
-    tableChildrenLength: gController._table.children.length,
-    tableChildren: [],
 
     inputValue: gController.input.value,
     ariaExpanded: gController.input.getAttribute("aria-expanded"),
   };
 
-  for (let i = 0; i < gController.numSuggestions; i++) {
-    state.suggestionAtIndex.push(gController.suggestionAtIndex(i));
-    state.isFormHistorySuggestionAtIndex.push(
-      gController.isFormHistorySuggestionAtIndex(i));
-  }
-
-  for (let child of gController._table.children) {
-    state.tableChildren.push({
-      textContent: child.textContent,
-      classes: new Set(child.className.split(/\s+/)),
-    });
+  if (state.numSuggestions) {
+    for (let i = 0; i < gController.numSuggestions; i++) {
+      state.suggestionAtIndex.push(gController.suggestionAtIndex(i));
+      state.isFormHistorySuggestionAtIndex.push(
+        gController.isFormHistorySuggestionAtIndex(i));
+    }
   }
 
   return state;
 }
 
 })();
--- a/browser/base/content/test/general/searchSuggestionEngine.xml
+++ b/browser/base/content/test/general/searchSuggestionEngine.xml
@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- Any copyright is dedicated to the Public Domain.
    - http://creativecommons.org/publicdomain/zero/1.0/ -->
 
 <SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
 <ShortName>browser_searchSuggestionEngine searchSuggestionEngine.xml</ShortName>
 <Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/base/content/test/general/searchSuggestionEngine.sjs?{searchTerms}"/>
-<Url type="text/html" method="GET" template="http://www.browser-searchSuggestionEngine.com/searchSuggestionEngine" rel="searchform"/>
+<Url type="text/html" method="GET" template="http://www.browser-searchSuggestionEngine.com/searchSuggestionEngine&amp;terms={searchTerms}" rel="searchform"/>
 </SearchPlugin>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/searchSuggestionEngine2.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Any copyright is dedicated to the Public Domain.
+   - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_searchSuggestionEngine searchSuggestionEngine2.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/base/content/test/general/searchSuggestionEngine.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://www.browser-searchSuggestionEngine.com/searchSuggestionEngine2&amp;terms={searchTerms}" rel="searchform"/>
+</SearchPlugin>
--- a/browser/base/content/test/newtab/browser_newtab_search.js
+++ b/browser/base/content/test/newtab/browser_newtab_search.js
@@ -71,23 +71,16 @@ let runTaskifiedTests = Task.async(funct
 
   yield addNewTabPageTabPromise();
 
   // The tab is removed at the end of the test, so there's no need to remove
   // this listener at the end of the test.
   info("Adding search event listener");
   getContentWindow().addEventListener(SERVICE_EVENT_NAME, searchEventListener);
 
-  let panel = searchPanel();
-  is(panel.state, "closed", "Search panel should be closed initially");
-
-  // The panel's animation often is not finished when the test clicks on panel
-  // children, which makes the test click the wrong children, so disable it.
-  panel.setAttribute("animate", "false");
-
   // Add the engine without any logos and switch to it.
   let noLogoEngine = yield promiseNewSearchEngine(ENGINE_NO_LOGO);
   Services.search.currentEngine = noLogoEngine;
   yield promiseSearchEvents(["CurrentEngine"]);
   yield checkCurrentEngine(ENGINE_NO_LOGO);
 
   // Add the engine with favicon and switch to it.
   let faviconEngine = yield promiseNewSearchEngine(ENGINE_FAVICON);
@@ -108,37 +101,24 @@ let runTaskifiedTests = Task.async(funct
   yield checkCurrentEngine(ENGINE_2X_LOGO);
 
   // Add the engine with 1x- and 2x-DPI logos and switch to it.
   let logo1x2xEngine = yield promiseNewSearchEngine(ENGINE_1X_2X_LOGO);
   Services.search.currentEngine = logo1x2xEngine;
   yield promiseSearchEvents(["CurrentEngine"]);
   yield checkCurrentEngine(ENGINE_1X_2X_LOGO);
 
-  // Click the logo to open the search panel.
-  yield Promise.all([
-    promisePanelShown(panel),
-    promiseClick(logoImg()),
-  ]);
-
-  let manageBox = $("manage");
-  ok(!!manageBox, "The Manage Engines box should be present in the document");
-  is(panel.childNodes.length, 1, "Search panel should only contain the Manage Engines entry");
-  is(panel.childNodes[0], manageBox, "Search panel should contain the Manage Engines entry");
-
-  panel.hidePopup();
-
   // Add the engine that provides search suggestions and switch to it.
   let suggestionEngine = yield promiseNewSearchEngine(ENGINE_SUGGESTIONS);
   Services.search.currentEngine = suggestionEngine;
   yield promiseSearchEvents(["CurrentEngine"]);
   yield checkCurrentEngine(ENGINE_SUGGESTIONS);
 
   // Avoid intermittent failures.
-  gSearch()._suggestionController.remoteTimeout = 5000;
+  gSearch().remoteTimeout = 5000;
 
   // Type an X in the search input.  This is only a smoke test.  See
   // browser_searchSuggestionUI.js for comprehensive content search suggestion
   // UI tests.
   let input = $("text");
   input.focus();
   EventUtils.synthesizeKey("x", {});
   let suggestionsPromise = promiseSearchEvents(["Suggestions"]);
@@ -304,51 +284,36 @@ function blobToBase64(blob) {
 
 let checkCurrentEngine = Task.async(function* ({name: basename, logoPrefix1x, logoPrefix2x}) {
   let engine = Services.search.currentEngine;
   ok(engine.name.includes(basename),
      "Sanity check: current engine: engine.name=" + engine.name +
      " basename=" + basename);
 
   // gSearch.currentEngineName
-  is(gSearch().currentEngineName, engine.name,
+  is(gSearch().defaultEngine.name, engine.name,
      "currentEngineName: " + engine.name);
 });
 
-function promisePanelShown(panel) {
-  let deferred = Promise.defer();
-  info("Waiting for popupshown");
-  panel.addEventListener("popupshown", function onEvent() {
-    panel.removeEventListener("popupshown", onEvent);
-    is(panel.state, "open", "Panel state");
-    deferred.resolve();
-  });
-  return deferred.promise;
-}
-
 function promiseClick(node) {
   let deferred = Promise.defer();
   let win = getContentWindow();
   SimpleTest.waitForFocus(() => {
     EventUtils.synthesizeMouseAtCenter(node, {}, win);
     deferred.resolve();
   }, win);
   return deferred.promise;
 }
 
-function searchPanel() {
-  return $("panel");
-}
-
 function logoImg() {
   return $("logo");
 }
 
 function gSearch() {
-  return getContentWindow().gSearch;
+  return getContentWindow().gSearch._contentSearchController;
 }
 
 /**
  * Waits for a load (or custom) event to finish in a given tab. If provided
  * load an uri into the tab.
  *
  * @param tab
  *        The tab to load into.
--- a/browser/base/content/test/newtab/head.js
+++ b/browser/base/content/test/newtab/head.js
@@ -717,24 +717,33 @@ function whenPagesUpdated(aCallback = Te
   });
 }
 
 /**
  * Waits for the response to the page's initial search state request.
  */
 function whenSearchInitDone() {
   let deferred = Promise.defer();
-  if (getContentWindow().gSearch._initialStateReceived) {
+  let searchController = getContentWindow().gSearch._contentSearchController;
+  if (searchController.defaultEngine) {
     return Promise.resolve();
   }
   let eventName = "ContentSearchService";
   getContentWindow().addEventListener(eventName, function onEvent(event) {
     if (event.detail.type == "State") {
       getContentWindow().removeEventListener(eventName, onEvent);
-      deferred.resolve();
+      // Wait for the search controller to receive the event, then resolve.
+      let resolver = function() {
+        if (searchController.defaultEngine) {
+          deferred.resolve();
+          return;
+        }
+        executeSoon(resolver);
+      }
+      executeSoon(resolver);
     }
   });
   return deferred.promise;
 }
 
 /**
  * Changes the newtab customization option and waits for the panel to open and close
  *
--- a/browser/base/jar.mn
+++ b/browser/base/jar.mn
@@ -139,18 +139,18 @@ browser.jar:
 #endif
         content/browser/safeMode.css                  (content/safeMode.css)
         content/browser/safeMode.js                   (content/safeMode.js)
         content/browser/safeMode.xul                  (content/safeMode.xul)
 *       content/browser/sanitize.js                   (content/sanitize.js)
 *       content/browser/sanitize.xul                  (content/sanitize.xul)
 *       content/browser/sanitizeDialog.js             (content/sanitizeDialog.js)
         content/browser/sanitizeDialog.css            (content/sanitizeDialog.css)
-        content/browser/searchSuggestionUI.js         (content/searchSuggestionUI.js)
-        content/browser/searchSuggestionUI.css        (content/searchSuggestionUI.css)
+        content/browser/contentSearchUI.js            (content/contentSearchUI.js)
+        content/browser/contentSearchUI.css           (content/contentSearchUI.css)
         content/browser/tabbrowser.css                (content/tabbrowser.css)
         content/browser/tabbrowser.xml                (content/tabbrowser.xml)
         content/browser/urlbarBindings.xml            (content/urlbarBindings.xml)
 *       content/browser/utilityOverlay.js             (content/utilityOverlay.js)
         content/browser/web-panels.js                 (content/web-panels.js)
 *       content/browser/web-panels.xul                (content/web-panels.xul)
 *       content/browser/baseMenuOverlay.xul           (content/baseMenuOverlay.xul)
 *       content/browser/nsContextMenu.js              (content/nsContextMenu.js)
--- a/browser/components/loop/test/mochitest/browser_mozLoop_context.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_context.js
@@ -25,17 +25,19 @@ add_task(function* test_mozLoop_getSelec
 
   let tab = gBrowser.selectedTab = gBrowser.addTab();
   yield promiseTabLoadEvent(tab, "about:home");
   metadata = yield promiseGetMetadata();
 
   Assert.strictEqual(metadata.url, null, "URL should be empty for about:home");
   Assert.strictEqual(metadata.favicon, null, "Favicon should be empty for about:home");
   Assert.ok(metadata.title, "Title should be set for about:home");
-  Assert.deepEqual(metadata.previews, [], "No previews available for about:home");
+  // Filter out null elements in the previews - contentSearchUI adds some img
+  // elements with chrome:// srcs, which show up as null in metadata.previews.
+  Assert.deepEqual(metadata.previews.filter(e => e), [], "No previews available for about:home");
 
   gBrowser.removeTab(tab);
 });
 
 add_task(function* test_mozLoop_getSelectedTabMetadata_defaultIcon() {
   let tab = gBrowser.selectedTab = gBrowser.addTab();
   yield promiseTabLoadEvent(tab, "http://example.com/");
   let metadata = yield promiseGetMetadata();
--- a/browser/components/nsBrowserContentHandler.js
+++ b/browser/components/nsBrowserContentHandler.js
@@ -543,16 +543,34 @@ nsBrowserContentHandler.prototype = {
         }
       }
     } catch (ex) {}
 
     // formatURLPref might return "about:blank" if getting the pref fails
     if (overridePage == "about:blank")
       overridePage = "";
 
+    // Temporary override page for users who are running Firefox on Windows 10 for their first time.
+    let platformVersion = Services.sysinfo.getProperty("version");
+    if (AppConstants.platform == "win" &&
+        Services.vc.compare(platformVersion, "10") == 0 &&
+        Services.prefs.getPrefType("browser.usedOnWindows10") == Services.prefs.PREF_BOOL &&
+        !Services.prefs.getBoolPref("browser.usedOnWindows10")) {
+      Services.prefs.setBoolPref("browser.usedOnWindows10", true);
+      let firstUseOnWindows10URL = Services.urlFormatter.formatURL("https://www.mozilla.org/%LOCALE%/firefox/windows10/");
+
+      if (firstUseOnWindows10URL && firstUseOnWindows10URL.length) {
+        if (overridePage) {
+          overridePage += "|" + firstUseOnWindows10URL;
+        } else {
+          overridePage = firstUseOnWindows10URL;
+        }
+      }
+    }
+
     var startPage = "";
     try {
       var choice = prefb.getIntPref("browser.startup.page");
       if (choice == 1 || choice == 3)
         startPage = this.startPage;
     } catch (e) {
       Components.utils.reportError(e);
     }
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_DownloadLastDirWithCPS.js
+++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_DownloadLastDirWithCPS.js
@@ -2,16 +2,17 @@
 /* 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/. */
 
 let gTests;
 function test() {
   waitForExplicitFinish();
   requestLongerTimeout(2);
+  requestCompleteLog();
   gTests = runTest();
   gTests.next();
 }
 
 /*
  * ================
  * Helper functions
  * ================
--- a/browser/config/mozconfigs/linux32/beta
+++ b/browser/config/mozconfigs/linux32/beta
@@ -1,7 +1,8 @@
 . "$topsrcdir/browser/config/mozconfigs/linux32/common-opt"
 
 ac_add_options --enable-official-branding
+ac_add_options --enable-verify-mar
 
 mk_add_options MOZ_PGO=1
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/linux32/debug
+++ b/browser/config/mozconfigs/linux32/debug
@@ -1,11 +1,11 @@
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
-ac_add_options --enable-signmar
+ac_add_options --enable-verify-mar
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
 
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . $topsrcdir/build/unix/mozconfig.linux32
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
--- a/browser/config/mozconfigs/linux32/nightly
+++ b/browser/config/mozconfigs/linux32/nightly
@@ -1,12 +1,12 @@
 . "$topsrcdir/browser/config/mozconfigs/linux32/common-opt"
 
-ac_add_options --enable-signmar
 ac_add_options --enable-profiling
+ac_add_options --enable-verify-mar
 
 # This will overwrite the default of stripping everything and keep the symbol table.
 # This is useful for profiling and debugging and only increases the package size
 # by 2 MBs.
 STRIP_FLAGS="--strip-debug"
 
 ac_add_options --with-branding=browser/branding/nightly
 
--- a/browser/config/mozconfigs/linux32/release
+++ b/browser/config/mozconfigs/linux32/release
@@ -1,13 +1,14 @@
 # This make file should be identical to the beta mozconfig, apart from the
 # safeguard below
 . "$topsrcdir/browser/config/mozconfigs/linux32/common-opt"
 
 ac_add_options --enable-official-branding
+ac_add_options --enable-verify-mar
 
 mk_add_options MOZ_PGO=1
 
 # safeguard against someone forgetting to re-set EARLY_BETA_OR_EARLIER in
 # defines.sh during the beta cycle
 export BUILDING_RELEASE=1
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/linux64/beta
+++ b/browser/config/mozconfigs/linux64/beta
@@ -1,7 +1,8 @@
 . "$topsrcdir/browser/config/mozconfigs/linux64/common-opt"
 
 ac_add_options --enable-official-branding
+ac_add_options --enable-verify-mar
 
 mk_add_options MOZ_PGO=1
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/linux64/debug
+++ b/browser/config/mozconfigs/linux64/debug
@@ -1,11 +1,11 @@
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
-ac_add_options --enable-signmar
+ac_add_options --enable-verify-mar
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
 
 MOZ_AUTOMATION_L10N_CHECK=0
 
 . $topsrcdir/build/unix/mozconfig.linux
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
--- a/browser/config/mozconfigs/linux64/nightly
+++ b/browser/config/mozconfigs/linux64/nightly
@@ -1,12 +1,12 @@
 . "$topsrcdir/browser/config/mozconfigs/linux64/common-opt"
 
-ac_add_options --enable-signmar
 ac_add_options --enable-profiling
+ac_add_options --enable-verify-mar
 
 # This will overwrite the default of stripping everything and keep the symbol table.
 # This is useful for profiling and debugging and only increases the package size
 # by 2 MBs.
 STRIP_FLAGS="--strip-debug"
 
 ac_add_options --with-branding=browser/branding/nightly
 
--- a/browser/config/mozconfigs/linux64/release
+++ b/browser/config/mozconfigs/linux64/release
@@ -1,13 +1,14 @@
 # This make file should be identical to the beta mozconfig, apart from the
 # safeguard below
 . "$topsrcdir/browser/config/mozconfigs/linux64/common-opt"
 
 ac_add_options --enable-official-branding
+ac_add_options --enable-verify-mar
 
 mk_add_options MOZ_PGO=1
 
 # safeguard against someone forgetting to re-set EARLY_BETA_OR_EARLIER in
 # defines.sh during the beta cycle
 export BUILDING_RELEASE=1
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/macosx-universal/beta
+++ b/browser/config/mozconfigs/macosx-universal/beta
@@ -1,6 +1,7 @@
 . "$topsrcdir/browser/config/mozconfigs/macosx-universal/common-opt"
 
 ac_add_options --enable-official-branding
+ac_add_options --enable-verify-mar
 
 . "$topsrcdir/build/mozconfig.common.override"
 . "$topsrcdir/build/mozconfig.cache"
--- a/browser/config/mozconfigs/macosx-universal/nightly
+++ b/browser/config/mozconfigs/macosx-universal/nightly
@@ -1,12 +1,12 @@
 . "$topsrcdir/browser/config/mozconfigs/macosx-universal/common-opt"
 
 ac_add_options --disable-install-strip
-ac_add_options --enable-signmar
+ac_add_options --enable-verify-mar
 ac_add_options --enable-profiling
 ac_add_options --enable-instruments
 ac_add_options --enable-dtrace
 
 if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
 ac_add_options --with-macbundlename-prefix=Firefox
 fi
 
--- a/browser/config/mozconfigs/macosx-universal/release
+++ b/browser/config/mozconfigs/macosx-universal/release
@@ -1,12 +1,13 @@
 # This make file should be identical to the beta mozconfig, apart from the
 # safeguard below
 . "$topsrcdir/browser/config/mozconfigs/macosx-universal/common-opt"
 
 ac_add_options --enable-official-branding
+ac_add_options --enable-verify-mar
 
 # safeguard against someone forgetting to re-set EARLY_BETA_OR_EARLIER in
 # defines.sh during the beta cycle
 export BUILDING_RELEASE=1
 
 . "$topsrcdir/build/mozconfig.common.override"
 . "$topsrcdir/build/mozconfig.cache"
--- a/browser/config/mozconfigs/macosx64/debug
+++ b/browser/config/mozconfigs/macosx64/debug
@@ -1,13 +1,13 @@
 . $topsrcdir/build/macosx/mozconfig.common
 
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
-ac_add_options --enable-signmar
+ac_add_options --enable-verify-mar
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
 if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
 ac_add_options --with-macbundlename-prefix=Firefox
 fi
@@ -15,10 +15,11 @@ fi
 # Treat warnings as errors in directories with FAIL_ON_WARNINGS.
 ac_add_options --enable-warnings-as-errors
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
 ac_add_options --with-branding=browser/branding/nightly
 
+. "$topsrcdir/build/macosx/mozconfig.rust"
 . "$topsrcdir/build/mozconfig.common.override"
 . "$topsrcdir/build/mozconfig.cache"
--- a/browser/config/mozconfigs/macosx64/nightly
+++ b/browser/config/mozconfigs/macosx64/nightly
@@ -1,11 +1,11 @@
 . $topsrcdir/build/macosx/mozconfig.common
 
-ac_add_options --enable-signmar
+ac_add_options --enable-verify-mar
 ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
 
 # Needed to enable breakpad in application.ini
 export MOZILLA_OFFICIAL=1
 
 if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
 ac_add_options --with-macbundlename-prefix=Firefox
 fi
@@ -13,10 +13,11 @@ fi
 # Treat warnings as errors in directories with FAIL_ON_WARNINGS.
 ac_add_options --enable-warnings-as-errors
 
 # Package js shell.
 export MOZ_PACKAGE_JSSHELL=1
 
 ac_add_options --with-branding=browser/branding/nightly
 
+. "$topsrcdir/build/macosx/mozconfig.rust"
 . "$topsrcdir/build/mozconfig.common.override"
 . "$topsrcdir/build/mozconfig.cache"
--- a/browser/config/mozconfigs/whitelist
+++ b/browser/config/mozconfigs/whitelist
@@ -15,19 +15,16 @@ for platform in all_platforms:
         'mk_add_options CLIENT_PY_ARGS="--hg-options=\'--verbose --time\' --hgtool=../tools/buildfarm/utils/hgtool.py --skip-chatzilla --skip-comm --skip-inspector --tinderbox-print"'
     ]
 
 for platform in ['linux32', 'linux64', 'macosx-universal']:
     whitelist['nightly'][platform] += [
         'mk_add_options MOZ_MAKE_FLAGS="-j4"',
     ]
 
-for platform in ['linux32', 'linux64', 'macosx-universal', 'win32', 'win64']:
-    whitelist['nightly'][platform] += ['ac_add_options --enable-signmar']
-
 whitelist['nightly']['linux32'] += [
     'CXX=$REAL_CXX',
     'CXX="ccache $REAL_CXX"',
     'CC="ccache $REAL_CC"',
     'mk_add_options PROFILE_GEN_SCRIPT=@TOPSRCDIR@/build/profile_pageloader.pl',
     'ac_add_options --with-ccache=/usr/bin/ccache',
     '. "$topsrcdir/build/mozconfig.cache"',
     'export MOZILLA_OFFICIAL=1',
--- a/browser/config/mozconfigs/win32/beta
+++ b/browser/config/mozconfigs/win32/beta
@@ -1,8 +1,9 @@
 . "$topsrcdir/build/mozconfig.win-common"
 . "$topsrcdir/browser/config/mozconfigs/win32/common-opt"
 
 mk_add_options MOZ_PGO=1
 
 ac_add_options --enable-official-branding
+ac_add_options --enable-verify-mar
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/win32/debug
+++ b/browser/config/mozconfigs/win32/debug
@@ -1,16 +1,16 @@
 . "$topsrcdir/build/mozconfig.win-common"
 MOZ_AUTOMATION_L10N_CHECK=0
 . "$topsrcdir/browser/config/mozconfigs/common"
 
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
 ac_add_options --enable-profiling  # needed for --enable-dmd to work on Windows
-ac_add_options --enable-signmar
+ac_add_options --enable-verify-mar
 ac_add_options --enable-require-all-d3dc-versions
 
 if [ -f /c/builds/google-oauth-api.key ]; then
   _google_oauth_api_keyfile=/c/builds/google-oauth-api.key
 else
   _google_oauth_api_keyfile=/e/builds/google-oauth-api.key
 fi
 ac_add_options --with-google-oauth-api-keyfile=${_google_oauth_api_keyfile}
--- a/browser/config/mozconfigs/win32/nightly
+++ b/browser/config/mozconfigs/win32/nightly
@@ -1,10 +1,10 @@
 . "$topsrcdir/build/mozconfig.win-common"
 . "$topsrcdir/browser/config/mozconfigs/win32/common-opt"
 
-ac_add_options --enable-signmar
 ac_add_options --enable-profiling
+ac_add_options --enable-verify-mar
 
 ac_add_options --with-branding=browser/branding/nightly
 
 . "$topsrcdir/build/mozconfig.common.override"
 . "$topsrcdir/build/mozconfig.cache"
--- a/browser/config/mozconfigs/win32/release
+++ b/browser/config/mozconfigs/win32/release
@@ -1,14 +1,15 @@
 # This make file should be identical to the beta mozconfig, apart from the
 # safeguard below
 . "$topsrcdir/build/mozconfig.win-common"
 . "$topsrcdir/browser/config/mozconfigs/win32/common-opt"
 
 mk_add_options MOZ_PGO=1
 
 ac_add_options --enable-official-branding
+ac_add_options --enable-verify-mar
 
 # safeguard against someone forgetting to re-set EARLY_BETA_OR_EARLIER in
 # defines.sh during the beta cycle
 export BUILDING_RELEASE=1
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/win64/beta
+++ b/browser/config/mozconfigs/win64/beta
@@ -1,9 +1,10 @@
 . "$topsrcdir/build/mozconfig.win-common"
 . "$topsrcdir/browser/config/mozconfigs/win64/common-win64"
 . "$topsrcdir/browser/config/mozconfigs/win64/common-opt"
 
 mk_add_options MOZ_PGO=1
 
 ac_add_options --enable-official-branding
+ac_add_options --enable-verify-mar
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/mozconfigs/win64/debug
+++ b/browser/config/mozconfigs/win64/debug
@@ -3,17 +3,17 @@ MOZ_AUTOMATION_L10N_CHECK=0
 . "$topsrcdir/browser/config/mozconfigs/common"
 
 ac_add_options --target=x86_64-pc-mingw32
 ac_add_options --host=x86_64-pc-mingw32
 
 ac_add_options --enable-debug
 ac_add_options --enable-dmd
 ac_add_options --enable-profiling  # needed for --enable-dmd to work on Windows
-ac_add_options --enable-signmar
+ac_add_options --enable-verify-mar
 
 if [ -f /c/builds/google-oauth-api.key ]; then
   _google_oauth_api_keyfile=/c/builds/google-oauth-api.key
 else
   _google_oauth_api_keyfile=/e/builds/google-oauth-api.key
 fi
 ac_add_options --with-google-oauth-api-keyfile=${_google_oauth_api_keyfile}
 
--- a/browser/config/mozconfigs/win64/nightly
+++ b/browser/config/mozconfigs/win64/nightly
@@ -1,11 +1,11 @@
 . "$topsrcdir/build/mozconfig.win-common"
 . "$topsrcdir/browser/config/mozconfigs/win64/common-win64"
 . "$topsrcdir/browser/config/mozconfigs/win64/common-opt"
 
-ac_add_options --enable-signmar
 ac_add_options --enable-profiling
+ac_add_options --enable-verify-mar
 
 ac_add_options --with-branding=browser/branding/nightly
 
 . "$topsrcdir/build/mozconfig.common.override"
 . "$topsrcdir/build/mozconfig.cache"
--- a/browser/config/mozconfigs/win64/release
+++ b/browser/config/mozconfigs/win64/release
@@ -2,14 +2,15 @@
 # safeguard below
 . "$topsrcdir/build/mozconfig.win-common"
 . "$topsrcdir/browser/config/mozconfigs/win64/common-win64"
 . "$topsrcdir/browser/config/mozconfigs/win64/common-opt"
 
 mk_add_options MOZ_PGO=1
 
 ac_add_options --enable-official-branding
+ac_add_options --enable-verify-mar
 
 # safeguard against someone forgetting to re-set EARLY_BETA_OR_EARLIER in
 # defines.sh during the beta cycle
 export BUILDING_RELEASE=1
 
 . "$topsrcdir/build/mozconfig.common.override"
--- a/browser/config/tooltool-manifests/macosx64/releng.manifest
+++ b/browser/config/tooltool-manifests/macosx64/releng.manifest
@@ -10,10 +10,17 @@
 "unpack": true
 },
 {
 "size": 167175,
 "digest": "0b71a936edf5bd70cf274aaa5d7abc8f77fe8e7b5593a208f805cc9436fac646b9c4f0b43c2b10de63ff3da671497d35536077ecbc72dba7f8159a38b580f831",
 "algorithm": "sha512",
 "filename": "sccache.tar.bz2",
 "unpack": true
+},
+{
+"size": 86474070,
+"digest": "85d528ba396d35e38280d01586d91c082a57846fb82c471579632995565caa54320b083581b04d96e526efa6cef53ea076a32c25f220f7f32f651a4dfdf30e31",
+"algorithm": "sha512",
+"filename": "rustc.tar.gz",
+"unpack": true
 }
 ]
--- a/browser/confvars.sh
+++ b/browser/confvars.sh
@@ -5,29 +5,26 @@
 
 MOZ_APP_BASENAME=Firefox
 MOZ_APP_VENDOR=Mozilla
 MOZ_UPDATER=1
 MOZ_PHOENIX=1
 
 if test "$OS_ARCH" = "WINNT"; then
   MOZ_MAINTENANCE_SERVICE=1
-  MOZ_VERIFY_MAR_SIGNATURE=1
   if ! test "$HAVE_64BIT_BUILD"; then
     if test "$MOZ_UPDATE_CHANNEL" = "nightly" -o \
             "$MOZ_UPDATE_CHANNEL" = "aurora" -o \
             "$MOZ_UPDATE_CHANNEL" = "beta" -o \
             "$MOZ_UPDATE_CHANNEL" = "release"; then
       if ! test "$MOZ_DEBUG"; then
         MOZ_STUB_INSTALLER=1
       fi
     fi
   fi
-elif test "$OS_ARCH" = "Darwin"; then
-  MOZ_VERIFY_MAR_SIGNATURE=1
 fi
 
 # Enable building ./signmar and running libmar signature tests
 MOZ_ENABLE_SIGNMAR=1
 
 MOZ_CHROME_FILE_FORMAT=omni
 MOZ_DISABLE_EXPORT_JS=1
 MOZ_SAFE_BROWSING=1
--- a/browser/locales/en-US/chrome/browser/aboutHome.dtd
+++ b/browser/locales/en-US/chrome/browser/aboutHome.dtd
@@ -6,17 +6,17 @@
 %brandDTD;
 <!ENTITY % syncBrandDTD SYSTEM "chrome://browser/locale/syncBrand.dtd">
 %syncBrandDTD;
 
 <!-- These strings are used in the about:home page -->
 
 <!ENTITY abouthome.pageTitle "&brandFullName; Start Page">
 
-<!ENTITY abouthome.searchEngineButton.label "Search">
+<!ENTITY abouthome.search.placeholder    "Search">
 
 <!-- LOCALIZATION NOTE (abouthome.defaultSnippet1.v1):
      text in <a/> will be linked to the Firefox features page on mozilla.com
 -->
 <!ENTITY abouthome.defaultSnippet1.v1 "Thanks for choosing Firefox! To get the most out of your browser, learn more about the <a>latest features</a>.">
 <!-- LOCALIZATION NOTE (abouthome.defaultSnippet2.v1):
      text in <a/> will be linked to the featured add-ons on addons.mozilla.org
 -->
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -416,16 +416,22 @@ These should match what Safari and other
   is a fundamental keybinding and we are maintaining a XP binding so that it is easy
   for people to switch to Linux.
 
  -->
 <!ENTITY searchFocus.commandkey       "k">
 <!ENTITY searchFocus.commandkey2      "e">
 <!ENTITY searchFocusUnix.commandkey   "j">
 
+<!-- LOCALIZATION NOTE (contentSearchInput.label, contentSearchSubmit.label):
+     These are set as the aria-label attribute for the search input box and
+     submit button in the in-content search UI, to be used by screen readers. -->
+<!ENTITY contentSearchInput.label     "Search query">
+<!ENTITY contentSearchSubmit.label    "Submit search">
+
 <!-- LOCALIZATION NOTE (searchFor.label, searchWith.label):
      These two strings are used to build the header above the list of one-click
      search providers:  "Search for <used typed keywords> with:" -->
 <!ENTITY searchFor.label              "Search for ">
 <!ENTITY searchWith.label             " with:">
 
 <!-- LOCALIZATION NOTE (search.label, searchAfter.label):
      This string is used to build the header above the list of one-click search
--- a/browser/locales/en-US/chrome/browser/search.properties
+++ b/browser/locales/en-US/chrome/browser/search.properties
@@ -27,8 +27,20 @@ cmd_showSuggestions_accesskey=S
 # a search engine offered by a web page. Each engine is displayed as a
 # menuitem at the bottom of the search panel.
 cmd_addFoundEngine=Add "%S"
 # LOCALIZATION NOTE (cmd_addFoundEngineMenu): When more than 5 engines
 # are offered by a web page, instead of listing all of them in the
 # search panel using the cmd_addFoundEngine string, they will be
 # grouped in a submenu using cmd_addFoundEngineMenu as a label.
 cmd_addFoundEngineMenu=Add search engine
+
+# LOCALIZATION NOTE (searchFor, searchWith):
+# These two strings are used to build the header above the list of one-click
+# search providers:  "Search for <user-typed keywords> with:"
+searchFor=Search for 
+searchWith= with:
+
+# LOCALIZATION NOTE (searchWithHeader):
+# The wording of this string should be as close as possible to
+# searchFor and searchWith. This string will be used instead of
+# them when the user has not typed any keyword.
+searchWithHeader=Search with:
--- a/browser/modules/AboutHome.jsm
+++ b/browser/modules/AboutHome.jsm
@@ -95,36 +95,24 @@ let AboutHome = {
     "AboutHome:Downloads",
     "AboutHome:Bookmarks",
     "AboutHome:History",
     "AboutHome:Apps",
     "AboutHome:Addons",
     "AboutHome:Sync",
     "AboutHome:Settings",
     "AboutHome:RequestUpdate",
-    "AboutHome:Search",
-    "AboutHome:OpenSearchPanel",
   ],
 
   init: function() {
     let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
 
     for (let msg of this.MESSAGES) {
       mm.addMessageListener(msg, this);
     }
-
-    Services.obs.addObserver(this, "browser-search-engine-modified", false);
-  },
-
-  observe: function(aEngine, aTopic, aVerb) {
-    switch (aTopic) {
-      case "browser-search-engine-modified":
-        this.sendAboutHomeData(null);
-        break;
-    }
   },
 
   receiveMessage: function(aMessage) {
     let window = aMessage.target.ownerDocument.defaultView;
 
     switch (aMessage.name) {
       case "AboutHome:RestorePreviousSession":
         let ss = Cc["@mozilla.org/browser/sessionstore;1"].
@@ -174,105 +162,33 @@ let AboutHome = {
 
       case "AboutHome:Settings":
         window.openPreferences();
         break;
 
       case "AboutHome:RequestUpdate":
         this.sendAboutHomeData(aMessage.target);
         break;
-
-      case "AboutHome:Search":
-        let data;
-        try {
-          data = JSON.parse(aMessage.data.searchData);
-        } catch(ex) {
-          Cu.reportError(ex);
-          break;
-        }
-
-        Services.search.init(function(status) {
-          if (!Components.isSuccessCode(status)) {
-            return;
-          }
-
-          let engine = Services.search.currentEngine;
-          if (AppConstants.MOZ_SERVICES_HEALTHREPORT) {
-            window.BrowserSearch.recordSearchInHealthReport(engine, "abouthome", data.selection);
-          }
-
-          // Trigger a search through nsISearchEngine.getSubmission()
-          let submission = engine.getSubmission(data.searchTerms, null, "homepage");
-          let where = window.whereToOpenLink(data.originalEvent);
-
-          // There is a chance that by the time we receive the search message, the
-          // user has switched away from the tab that triggered the search. If,
-          // based on the event, we need to load the search in the same tab that
-          // triggered it (i.e. where == "current"), openUILinkIn will not work
-          // because that tab is no longer the current one. For this case we
-          // manually load the URI in the target browser.
-          if (where == "current") {
-            aMessage.target.loadURIWithFlags(submission.uri.spec,
-                                             Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
-                                             null, null, submission.postData);
-          } else {
-            let params = {
-              postData: submission.postData,
-              inBackground: Services.prefs.getBoolPref("browser.tabs.loadInBackground"),
-            };
-            window.openLinkIn(submission.uri.spec, where, params);
-          }
-          // Used for testing
-          let mm = aMessage.target.messageManager;
-          mm.sendAsyncMessage("AboutHome:SearchTriggered", aMessage.data.searchData);
-        });
-
-        break;
-
-      case "AboutHome:OpenSearchPanel":
-        let panel = window.document.getElementById("abouthome-search-panel");
-        let anchor = aMessage.objects.anchor;
-        panel.hidden = false;
-        panel.openPopup(anchor);
-        anchor.setAttribute("active", "true");
-        panel.addEventListener("popuphidden", function onHidden() {
-          panel.removeEventListener("popuphidden", onHidden);
-          anchor.removeAttribute("active");
-        });
-        break;
     }
   },
 
   // Send all the chrome-privileged data needed by about:home. This
   // gets re-sent when the search engine changes.
   sendAboutHomeData: function(target) {
     let wrapper = {};
     Components.utils.import("resource:///modules/sessionstore/SessionStore.jsm",
       wrapper);
     let ss = wrapper.SessionStore;
 
     ss.promiseInitialized.then(function() {
-      let deferred = Promise.defer();
-
-      Services.search.init(function (status){
-        if (!Components.isSuccessCode(status)) {
-          deferred.reject(status);
-        } else {
-          deferred.resolve(Services.search.defaultEngine.name);
-        }
-      });
-
-      return deferred.promise;
-    }).then(function(engineName) {
       let data = {
         showRestoreLastSession: ss.canRestoreLastSession,
         snippetsURL: AboutHomeUtils.snippetsURL,
         showKnowYourRights: AboutHomeUtils.showKnowYourRights,
         snippetsVersion: AboutHomeUtils.snippetsVersion,
-        defaultEngineName: engineName
       };
 
       if (AboutHomeUtils.showKnowYourRights) {
         // Set pref to indicate we've shown the notification.
         let currentVersion = Services.prefs.getIntPref("browser.rights.version");
         Services.prefs.setBoolPref("browser.rights." + currentVersion + ".shown", true);
       }
 
@@ -280,19 +196,10 @@ let AboutHome = {
         target.messageManager.sendAsyncMessage("AboutHome:Update", data);
       } else {
         let mm = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
         mm.broadcastAsyncMessage("AboutHome:Update", data);
       }
     }).then(null, function onError(x) {
       Cu.reportError("Error in AboutHome.sendAboutHomeData: " + x);
     });
-  },
-
-  /**
-   * Focuses the search input in the page with the given message manager.
-   * @param  messageManager
-   *         The MessageManager object of the selected browser.
-   */
-  focusInput: function (messageManager) {
-    messageManager.sendAsyncMessage("AboutHome:FocusInput");
   }
 };
--- a/browser/modules/ContentSearch.jsm
+++ b/browser/modules/ContentSearch.jsm
@@ -40,25 +40,28 @@ const MAX_SUGGESTIONS = 6;
  *     Adds an entry to the search form history.
  *     data: the entry, a string
  *   GetSuggestions
  *     Retrieves an array of search suggestions given a search string.
  *     data: { engineName, searchString, [remoteTimeout] }
  *   GetState
  *     Retrieves the current search engine state.
  *     data: null
+ *   GetStrings
+ *     Retrieves localized search UI strings.
+ *     data: null
  *   ManageEngines
  *     Opens the search engine management window.
  *     data: null
  *   RemoveFormHistoryEntry
  *     Removes an entry from the search form history.
  *     data: the entry, a string
  *   Search
  *     Performs a search.
- *     data: { engineName, searchString, whence }
+ *     data: { engineName, searchString, healthReportKey, searchPurpose }
  *   SetCurrentEngine
  *     Sets the current engine.
  *     data: the name of the engine
  *   SpeculativeConnect
  *     Speculatively connects to an engine.
  *     data: the name of the engine
  *
  * Outbound messages have the following types:
@@ -67,16 +70,19 @@ const MAX_SUGGESTIONS = 6;
  *     Broadcast when the current engine changes.
  *     data: see _currentEngineObj
  *   CurrentState
  *     Broadcast when the current search state changes.
  *     data: see _currentStateObj
  *   State
  *     Sent in reply to GetState.
  *     data: see _currentStateObj
+ *   Strings
+ *     Sent in reply to GetStrings
+ *     data: Object containing string names and values for the current locale.
  *   Suggestions
  *     Sent in reply to GetSuggestions.
  *     data: see _onMessageGetSuggestions
  */
 
 this.ContentSearch = {
 
   // Inbound events are queued and processed in FIFO order instead of handling
@@ -93,19 +99,34 @@ this.ContentSearch = {
   _destroyedPromise: null,
 
   init: function () {
     Cc["@mozilla.org/globalmessagemanager;1"].
       getService(Ci.nsIMessageListenerManager).
       addMessageListener(INBOUND_MESSAGE, this);
     Services.obs.addObserver(this, "browser-search-engine-modified", false);
     Services.obs.addObserver(this, "shutdown-leaks-before-check", false);
+    Services.prefs.addObserver("browser.search.hiddenOneOffs", this, false);
     this._stringBundle = Services.strings.createBundle("chrome://global/locale/autocomplete.properties");
   },
 
+  get searchSuggestionUIStrings() {
+    if (this._searchSuggestionUIStrings) {
+      return this._searchSuggestionUIStrings;
+    }
+    this._searchSuggestionUIStrings = {};
+    let searchBundle = Services.strings.createBundle("chrome://browser/locale/search.properties");
+    let stringNames = ["searchHeader", "searchPlaceholder", "searchFor",
+                       "searchWith", "searchWithHeader"];
+    for (let name of stringNames) {
+      this._searchSuggestionUIStrings[name] = searchBundle.GetStringFromName(name);
+    }
+    return this._searchSuggestionUIStrings;
+  },
+
   destroy: function () {
     if (this._destroyedPromise) {
       return this._destroyedPromise;
     }
 
     Cc["@mozilla.org/globalmessagemanager;1"].
       getService(Ci.nsIMessageListenerManager).
       removeMessageListener(INBOUND_MESSAGE, this);
@@ -148,16 +169,17 @@ this.ContentSearch = {
       type: "Message",
       data: msg,
     });
     this._processEventQueue();
   },
 
   observe: function (subj, topic, data) {
     switch (topic) {
+    case "nsPref:changed":
     case "browser-search-engine-modified":
       this._eventQueue.push({
         type: "Observe",
         data: data,
       });
       this._processEventQueue();
       break;
     case "shutdown-leaks-before-check":
@@ -196,24 +218,29 @@ this.ContentSearch = {
   }),
 
   _onMessageGetState: function (msg, data) {
     return this._currentStateObj().then(state => {
       this._reply(msg, "State", state);
     });
   },
 
+  _onMessageGetStrings: function (msg, data) {
+    this._reply(msg, "Strings", this.searchSuggestionUIStrings);
+  },
+
   _onMessageSearch: function (msg, data) {
     this._ensureDataHasProperties(data, [
       "engineName",
       "searchString",
-      "whence",
+      "healthReportKey",
+      "searchPurpose",
     ]);
     let engine = Services.search.getEngineByName(data.engineName);
-    let submission = engine.getSubmission(data.searchString, "", data.whence);
+    let submission = engine.getSubmission(data.searchString, "", data.searchPurpose);
     let browser = msg.target;
     let win;
     try {
       win = browser.ownerDocument.defaultView;
     }
     catch (err) {
       // The browser may have been closed between the time its content sent the
       // message and the time we handle it.  In that case, trying to call any
@@ -234,17 +261,17 @@ this.ContentSearch = {
                                submission.postData);
     } else {
       let params = {
         postData: submission.postData,
         inBackground: Services.prefs.getBoolPref("browser.tabs.loadInBackground"),
       };
       win.openUILinkIn(submission.uri.spec, where, params);
     }
-    win.BrowserSearch.recordSearchInHealthReport(engine, data.whence,
+    win.BrowserSearch.recordSearchInHealthReport(engine, data.healthReportKey,
                                                  data.selection || null);
     return Promise.resolve();
   },
 
   _onMessageSetCurrentEngine: function (msg, data) {
     Services.search.currentEngine = Services.search.getEngineByName(data);
     return Promise.resolve();
   },
@@ -409,17 +436,22 @@ this.ContentSearch = {
     }];
   },
 
   _currentStateObj: Task.async(function* () {
     let state = {
       engines: [],
       currentEngine: yield this._currentEngineObj(),
     };
+    let pref = Services.prefs.getCharPref("browser.search.hiddenOneOffs");
+    let hiddenList = pref ? pref.split(",") : [];
     for (let engine of Services.search.getVisibleEngines()) {
+      if (hiddenList.indexOf(engine.name) != -1) {
+        continue;
+      }
       let uri = engine.getIconURLBySize(16, 16);
       state.engines.push({
         name: engine.name,
         iconBuffer: yield this._arrayBufferFromDataURI(uri),
       });
     }
     return state;
   }),
--- a/browser/modules/DirectoryLinksProvider.jsm
+++ b/browser/modules/DirectoryLinksProvider.jsm
@@ -668,17 +668,17 @@ let DirectoryLinksProvider = {
    * @param action String of the behavior to report
    * @param triggeringSiteIndex optional Int index of the site triggering action
    * @return download promise
    */
   reportSitesAction: function DirectoryLinksProvider_reportSitesAction(sites, action, triggeringSiteIndex) {
     let pastImpressions;
     // Check if the suggested tile was shown
     if (action == "view") {
-      sites.slice(0, triggeringSiteIndex + 1).forEach(site => {
+      sites.slice(0, triggeringSiteIndex + 1).filter(s => s).forEach(site => {
         let {targetedSite, url} = site.link;
         if (targetedSite) {
           this._addFrequencyCapView(url);
         }
       });
     }
     // any click action on a suggested tile should stop that tile suggestion
     // click/block - user either removed a tile or went to a landing page
--- a/browser/modules/test/browser_ContentSearch.js
+++ b/browser/modules/test/browser_ContentSearch.js
@@ -89,17 +89,18 @@ add_task(function* modifyEngine() {
 });
 
 add_task(function* search() {
   yield addTab();
   let engine = Services.search.currentEngine;
   let data = {
     engineName: engine.name,
     searchString: "ContentSearchTest",
-    whence: "ContentSearchTest",
+    healthReportKey: "ContentSearchTest",
+    searchPurpose: "ContentSearchTest",
   };
   gMsgMan.sendAsyncMessage(TEST_MSG, {
     type: "Search",
     data: data,
   });
   let submissionURL =
     engine.getSubmission(data.searchString, "", data.whence).uri.spec;
   yield waitForLoadAndStopIt(gBrowser.selectedBrowser, submissionURL);
@@ -111,17 +112,18 @@ add_task(function* searchInBackgroundTab
   // search page should be loaded in the same tab that performed the search, in
   // the background tab.
   yield addTab();
   let searchBrowser = gBrowser.selectedBrowser;
   let engine = Services.search.currentEngine;
   let data = {
     engineName: engine.name,
     searchString: "ContentSearchTest",
-    whence: "ContentSearchTest",
+    healthReportKey: "ContentSearchTest",
+    searchPurpose: "ContentSearchTest",
   };
   gMsgMan.sendAsyncMessage(TEST_MSG, {
     type: "Search",
     data: data,
   });
 
   let newTab = gBrowser.addTab();
   gBrowser.selectedTab = newTab;
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -89,16 +89,18 @@ browser.jar:
   skin/classic/browser/reload-stop-go@2x.png
   skin/classic/browser/searchbar.css
   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/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/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -120,16 +120,18 @@ browser.jar:
   skin/classic/browser/search-indicator@2x.png                 (../shared/search/search-indicator@2x.png)
   skin/classic/browser/search-engine-placeholder.png           (../shared/search/search-engine-placeholder.png)
   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/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/search/search-arrow-go.svg
@@ -0,0 +1,22 @@
+<?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 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="16" height="16" viewBox="0 0 16 16">
+  <style>
+    use:not(:target) {
+      display: none;
+    }
+    use {
+      fill: #616366;
+    }
+    use[id$="-inverted"] {
+      fill: #fff;
+    }
+  </style>
+  <defs>
+    <path id="search-arrow-go-glyph" d="M1,7v2.2C1,9.8,1.4,10,2,10h7.5l-3,3.1c-0.4,0.3-0.4,1,0,1.4l0.8,0.8 c0.4,0.4,1,0.4,1.4,0l6.6-6.6c0.4-0.4,0.4-1,0-1.4L8.7,0.7c-0.4-0.4-1-0.4-1.4,0L6.5,1.6C6.1,2,6.1,2.6,6.5,3l3,3H2C1.4,6,1,6.4,1,7z"/>
+  </defs>
+  <use id="search-arrow-go" xlink:href="#search-arrow-go-glyph"/>
+  <use id="search-arrow-go-inverted" xlink:href="#search-arrow-go-glyph"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/search/search-indicator-magnifying-glass.svg
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
+  <path fill="#808080" d="M21.7,20.3l-1.4,1.4l-5.4-5.4c-1.3,1-3,1.7-4.9,1.7 c-4.4,0-8-3.6-8-8c0-4.4,3.6-8,8-8c4.4,0,8,3.6,8,8c0,1.8-0.6,3.5-1.7,4.9L21.7,20.3z M10,4c-3.3,0-6,2.7-6,6s2.7,6,6,6s6-2.7,6-6 S13.3,4,10,4z"/>
+</svg>
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -115,16 +115,18 @@ browser.jar:
         skin/classic/browser/search-indicator@2x.png                 (../shared/search/search-indicator@2x.png)
         skin/classic/browser/search-engine-placeholder.png           (../shared/search/search-engine-placeholder.png)
         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/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/build/mach_bootstrap.py
+++ b/build/mach_bootstrap.py
@@ -240,17 +240,17 @@ def bootstrap(topsrcdir, mozilla_dir=Non
         if 'MOZ_AUTOMATION' in os.environ or 'TASK_ID' in os.environ:
             return
 
         # We are a curmudgeon who has found this undocumented variable.
         if 'I_PREFER_A_SUBOPTIMAL_MERCURIAL_EXPERIENCE' in os.environ:
             return
 
         # The environment is likely a machine invocation.
-        if not sys.stdin.isatty():
+        if sys.stdin.closed or not sys.stdin.isatty():
             return
 
         # Mercurial isn't managing this source checkout.
         if not os.path.exists(os.path.join(topsrcdir, '.hg')):
             return
 
         state_dir = get_state_dir()[0]
         last_check_path = os.path.join(state_dir, 'mercurial',
new file mode 100644
--- /dev/null
+++ b/build/macosx/mozconfig.rust
@@ -0,0 +1,5 @@
+# Options to enable rust in automation builds.
+
+RUSTC="$topsrcdir/rustc/bin/rustc"
+mk_add_options "export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:$topsrcdir/rustc/lib"
+ac_add_options --enable-rust
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -2924,34 +2924,32 @@ nsDocShell::HistoryTransactionRemoved(in
     if (shell) {
       static_cast<nsDocShell*>(shell.get())->HistoryTransactionRemoved(aIndex);
     }
   }
 
   return NS_OK;
 }
 
-unsigned long nsDocShell::gProfileTimelineRecordingsCount = 0;
-
 mozilla::LinkedList<nsDocShell::ObservedDocShell>* nsDocShell::gObservedDocShells = nullptr;
 
 NS_IMETHODIMP
 nsDocShell::SetRecordProfileTimelineMarkers(bool aValue)
 {
   bool currentValue = nsIDocShell::GetRecordProfileTimelineMarkers();
   if (currentValue != aValue) {
     if (aValue) {
-      ++gProfileTimelineRecordingsCount;
+      TimelineConsumers::AddConsumer();
       UseEntryScriptProfiling();
 
       MOZ_ASSERT(!mObserved);
       mObserved.reset(new ObservedDocShell(this));
       GetOrCreateObservedDocShells().insertFront(mObserved.get());
     } else {
-      --gProfileTimelineRecordingsCount;
+      TimelineConsumers::RemoveConsumer();
       UnuseEntryScriptProfiling();
 
       mObserved.reset(nullptr);
 
       ClearProfileTimelineMarkers();
     }
   }
 
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -29,16 +29,17 @@
 // Helper Classes
 #include "nsCOMPtr.h"
 #include "nsPoint.h" // mCurrent/mDefaultScrollbarPreferences
 #include "nsString.h"
 #include "nsAutoPtr.h"
 #include "nsThreadUtils.h"
 #include "nsContentUtils.h"
 #include "timeline/TimelineMarker.h"
+#include "timeline/TimelineConsumers.h"
 
 // Threshold value in ms for META refresh based redirects
 #define REFRESH_REDIRECT_TIMER 15000
 
 // Interfaces Needed
 #include "nsIDocCharset.h"
 #include "nsIInterfaceRequestor.h"
 #include "nsIRefreshURI.h"
@@ -257,20 +258,16 @@ public:
   void NotifyAsyncPanZoomStopped();
 
   // Add new profile timeline markers to this docShell. This will only add
   // markers if the docShell is currently recording profile timeline markers.
   // See nsIDocShell::recordProfileTimelineMarkers
   void AddProfileTimelineMarker(const char* aName, TracingMetadata aMetaData);
   void AddProfileTimelineMarker(mozilla::UniquePtr<TimelineMarker>&& aMarker);
 
-  // Global counter for how many docShells are currently recording profile
-  // timeline markers
-  static unsigned long gProfileTimelineRecordingsCount;
-
   class ObservedDocShell : public mozilla::LinkedListElement<ObservedDocShell>
   {
   public:
     explicit ObservedDocShell(nsDocShell* aDocShell)
       : mDocShell(aDocShell)
     { }
 
     nsDocShell* operator*() const { return mDocShell.get(); }
--- a/docshell/base/timeline/AutoGlobalTimelineMarker.cpp
+++ b/docshell/base/timeline/AutoGlobalTimelineMarker.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/AutoGlobalTimelineMarker.h"
 
+#include "mozilla/TimelineConsumers.h"
 #include "MainThreadUtils.h"
 #include "nsDocShell.h"
 
 namespace mozilla {
 
 void
 AutoGlobalTimelineMarker::PopulateDocShells()
 {
@@ -32,17 +33,17 @@ AutoGlobalTimelineMarker::AutoGlobalTime
                                                    MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL)
   : mOk(true)
   , mDocShells()
   , mName(aName)
 {
   MOZ_GUARD_OBJECT_NOTIFIER_INIT;
   MOZ_ASSERT(NS_IsMainThread());
 
-  if (nsDocShell::gProfileTimelineRecordingsCount == 0) {
+  if (TimelineConsumers::IsEmpty()) {
     return;
   }
 
   PopulateDocShells();
   if (!mOk) {
     // If we don't successfully populate our vector with *all* docshells being
     // observed, don't add markers to *any* of them.
     return;
--- a/docshell/base/timeline/AutoTimelineMarker.cpp
+++ b/docshell/base/timeline/AutoTimelineMarker.cpp
@@ -10,17 +10,17 @@
 #include "nsDocShell.h"
 
 namespace mozilla {
 
 bool
 AutoTimelineMarker::DocShellIsRecording(nsDocShell& aDocShell)
 {
   bool isRecording = false;
-  if (nsDocShell::gProfileTimelineRecordingsCount > 0) {
+  if (!TimelineConsumers::IsEmpty()) {
     aDocShell.GetRecordProfileTimelineMarkers(&isRecording);
   }
   return isRecording;
 }
 
 AutoTimelineMarker::AutoTimelineMarker(nsIDocShell* aDocShell, const char* aName
                                        MOZ_GUARD_OBJECT_NOTIFIER_PARAM_IN_IMPL)
   : mDocShell(nullptr)
new file mode 100644
--- /dev/null
+++ b/docshell/base/timeline/TimelineConsumers.cpp
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/TimelineConsumers.h"
+
+namespace mozilla {
+
+unsigned long TimelineConsumers::sActiveConsumers = 0;
+
+void
+TimelineConsumers::AddConsumer()
+{
+  sActiveConsumers++;
+}
+
+void
+TimelineConsumers::RemoveConsumer()
+{
+  sActiveConsumers--;
+}
+
+bool
+TimelineConsumers::IsEmpty()
+{
+  return sActiveConsumers == 0;
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/docshell/base/timeline/TimelineConsumers.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_TimelineConsumers_h_
+#define mozilla_TimelineConsumers_h_
+
+class nsDocShell;
+
+namespace mozilla {
+
+// # TimelineConsumers
+//
+// A class to trace how many frontends are interested in markers. Whenever
+// interest is expressed in markers, these fields will keep track of that.
+class TimelineConsumers
+{
+private:
+  // Counter for how many timelines are currently interested in markers.
+  static unsigned long sActiveConsumers;
+
+public:
+  static void AddConsumer();
+  static void RemoveConsumer();
+  static bool IsEmpty();
+};
+
+} // namespace mozilla
+
+#endif /* mozilla_TimelineConsumers_h_ */
--- a/docshell/base/timeline/moz.build
+++ b/docshell/base/timeline/moz.build
@@ -2,21 +2,23 @@
 # vim: set filetype=python:
 # 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/.
 
 EXPORTS.mozilla += [
     'AutoGlobalTimelineMarker.h',
     'AutoTimelineMarker.h',
+    'TimelineConsumers.h',
 ]
 
 UNIFIED_SOURCES += [
     'AutoGlobalTimelineMarker.cpp',
     'AutoTimelineMarker.cpp',
+    'TimelineConsumers.cpp',
     'TimelineMarker.cpp',
 ]
 
 FAIL_ON_WARNINGS = True
 
 FINAL_LIBRARY = 'xul'
 
 LOCAL_INCLUDES += [
--- a/dom/events/EventListenerManager.cpp
+++ b/dom/events/EventListenerManager.cpp
@@ -18,16 +18,17 @@
 #endif // #ifdef MOZ_B2G
 #include "mozilla/HalSensor.h"
 #include "mozilla/InternalMutationEvent.h"
 #include "mozilla/JSEventHandler.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/dom/BindingUtils.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/Event.h"
+#include "mozilla/TimelineConsumers.h"
 
 #include "EventListenerService.h"
 #include "nsCOMArray.h"
 #include "nsCOMPtr.h"
 #include "nsContentUtils.h"
 #include "nsDocShell.h"
 #include "nsDOMCID.h"
 #include "nsError.h"
@@ -1119,17 +1120,17 @@ EventListenerManager::HandleEventInterna
             }
           }
 
           // Maybe add a marker to the docshell's timeline, but only
           // bother with all the logic if some docshell is recording.
           nsCOMPtr<nsIDocShell> docShell;
           bool isTimelineRecording = false;
           if (mIsMainThreadELM &&
-              nsDocShell::gProfileTimelineRecordingsCount > 0 &&
+              !TimelineConsumers::IsEmpty() &&
               listener->mListenerType != Listener::eNativeListener) {
             docShell = GetDocShellForTarget();
             if (docShell) {
               docShell->GetRecordProfileTimelineMarkers(&isTimelineRecording);
             }
             if (isTimelineRecording) {
               nsDocShell* ds = static_cast<nsDocShell*>(docShell.get());
               nsAutoString typeStr;
--- a/dom/ipc/TabParent.cpp
+++ b/dom/ipc/TabParent.cpp
@@ -1206,21 +1206,35 @@ TabParent::DeallocPDocAccessibleParent(P
 bool
 TabParent::RecvPDocAccessibleConstructor(PDocAccessibleParent* aDoc,
                                          PDocAccessibleParent* aParentDoc,
                                          const uint64_t& aParentID)
 {
 #ifdef ACCESSIBILITY
   auto doc = static_cast<a11y::DocAccessibleParent*>(aDoc);
   if (aParentDoc) {
+    // A document should never directly be the parent of another document.
+    // There should always be an outer doc accessible child of the outer
+    // document containing the child.
     MOZ_ASSERT(aParentID);
+    if (!aParentID) {
+      return false;
+    }
+
     auto parentDoc = static_cast<a11y::DocAccessibleParent*>(aParentDoc);
     return parentDoc->AddChildDoc(doc, aParentID);
   } else {
+    // null aParentDoc means this document is at the top level in the child
+    // process.  That means it makes no sense to get an id for an accessible
+    // that is its parent.
     MOZ_ASSERT(!aParentID);
+    if (aParentID) {
+      return false;
+    }
+
     doc->SetTopLevel();
     a11y::DocManager::RemoteDocAdded(doc);
   }
 #endif
   return true;
 }
 
 PDocumentRendererParent*
--- a/gfx/thebes/gfxPrefs.h
+++ b/gfx/thebes/gfxPrefs.h
@@ -256,17 +256,16 @@ private:
   DECL_GFX_PREF(Once, "gfx.work-around-driver-bugs",           WorkAroundDriverBugs, bool, true);
 
   DECL_GFX_PREF(Live, "gl.msaa-level",                         MSAALevel, uint32_t, 2);
   DECL_GFX_PREF(Live, "gl.require-hardware",                   RequireHardwareGL, bool, false);
 
   DECL_GFX_PREF(Once, "image.cache.size",                      ImageCacheSize, int32_t, 5*1024*1024);
   DECL_GFX_PREF(Once, "image.cache.timeweight",                ImageCacheTimeWeight, int32_t, 500);
   DECL_GFX_PREF(Live, "image.decode-immediately.enabled",      ImageDecodeImmediatelyEnabled, bool, false);
-  DECL_GFX_PREF(Once, "image.decode.retry-on-alloc-failure",   ImageDecodeRetryOnAllocFailure, bool, false);
   DECL_GFX_PREF(Live, "image.downscale-during-decode.enabled", ImageDownscaleDuringDecodeEnabled, bool, true);
   DECL_GFX_PREF(Live, "image.high_quality_downscaling.enabled", ImageHQDownscalingEnabled, bool, false);
   DECL_GFX_PREF(Live, "image.high_quality_downscaling.min_factor", ImageHQDownscalingMinFactor, uint32_t, 1000);
   DECL_GFX_PREF(Live, "image.high_quality_upscaling.max_size", ImageHQUpscalingMaxSize, uint32_t, 20971520);
   DECL_GFX_PREF(Live, "image.infer-src-animation.threshold-ms", ImageInferSrcAnimationThresholdMS, uint32_t, 2000);
   DECL_GFX_PREF(Once, "image.mem.decode_bytes_at_a_time",      ImageMemDecodeBytesAtATime, uint32_t, 200000);
   DECL_GFX_PREF(Live, "image.mem.discardable",                 ImageMemDiscardable, bool, false);
   DECL_GFX_PREF(Once, "image.mem.surfacecache.discard_factor", ImageMemSurfaceCacheDiscardFactor, uint32_t, 1);
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -258,17 +258,16 @@ RasterImage::RasterImage(ImageURL* aURI 
   mDecodeCount(0),
   mRequestedSampleSize(0),
   mLastImageContainerDrawResult(DrawResult::NOT_READY),
 #ifdef DEBUG
   mFramesNotified(0),
 #endif
   mSourceBuffer(new SourceBuffer()),
   mFrameCount(0),
-  mRetryCount(0),
   mHasSize(false),
   mTransient(false),
   mSyncLoad(false),
   mDiscardable(false),
   mHasSourceData(false),
   mHasBeenDecoded(false),
   mDownscaleDuringDecode(false),
   mPendingAnimation(false),
@@ -1366,41 +1365,16 @@ RasterImage::Discard()
 }
 
 bool
 RasterImage::CanDiscard() {
   return mHasSourceData &&       // ...have the source data...
          !mAnim;                 // Can never discard animated images
 }
 
-class RetryDecodeRunnable : public nsRunnable
-{
-public:
-  RetryDecodeRunnable(RasterImage* aImage,
-                      const IntSize& aSize,
-                      uint32_t aFlags)
-    : mImage(aImage)
-    , mSize(aSize)
-    , mFlags(aFlags)
-  {
-    MOZ_ASSERT(aImage);
-  }
-
-  NS_IMETHOD Run()
-  {
-    mImage->RequestDecodeForSize(mSize, mFlags);
-    return NS_OK;
-  }
-
-private:
-  nsRefPtr<RasterImage> mImage;
-  const IntSize mSize;
-  const uint32_t mFlags;
-};
-
 // Sets up a decoder for this image.
 already_AddRefed<Decoder>
 RasterImage::CreateDecoder(const Maybe<IntSize>& aSize, uint32_t aFlags)
 {
   // Make sure we actually get size before doing a full decode.
   if (aSize) {
     MOZ_ASSERT(mHasSize, "Must do a size decode before a full decode!");
     MOZ_ASSERT(mDownscaleDuringDecode || *aSize == mSize,
@@ -1456,42 +1430,16 @@ RasterImage::CreateDecoder(const Maybe<I
   if (!mHasBeenDecoded && aSize) {
     // Lock the image while we're decoding, so that it doesn't get evicted from
     // the SurfaceCache before we have a chance to realize that it's animated.
     // The corresponding unlock happens in FinalizeDecoder.
     LockImage();
     decoder->SetImageIsLocked();
   }
 
-  if (aSize && decoder->HasError()) {
-    if (gfxPrefs::ImageDecodeRetryOnAllocFailure() &&
-        mRetryCount < 10) {
-      // We couldn't allocate the first frame for this image. We're probably in
-      // a temporary low-memory situation, so fire off a runnable and hope that
-      // things have improved when it runs. (Unless we've already retried 10
-      // times in a row, in which case just give up.)
-      mRetryCount++;
-
-      if (decoder->ImageIsLocked()) {
-        UnlockImage();
-      }
-      decoder->TakeProgress();
-      decoder->TakeInvalidRect();
-
-      nsCOMPtr<nsIRunnable> runnable =
-        new RetryDecodeRunnable(this, *aSize, aFlags);
-      NS_DispatchToMainThread(runnable);
-
-      return nullptr;
-    }
-  } else {
-    // We didn't encounter an error when allocating the first frame.
-    mRetryCount = 0;
-  }
-
   decoder->SetIterator(mSourceBuffer->Iterator());
 
   // Set a target size for downscale-during-decode if applicable.
   if (mDownscaleDuringDecode && aSize && *aSize != mSize) {
     DebugOnly<nsresult> rv = decoder->SetTargetSize(*aSize);
     MOZ_ASSERT(nsresult(rv) != NS_ERROR_NOT_AVAILABLE,
                "We're downscale-during-decode but decoder doesn't support it?");
     MOZ_ASSERT(NS_SUCCEEDED(rv), "Bad downscale-during-decode target size?");
--- a/image/RasterImage.h
+++ b/image/RasterImage.h
@@ -402,19 +402,16 @@ private: // data
 #endif
 
   // The source data for this image.
   nsRefPtr<SourceBuffer>     mSourceBuffer;
 
   // The number of frames this image has.
   uint32_t                   mFrameCount;
 
-  // The number of times we've retried decoding this image.
-  uint8_t                    mRetryCount;
-
   // Boolean flags (clustered together to conserve space):
   bool                       mHasSize:1;       // Has SetSize() been called?
   bool                       mTransient:1;     // Is the image short-lived?
   bool                       mSyncLoad:1;      // Are we loading synchronously?
   bool                       mDiscardable:1;   // Is container discardable?
   bool                       mHasSourceData:1; // Do we have source data?
   bool                       mHasBeenDecoded:1; // Decoded at least once?
   bool                       mDownscaleDuringDecode:1;
--- a/js/src/jit/MIR.h
+++ b/js/src/jit/MIR.h
@@ -6883,23 +6883,17 @@ class MPhi final
         // once Vector supports that.
         inputs_.infallibleGrowByUninitialized(1);
         new (&inputs_.back()) MUse(ins, this);
     }
 
     // Appends a new input to the input vector. May perform reallocation.
     // Prefer reserveLength() and addInput() instead, where possible.
     bool addInputSlow(MDefinition* ins) {
-        // Use growByUninitialized and placement-new instead of just append,
-        // similar to what addInput does.
-        if (!inputs_.growByUninitialized(1))
-            return false;
-
-        new (&inputs_.back()) MUse(ins, this);
-        return true;
+        return inputs_.emplaceBack(ins, this);
     }
 
     // Update the type of this phi after adding |ins| as an input. Set
     // |*ptypeChange| to true if the type changed.
     bool checkForTypeChange(MDefinition* ins, bool* ptypeChange);
 
     MDefinition* foldsTo(TempAllocator& alloc) override;
     MDefinition* foldsTernary();
--- a/js/src/vm/SavedStacks.cpp
+++ b/js/src/vm/SavedStacks.cpp
@@ -925,38 +925,34 @@ SavedStacks::insertFrames(JSContext* cx,
             }
         } else if (asyncActivation != &activation) {
             // We found an async stack in the previous activation, and we
             // walked past the oldest frame of that activation, we're done.
             break;
         }
 
         AutoLocationValueRooter location(cx);
-
         {
             AutoCompartment ac(cx, iter.compartment());
             if (!cx->compartment()->savedStacks().getLocation(cx, iter, &location))
                 return false;
         }
 
-        // Use growByUninitialized and placement-new instead of just append.
-        // We'd ideally like to use an emplace method once Vector supports it.
-        if (!stackChain->growByUninitialized(1)) {
+        auto displayAtom = iter.isNonEvalFunctionFrame() ? iter.functionDisplayAtom() : nullptr;
+        if (!stackChain->emplaceBack(location->source,
+                                     location->line,
+                                     location->column,
+                                     displayAtom,
+                                     nullptr,
+                                     nullptr,
+                                     iter.compartment()->principals()))
+        {
             ReportOutOfMemory(cx);
             return false;
         }
-        new (&stackChain->back()) SavedFrame::Lookup(
-          location->source,
-          location->line,
-          location->column,
-          iter.isNonEvalFunctionFrame() ? iter.functionDisplayAtom() : nullptr,
-          nullptr,
-          nullptr,
-          iter.compartment()->principals()
-        );
 
         ++iter;
 
         // If maxFrameCount is zero there's no limit on the number of frames.
         if (maxFrameCount == 0)
             continue;
 
         if (maxFrameCount == 1) {
@@ -1006,23 +1002,20 @@ SavedStacks::adoptAsyncStack(JSContext* 
     // still don't enforce an upper limit if the caller requested more frames.
     if (maxFrameCount == 0)
         maxFrameCount = ASYNC_STACK_MAX_FRAME_COUNT;
 
     // Accumulate the vector of Lookup objects in |stackChain|.
     SavedFrame::AutoLookupVector stackChain(cx);
     SavedFrame* currentSavedFrame = asyncStack;
     for (unsigned i = 0; i < maxFrameCount && currentSavedFrame; i++) {
-        // Use growByUninitialized and placement-new instead of just append.
-        // We'd ideally like to use an emplace method once Vector supports it.
-        if (!stackChain->growByUninitialized(1)) {
+        if (!stackChain->emplaceBack(*currentSavedFrame)) {
             ReportOutOfMemory(cx);
             return false;
         }
-        new (&stackChain->back()) SavedFrame::Lookup(*currentSavedFrame);
 
         // Attach the asyncCause to the youngest frame.
         if (i == 0)
             stackChain->back().asyncCause = asyncCauseAtom;
 
         currentSavedFrame = currentSavedFrame->getParent();
     }
 
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -58,16 +58,17 @@
 #include "BackgroundChild.h"
 #include "mozilla/ipc/PBackgroundChild.h"
 #include "nsIIPCBackgroundChildCreateCallback.h"
 #include "mozilla/layout/VsyncChild.h"
 #include "VsyncSource.h"
 #include "mozilla/VsyncDispatcher.h"
 #include "nsThreadUtils.h"
 #include "mozilla/unused.h"
+#include "mozilla/TimelineConsumers.h"
 
 #ifdef MOZ_NUWA_PROCESS
 #include "ipc/Nuwa.h"
 #endif
 
 using namespace mozilla;
 using namespace mozilla::widget;
 using namespace mozilla::ipc;
@@ -987,17 +988,17 @@ nsRefreshDriver::GetRefreshTimerInterval
 {
   return mThrottled ? GetThrottledTimerInterval() : GetRegularTimerInterval();
 }
 
 RefreshDriverTimer*
 nsRefreshDriver::ChooseTimer() const
 {
   if (mThrottled) {
-    if (!sThrottledRateTimer) 
+    if (!sThrottledRateTimer)
       sThrottledRateTimer = new InactiveRefreshDriverTimer(GetThrottledTimerInterval(),
                                                            DEFAULT_INACTIVE_TIMER_DISABLE_SECONDS * 1000.0);
     return sThrottledRateTimer;
   }
 
   if (!sRegularRateTimer) {
     bool isDefault = true;
     double rate = GetRegularTimerInterval(&isDefault);
@@ -1045,17 +1046,17 @@ nsRefreshDriver::nsRefreshDriver(nsPresC
   mNextRecomputeVisibilityTick = mMostRecentTick;
 }
 
 nsRefreshDriver::~nsRefreshDriver()
 {
   MOZ_ASSERT(ObserverCount() == 0,
              "observers should have unregistered");
   MOZ_ASSERT(!mActiveTimer, "timer should be gone");
-  
+
   if (mRootRefresh) {
     mRootRefresh->RemoveRefreshObserver(this, Flush_Style);
     mRootRefresh = nullptr;
   }
   for (nsIPresShell* shell : mPresShellsToInvalidateIfHidden) {
     shell->InvalidatePresShellIfHidden();
   }
   mPresShellsToInvalidateIfHidden.Clear();
@@ -1430,17 +1431,17 @@ HasPendingAnimations(nsIPresShell* aShel
 
 /**
  * Return a list of all the child docShells in a given root docShell that are
  * visible and are recording markers for the profilingTimeline
  */
 static void GetProfileTimelineSubDocShells(nsDocShell* aRootDocShell,
                                            nsTArray<nsDocShell*>& aShells)
 {
-  if (!aRootDocShell || nsDocShell::gProfileTimelineRecordingsCount == 0) {
+  if (!aRootDocShell || TimelineConsumers::IsEmpty()) {
     return;
   }
 
   nsCOMPtr<nsISimpleEnumerator> enumerator;
   nsresult rv = aRootDocShell->GetDocShellEnumerator(nsIDocShellTreeItem::typeAll,
     nsIDocShell::ENUMERATE_BACKWARDS, getter_AddRefs(enumerator));
 
   if (NS_FAILED(rv)) {
--- a/mfbt/Tuple.h
+++ b/mfbt/Tuple.h
@@ -137,17 +137,40 @@ struct TupleImpl<Index, HeadT, TailT...>
   // Copy and move constructors.
   // We'd like to use '= default' to implement these, but MSVC 2013's support
   // for '= default' is incomplete and this doesn't work.
   TupleImpl(const TupleImpl& aOther)
     : Base(Tail(aOther))
     , mHead(Head(aOther)) {}
   TupleImpl(TupleImpl&& aOther)
     : Base(Move(Tail(aOther)))
-    , mHead(Move(Head(aOther))) {}
+    , mHead(Forward<HeadT>(Head(aOther))) {}
+
+  // Assign from a tuple whose elements are convertible to the elements
+  // of this tuple.
+  template <typename... OtherElements,
+            typename = typename EnableIf<
+                sizeof...(OtherElements) == sizeof...(TailT) + 1>::Type>
+  TupleImpl& operator=(const TupleImpl<Index, OtherElements...>& aOther)
+  {
+    typedef TupleImpl<Index, OtherElements...> OtherT;
+    Head(*this) = OtherT::Head(aOther);
+    Tail(*this) = OtherT::Tail(aOther);
+    return *this;
+  }
+  template <typename... OtherElements,
+            typename = typename EnableIf<
+                sizeof...(OtherElements) == sizeof...(TailT) + 1>::Type>
+  TupleImpl& operator=(TupleImpl<Index, OtherElements...>&& aOther)
+  {
+    typedef TupleImpl<Index, OtherElements...> OtherT;
+    Head(*this) = Move(OtherT::Head(aOther));
+    Tail(*this) = Move(OtherT::Tail(aOther));
+    return *this;
+  }
 
   // Copy and move assignment operators.
   TupleImpl& operator=(const TupleImpl& aOther)
   {
     Head(*this) = Head(aOther);
     Tail(*this) = Tail(aOther);
     return *this;
   }
@@ -190,16 +213,32 @@ public:
                 detail::CheckConvertibility<
                     detail::Group<OtherHead, OtherTail...>,
                     detail::Group<Elements...>>::value>::Type>
   explicit Tuple(OtherHead&& aHead, OtherTail&&... aTail)
     : Impl(Forward<OtherHead>(aHead), Forward<OtherTail>(aTail)...) { }
   Tuple(const Tuple& aOther) : Impl(aOther) { }
   Tuple(Tuple&& aOther) : Impl(Move(aOther)) { }
 
+  template <typename... OtherElements,
+            typename = typename EnableIf<
+                sizeof...(OtherElements) == sizeof...(Elements)>::Type>
+  Tuple& operator=(const Tuple<OtherElements...>& aOther)
+  {
+    static_cast<Impl&>(*this) = aOther;
+    return *this;
+  }
+  template <typename... OtherElements,
+            typename = typename EnableIf<
+                sizeof...(OtherElements) == sizeof...(Elements)>::Type>
+  Tuple& operator=(Tuple<OtherElements...>&& aOther)
+  {
+    static_cast<Impl&>(*this) = Move(aOther);
+    return *this;
+  }
   Tuple& operator=(const Tuple& aOther)
   {
     static_cast<Impl&>(*this) = aOther;
     return *this;
   }
   Tuple& operator=(Tuple&& aOther)
   {
     static_cast<Impl&>(*this) = Move(aOther);
@@ -291,11 +330,30 @@ auto Get(Tuple<Elements...>&& aTuple)
  * auto tuple = MakeTuple(42, 0.5f, 'c');  // has type Tuple<int, float, char>
  */
 template<typename... Elements>
 Tuple<Elements...> MakeTuple(Elements&&... aElements)
 {
   return Tuple<Elements...>(Forward<Elements>(aElements)...);
 }
 
+/**
+ * A convenience function for constructing a tuple of references to a
+ * sequence of variables. Since assignments to the elements of the tuple
+ * "go through" to the referenced variables, this can be used to "unpack"
+ * a tuple into individual variables.
+ *
+ * Example:
+ *
+ * int i;
+ * float f;
+ * char c;
+ * Tie(i, f, c) = FunctionThatReturnsATuple();
+ */
+template<typename... Elements>
+Tuple<Elements&...> Tie(Elements&... aVariables)
+{
+  return Tuple<Elements&...>(aVariables...);
+}
+
 } // namespace mozilla
 
 #endif /* mozilla_Tuple_h */
--- a/mfbt/Vector.h
+++ b/mfbt/Vector.h
@@ -559,16 +559,28 @@ public:
 
   /**
    * This can take either a T& or a T&&. Given a T&&, it moves |aU| into the
    * vector, instead of copying it. If it fails, |aU| is left unmoved. ("We are
    * not amused.")
    */
   template<typename U> bool append(U&& aU);
 
+  /**
+   * Construct a T in-place as a new entry at the end of this vector.
+   */
+  template<typename... Args>
+  bool emplaceBack(Args&&... aArgs)
+  {
+    if (!growByUninitialized(1))
+      return false;
+    new (&back()) T(Forward<Args>(aArgs)...);
+    return true;
+  }
+
   template<typename U, size_t O, class BP, class UV>
   bool appendAll(const VectorBase<U, O, BP, UV>& aU);
   bool appendN(const T& aT, size_t aN);
   template<typename U> bool append(const U* aBegin, const U* aEnd);
   template<typename U> bool append(const U* aBegin, size_t aLength);
 
   /*
    * Guaranteed-infallible append operations for use upon vectors whose
--- a/mfbt/tests/TestTuple.cpp
+++ b/mfbt/tests/TestTuple.cpp
@@ -13,16 +13,17 @@
 
 #include <stddef.h>
 
 using mozilla::Get;
 using mozilla::IsSame;
 using mozilla::MakeTuple;
 using mozilla::MakeUnique;
 using mozilla::Move;
+using mozilla::Tie;
 using mozilla::Tuple;
 using mozilla::UniquePtr;
 using mozilla::unused;
 
 #define CHECK(c) \
   do { \
     bool cond = !!(c); \
     MOZ_RELEASE_ASSERT(cond, "Failed assertion: " #c); \
@@ -31,17 +32,17 @@ using mozilla::unused;
 // The second argument is the expected type. It's variadic to allow the
 // type to contain commas.
 #define CHECK_TYPE(expression, ...)  \
   static_assert(IsSame<decltype(expression), __VA_ARGS__>::value, \
       "Type mismatch!")
 
 struct ConvertibleToInt
 {
-  operator int() { return 42; }
+  operator int() const { return 42; }
 };
 
 static void
 TestConstruction()
 {
   // Default construction
   Tuple<> a;
   unused << a;
@@ -131,17 +132,39 @@ TestMakeTuple()
   auto tuple = MakeTuple(42, 0.5f, 'c');
   CHECK_TYPE(tuple, Tuple<int, float, char>);
   CHECK(Get<0>(tuple) == 42);
   CHECK(Get<1>(tuple) == 1.0f);
   CHECK(Get<2>(tuple) == 'c');
   return true;
 }
 
+static bool
+TestTie()
+{
+  int i;
+  float f;
+  char c;
+  Tuple<int, float, char> rhs1(42, 0.5f, 'c');
+  Tie(i, f, c) = rhs1;
+  CHECK(i == Get<0>(rhs1));
+  CHECK(f == Get<1>(rhs1));
+  CHECK(c == Get<2>(rhs1));
+  // Test conversions
+  Tuple<ConvertibleToInt, double, unsigned char> rhs2(ConvertibleToInt(),
+      0.7f, 'd');
+  Tie(i, f, c) = rhs2;
+  CHECK(i == Get<0>(rhs2));
+  CHECK(f == Get<1>(rhs2));
+  CHECK(c == Get<2>(rhs2));
+  return true;
+}
+
 int
 main()
 {
   TestConstruction();
   TestAssignment();
   TestGet();
   TestMakeTuple();
+  TestTie();
   return 0;
 }
--- a/mfbt/tests/TestVector.cpp
+++ b/mfbt/tests/TestVector.cpp
@@ -1,25 +1,29 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "mozilla/Move.h"
+#include "mozilla/UniquePtr.h"
 #include "mozilla/Vector.h"
 
 using mozilla::detail::VectorTesting;
+using mozilla::MakeUnique;
 using mozilla::Move;
+using mozilla::UniquePtr;
 using mozilla::Vector;
 
 struct mozilla::detail::VectorTesting
 {
   static void testReserved();
   static void testConstRange();
+  static void testEmplaceBack();
 };
 
 void
 mozilla::detail::VectorTesting::testReserved()
 {
 #ifdef DEBUG
   Vector<bool> bv;
   MOZ_RELEASE_ASSERT(bv.reserved() == 0);
@@ -92,15 +96,78 @@ mozilla::detail::VectorTesting::testCons
   for (int i = 0; i < 10; i++) {
     MOZ_RELEASE_ASSERT(!range.empty());
     MOZ_RELEASE_ASSERT(range.front() == i);
     range.popFront();
   }
 #endif
 }
 
+namespace {
+
+struct S
+{
+  size_t            j;
+  UniquePtr<size_t> k;
+
+  static size_t constructCount;
+  static size_t moveCount;
+
+  S(size_t j, size_t k)
+    : j(j)
+    , k(MakeUnique<size_t>(k))
+  {
+    constructCount++;
+  }
+
+  S(S&& rhs)
+    : j(rhs.j)
+    , k(Move(rhs.k))
+  {
+    rhs.~S();
+    moveCount++;
+  }
+
+  S(const S&) = delete;
+  S& operator=(const S&) = delete;
+};
+
+size_t S::constructCount = 0;
+size_t S::moveCount = 0;
+
+}
+
+void
+mozilla::detail::VectorTesting::testEmplaceBack()
+{
+  Vector<S> vec;
+  MOZ_RELEASE_ASSERT(vec.reserve(20));
+
+  for (size_t i = 0; i < 10; i++) {
+    S s(i, i*i);
+    MOZ_RELEASE_ASSERT(vec.append(Move(s)));
+  }
+
+  MOZ_RELEASE_ASSERT(vec.length() == 10);
+  MOZ_RELEASE_ASSERT(S::constructCount == 10);
+  MOZ_RELEASE_ASSERT(S::moveCount == 10);
+
+  for (size_t i = 10; i < 20; i++) {
+    MOZ_RELEASE_ASSERT(vec.emplaceBack(i, i*i));
+  }
+
+  MOZ_RELEASE_ASSERT(vec.length() == 20);
+  MOZ_RELEASE_ASSERT(S::constructCount == 20);
+  MOZ_RELEASE_ASSERT(S::moveCount == 10);
+
+  for (size_t i = 0; i < 20; i++) {
+    MOZ_RELEASE_ASSERT(vec[i].j == i);
+    MOZ_RELEASE_ASSERT(*vec[i].k == i*i);
+  }
+}
 
 int
 main()
 {
   VectorTesting::testReserved();
   VectorTesting::testConstRange();
+  VectorTesting::testEmplaceBack();
 }
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -3,16 +3,17 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
       package="@ANDROID_PACKAGE_NAME@"
       android:installLocation="auto"
       android:versionCode="@ANDROID_VERSION_CODE@"
       android:versionName="@MOZ_APP_VERSION@"
 #ifdef MOZ_ANDROID_SHARED_ID
       android:sharedUserId="@MOZ_ANDROID_SHARED_ID@"
 #endif
+      android:theme="@style/FilteredTouches"
       >
     <uses-sdk android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
 #ifdef MOZ_ANDROID_MAX_SDK_VERSION
               android:maxSdkVersion="@MOZ_ANDROID_MAX_SDK_VERSION@"
 #endif
               android:targetSdkVersion="@ANDROID_TARGET_SDK@"/>
 
 #include ../services/manifests/FxAccountAndroidManifest_permissions.xml.in
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -828,11 +828,9 @@
         <item name="android:textColor">#FFFFFF</item>
         <item name="android:textSize">20sp</item>
         <item name="android:gravity">center</item>
         <item name="android:paddingTop">16dp</item>
         <item name="android:paddingBottom">16dp</item>
         <item name="android:paddingLeft">8dp</item>
         <item name="android:paddingRight">8dp</item>
     </style>
-
-    <style name="TabQueueActivity" parent="android:style/Theme.NoDisplay" />
 </resources>
--- a/mobile/android/base/resources/values/themes.xml
+++ b/mobile/android/base/resources/values/themes.xml
@@ -1,40 +1,52 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- This Source Code Form is subject to the terms of the Mozilla Public
    - License, v. 2.0. If a copy of the MPL was not distributed with this
    - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
 
 <resources>
 
+    <!-- Bug 1147265: to prevent tap jacking, we should set
+         filterTouchesWhenObscured=true for all activities. This theme
+         should be used when there is no other relevant theme for the
+         Activity. Note: it may be helpful to inherit from a base
+         Android theme (i.e. what GeckoBase inherits from). -->
+    <style name="FilteredTouches">
+        <item name="android:filterTouchesWhenObscured">true</item>
+    </style>
+
     <!--
         Base application theme. This could be overridden by GeckoBaseTheme
         in other res/values-XXX/themes.xml.
     -->
     <style name="GeckoBase" parent="@android:style/Theme.Light">
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowContentOverlay">@null</item>
     </style>
 
     <style name="GeckoDialogBase" parent="@android:style/Theme.Dialog">
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowContentOverlay">@null</item>
+        <item name="android:filterTouchesWhenObscured">true</item>
     </style>
 
     <style name="GeckoTitleDialogBase" parent="@android:style/Theme.Dialog" />
 
     <style name="GeckoPreferencesBase" parent="GeckoBase">
         <item name="android:windowNoTitle">false</item>
     </style>
 
     <!--
         Application Theme. All customizations that are not specific
         to a particular API level can go here.
     -->
     <style name="Gecko" parent="GeckoBase">
+        <item name="android:filterTouchesWhenObscured">true</item>
+
         <!-- Default colors -->
         <item name="android:textColorPrimary">@color/primary_text</item>
         <item name="android:textColorSecondary">@color/secondary_text</item>
         <item name="android:textColorTertiary">@color/tertiary_text</item>
 
         <!-- Default inverse colors -->
         <item name="android:textColorPrimaryInverse">@color/primary_text</item>
         <item name="android:textColorSecondaryInverse">@color/secondary_text</item>
@@ -95,16 +107,17 @@
         <item name="menuItemActionModeStyle">@style/GeckoActionBar.Button</item>
         <item name="menuItemShareActionButtonStyle">@style/Widget.MenuItemSecondaryActionBar</item>
         <item name="topSitesGridItemViewStyle">@style/Widget.TopSitesGridItemView</item>
         <item name="topSitesGridViewStyle">@style/Widget.TopSitesGridView</item>
         <item name="topSitesThumbnailViewStyle">@style/Widget.TopSitesThumbnailView</item>
     </style>
 
     <style name="Gecko.Preferences" parent="GeckoPreferencesBase">
+        <item name="android:filterTouchesWhenObscured">true</item>
         <item name="floatingHintEditTextStyle">@style/FloatingHintEditText</item>
     </style>
 
     <!-- Make an activity appear like an overlay. -->
     <style name="OverlayActivity" parent="Gecko">
         <item name="android:windowBackground">@android:color/transparent</item>
         <item name="android:windowNoTitle">true</item>
         <item name="android:windowIsTranslucent">true</item>
@@ -113,9 +126,14 @@
         <!-- Set the app's title bar color in the recent app switcher.
 
              Note: We'd prefer not to show up in the recent app switcher (bug 1137928). -->
         <item name="android:colorPrimary">@color/text_and_tabs_tray_grey</item>
         <!-- We display the overlay on top of other Activities so show their status bar. -->
         <item name="android:statusBarColor">@android:color/transparent</item>
     </style>
 
+    <style name="TabQueueActivity" parent="android:style/Theme.NoDisplay">
+        <item name="android:filterTouchesWhenObscured">true</item>
+    </style>
+
+
 </resources>
--- a/python/mozboot/mozboot/osx.py
+++ b/python/mozboot/mozboot/osx.py
@@ -301,16 +301,17 @@ class OSXBootstrapper(BaseBootstrapper):
             # We need to install Python because Mercurial requires the Python
             # development headers which are missing from OS X (at least on
             # 10.8) and because the build system wants a version newer than
             # what Apple ships.
             ('python', 'python'),
             ('mercurial', 'mercurial'),
             ('git', 'git'),
             ('autoconf213', HOMEBREW_AUTOCONF213),
+            ('gnu-tar', 'gnu-tar'),
         ]
         self._ensure_homebrew_packages(packages)
 
     def ensure_homebrew_browser_packages(self):
         packages = [
             ('yasm', 'yasm'),
         ]
         self._ensure_homebrew_packages(packages)
@@ -367,17 +368,18 @@ class OSXBootstrapper(BaseBootstrapper):
         missing = [package for package in packages if package not in installed]
         if missing:
             print(PACKAGE_MANAGER_PACKAGES % ('MacPorts',))
             self.run_as_root([self.port, '-v', 'install'] + missing)
 
     def ensure_macports_system_packages(self):
         packages = ['python27',
                     'mercurial',
-                    'autoconf213']
+                    'autoconf213',
+                    'gnutar']
 
         self._ensure_macports_packages(packages)
         self.run_as_root([self.port, 'select', '--set', 'python', 'python27'])
 
     def ensure_macports_browser_packages(self):
         packages = ['yasm']
 
         self._ensure_macports_packages(packages)
--- a/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini
+++ b/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini
@@ -12,20 +12,20 @@ head = head_update.js
 tail =
 
 [marSuccessComplete.js]
 [marSuccessPartial.js]
 [marFailurePartial.js]
 [marStageSuccessComplete.js]
 [marStageSuccessPartial.js]
 [marVersionDowngrade.js]
-skip-if = os != 'win' && os != 'mac'
+skip-if = toolkit == 'gonk'
 reason = mar signing
 [marWrongChannel.js]
-skip-if = os != 'win' && os != 'mac'
+skip-if = toolkit == 'gonk'
 reason = mar signing
 [marStageFailurePartial.js]
 [marCallbackAppSuccessComplete_win.js]
 skip-if = os != 'win'
 [marCallbackAppSuccessPartial_win.js]
 skip-if = os != 'win'
 [marCallbackAppStageSuccessComplete_win.js]
 skip-if = os != 'win'
--- a/toolkit/mozapps/update/updater/updater.cpp
+++ b/toolkit/mozapps/update/updater/updater.cpp
@@ -115,17 +115,18 @@ static int ioprio_set(int which, int who
 #endif
 
 # define MAYBE_USE_HARD_LINKS 1
 static bool sUseHardLinks = true;
 #else
 # define MAYBE_USE_HARD_LINKS 0
 #endif
 
-#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && !defined(XP_MACOSX)
+#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && \
+    !defined(XP_MACOSX) && !defined(MOZ_WIDGET_GONK)
 #include "nss.h"
 #include "prerror.h"
 #endif
 
 #ifdef XP_WIN
 #include "updatehelper.h"
 
 // Closes the handle if valid and if the updater is elevated returns with the
@@ -2371,17 +2372,18 @@ int NS_main(int argc, NS_tchar **argv)
     unsetenv("LD_PRELOAD");
     execv(argv[0], argv);
     __android_log_print(ANDROID_LOG_INFO, "updater",
                         "execve failed: errno: %d. Exiting...", errno);
     _exit(1);
   }
 #endif
 
-#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && !defined(XP_MACOSX)
+#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && \
+    !defined(XP_MACOSX) && !defined(MOZ_WIDGET_GONK)
   // On Windows and Mac we rely on native APIs to do verifications so we don't
   // need to initialize NSS at all there.
   // Otherwise, minimize the amount of NSS we depend on by avoiding all the NSS
   // databases.
   if (NSS_NoDB_Init(NULL) != SECSuccess) {
    PRErrorCode error = PR_GetError();
    fprintf(stderr, "Could not initialize NSS: %s (%d)",
            PR_ErrorToName(error), (int) error);
@@ -2873,17 +2875,17 @@ int NS_main(int argc, NS_tchar **argv)
       CloseHandle(elevatedFileHandle);
 
       if (!useService && !noServiceFallback &&
           INVALID_HANDLE_VALUE == updateLockFileHandle) {
         // We didn't use the service and we did run the elevated updater.exe.
         // The elevated updater.exe is responsible for writing out the
         // update.status file.
         return 0;
-      } else if(useService) {
+      } else if (useService) {
         // The service command was launched. The service is responsible for
         // writing out the update.status file.
         if (updateLockFileHandle != INVALID_HANDLE_VALUE) {
           CloseHandle(updateLockFileHandle);
         }
         return 0;
       } else {
         // Otherwise the service command was not launched at all.
@@ -3239,16 +3241,17 @@ int NS_main(int argc, NS_tchar **argv)
     }
     EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 0);
 #endif /* XP_WIN */
 #ifdef XP_MACOSX
     if (gSucceeded) {
       LaunchMacPostProcess(gInstallDirPath);
     }
 #endif /* XP_MACOSX */
+
     LaunchCallbackApp(argv[5],
                       argc - callbackIndex,
                       argv + callbackIndex,
                       sUsingService);
   }
 
   return gSucceeded ? 0 : 1;
 }
--- a/toolkit/xre/nsUpdateDriver.cpp
+++ b/toolkit/xre/nsUpdateDriver.cpp
@@ -397,17 +397,18 @@ CopyUpdaterIntoUpdateDir(nsIFile *greDir
 }
 
 /**
  * Appends the specified path to the library path.
  * This is used so that updater can find libmozsqlite3.so and other shared libs.
  *
  * @param pathToAppend A new library path to prepend to LD_LIBRARY_PATH
  */
-#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && !defined(XP_MACOSX)
+#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && \
+    !defined(XP_MACOSX) && !defined(MOZ_WIDGET_GONK)
 #include "prprf.h"
 #define PATH_SEPARATOR ":"
 #define LD_LIBRARY_PATH_ENVVAR_NAME "LD_LIBRARY_PATH"
 static void
 AppendToLibPath(const char *pathToAppend)
 {
   char *pathValue = getenv(LD_LIBRARY_PATH_ENVVAR_NAME);
   if (nullptr == pathValue || '\0' == *pathValue) {
@@ -621,17 +622,18 @@ SwitchToUpdatedApp(nsIFile *greDir, nsIF
   } else {
     argc = 5;
     argv[5] = nullptr;
   }
 
   if (gSafeMode) {
     PR_SetEnv("MOZ_SAFE_MODE_RESTART=1");
   }
-#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && !defined(XP_MACOSX)
+#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && \
+    !defined(XP_MACOSX) && !defined(MOZ_WIDGET_GONK)
   AppendToLibPath(installDirPath.get());
 #endif
 
   LOG(("spawning updater process for replacing [%s]\n", updaterPath.get()));
 
 #if defined(USE_EXECV)
 # if defined(MOZ_WIDGET_GONK)
   // In Gonk, we preload libmozglue, which the updater process doesn't need.
@@ -901,17 +903,18 @@ ApplyUpdate(nsIFile *greDir, nsIFile *up
   } else {
     argc = 5;
     argv[5] = nullptr;
   }
 
   if (gSafeMode) {
     PR_SetEnv("MOZ_SAFE_MODE_RESTART=1");
   }
-#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && !defined(XP_MACOSX)
+#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && \
+    !defined(XP_MACOSX) && !defined(MOZ_WIDGET_GONK)
   AppendToLibPath(installDirPath.get());
 #endif
 
   if (isOSUpdate) {
     PR_SetEnv("MOZ_OS_UPDATE=1");
   }
 #if defined(MOZ_WIDGET_GONK)
   // We want the updater to be CPU friendly and not subject to being killed by