[NTP] Create service to fetch search suggestions

Bug: 904565
Change-Id: Ibe5112bb6a0b938c24315606ae5647a5ae6913fd
Reviewed-on: https://chromium-review.googlesource.com/c/1338122
Commit-Queue: Kyle Milka <[email protected]>
Reviewed-by: Ramya Nagarajan <[email protected]>
Reviewed-by: Robert Kaplow <[email protected]>
Reviewed-by: Nicolas Ouellet-Payeur <[email protected]>
Cr-Commit-Position: refs/heads/master@{#622104}
diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn
index c0f23ce..e471444 100644
--- a/chrome/browser/BUILD.gn
+++ b/chrome/browser/BUILD.gn
@@ -2993,6 +2993,16 @@
       "search/promos/promo_service_observer.h",
       "search/search_engine_base_url_tracker.cc",
       "search/search_engine_base_url_tracker.h",
+      "search/search_suggest/search_suggest_data.cc",
+      "search/search_suggest/search_suggest_data.h",
+      "search/search_suggest/search_suggest_loader.h",
+      "search/search_suggest/search_suggest_loader_impl.cc",
+      "search/search_suggest/search_suggest_loader_impl.h",
+      "search/search_suggest/search_suggest_service.cc",
+      "search/search_suggest/search_suggest_service.h",
+      "search/search_suggest/search_suggest_service_factory.cc",
+      "search/search_suggest/search_suggest_service_factory.h",
+      "search/search_suggest/search_suggest_service_observer.h",
       "signin/mutable_profile_oauth2_token_service_delegate.cc",
       "signin/mutable_profile_oauth2_token_service_delegate.h",
       "signin/signin_promo.cc",
diff --git a/chrome/browser/resources/local_ntp/local_ntp.js b/chrome/browser/resources/local_ntp/local_ntp.js
index 34cde5b..db59946da 100644
--- a/chrome/browser/resources/local_ntp/local_ntp.js
+++ b/chrome/browser/resources/local_ntp/local_ntp.js
@@ -124,6 +124,7 @@
   NTP_CONTENTS: 'ntp-contents',
   PROMO: 'promo',
   RESTORE_ALL_LINK: 'mv-restore',
+  SUGGESTIONS: 'suggestions',
   TILES: 'mv-tiles',
   TILES_IFRAME: 'mv-single',
   UNDO_LINK: 'mv-undo',
@@ -850,6 +851,10 @@
   if (cmd === 'loaded') {
     tilesAreLoaded = true;
     if (configData.isGooglePage) {
+      // Show search suggestions if they were previously hidden.
+      if ($(IDS.SUGGESTIONS)) {
+        $(IDS.SUGGESTIONS).style.visibility = 'visible';
+      }
       if (!$('one-google-loader')) {
         // Load the OneGoogleBar script. It'll create a global variable name
         // "og" which is a dict corresponding to the native OneGoogleBarData
@@ -908,6 +913,21 @@
   }
 }
 
+function showSearchSuggestions() {
+  // Inject search suggestions as early as possible to avoid shifting of other
+  // elements.
+  if (!$('search-suggestions-loader')) {
+    var ssScript = document.createElement('script');
+    ssScript.id = 'search-suggestions-loader';
+    ssScript.src = 'chrome-search://local-ntp/search-suggestions.js';
+    ssScript.async = false;
+    document.body.appendChild(ssScript);
+    ssScript.onload = function() {
+      injectSearchSuggestions(search_suggestions);
+    };
+  }
+}
+
 
 /**
  * Enables Material Design styles for the Most Visited section. Implicitly
@@ -971,6 +991,7 @@
   var searchboxApiHandle = embeddedSearchApiHandle.searchBox;
 
   if (configData.isGooglePage) {
+    showSearchSuggestions();
     enableMDIcons();
 
     ntpApiHandle.onaddcustomlinkdone = onAddCustomLinkDone;
@@ -1184,6 +1205,23 @@
 
 
 /**
+ * Injects search suggestions into the page. Called *synchronously* with cached
+ * data as not to cause shifting of the most visited tiles.
+ */
+function injectSearchSuggestions(suggestions) {
+  if (suggestions.suggestionsHtml === '') {
+    return;
+  }
+
+  let suggestionsContainer = document.createElement('div');
+  suggestionsContainer.id = IDS.SUGGESTIONS;
+  suggestionsContainer.style.visibility = 'hidden';
+  suggestionsContainer.innerHTML += suggestions.suggestionsHtml;
+  $(IDS.NTP_CONTENTS).insertBefore(suggestionsContainer, $('most-visited'));
+}
+
+
+/**
  * Injects the One Google Bar into the page. Called asynchronously, so that it
  * doesn't block the main page load.
  */
diff --git a/chrome/browser/search/README.md b/chrome/browser/search/README.md
index 5115c2d..b802e4c 100644
--- a/chrome/browser/search/README.md
+++ b/chrome/browser/search/README.md
@@ -120,6 +120,15 @@
 and if the user interacts with it, the NTP moves keyboard focus and any
 text to the Omnibox and hides the Fakebox.
 
+##### Search Suggestions
+
+Above the NTP tiles there is space for search suggestions. Search suggestions
+are typically 3-4 queries recommended to logged-in users based on their previous
+search history.
+
+Search suggestions are fetched from Google servers on NTP load and cached to be
+displayed on the following NTP load.
+
 ##### Middle-slot Promos
 
 Below the NTP tiles, there is space for a **Middle-slot Promo**. A promo is
diff --git a/chrome/browser/search/local_ntp_source.cc b/chrome/browser/search/local_ntp_source.cc
index d842aa4..a7921545 100644
--- a/chrome/browser/search/local_ntp_source.cc
+++ b/chrome/browser/search/local_ntp_source.cc
@@ -37,6 +37,9 @@
 #include "chrome/browser/search/promos/promo_service.h"
 #include "chrome/browser/search/promos/promo_service_factory.h"
 #include "chrome/browser/search/search.h"
+#include "chrome/browser/search/search_suggest/search_suggest_data.h"
+#include "chrome/browser/search/search_suggest/search_suggest_service.h"
+#include "chrome/browser/search/search_suggest/search_suggest_service_factory.h"
 #include "chrome/browser/search_engines/template_url_service_factory.h"
 #include "chrome/browser/search_provider_logos/logo_service_factory.h"
 #include "chrome/browser/themes/theme_properties.h"
@@ -95,6 +98,7 @@
 const char kNtpBackgroundImageScriptFilename[] = "ntp-background-images.js";
 const char kOneGoogleBarScriptFilename[] = "one-google.js";
 const char kPromoScriptFilename[] = "promo.js";
+const char kSearchSuggestionsScriptFilename[] = "search-suggestions.js";
 const char kThemeCSSFilename[] = "theme.css";
 
 const struct Resource{
@@ -124,6 +128,7 @@
     {kNtpBackgroundImageScriptFilename, kLocalResource, "text/javascript"},
     {kOneGoogleBarScriptFilename, kLocalResource, "text/javascript"},
     {kPromoScriptFilename, kLocalResource, "text/javascript"},
+    {kSearchSuggestionsScriptFilename, kLocalResource, "text/javascript"},
     {kThemeCSSFilename, kLocalResource, "text/css"},
     // Image may not be a jpeg but the .jpg extension here still works for other
     // filetypes. Special handling for different extensions isn't worth the
@@ -403,6 +408,13 @@
   return result;
 }
 
+std::unique_ptr<base::DictionaryValue> ConvertSearchSuggestDataToDict(
+    const SearchSuggestData& data) {
+  auto result = std::make_unique<base::DictionaryValue>();
+  result->SetString("suggestionsHtml", data.suggestions_html);
+  return result;
+}
+
 std::string ConvertLogoImageToBase64(const EncodedLogo& logo) {
   std::string base64;
   base::Base64Encode(logo.encoded_image->data(), &base64);
@@ -702,6 +714,9 @@
       one_google_bar_service_observer_(this),
       promo_service_(PromoServiceFactory::GetForProfile(profile_)),
       promo_service_observer_(this),
+      search_suggest_service_(
+          SearchSuggestServiceFactory::GetForProfile(profile_)),
+      search_suggest_service_observer_(this),
       logo_service_(nullptr),
       weak_ptr_factory_(this) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
@@ -716,6 +731,11 @@
   if (one_google_bar_service_)
     one_google_bar_service_observer_.Add(one_google_bar_service_);
 
+  // |search_suggest_service_| is null in incognito, or when the feature is
+  // disabled.
+  if (search_suggest_service_)
+    search_suggest_service_observer_.Add(search_suggest_service_);
+
   // |promo_service_| is null in incognito, or when the feature is
   // disabled.
   if (promo_service_)
@@ -858,6 +878,20 @@
     return;
   }
 
+  if (stripped_path == kSearchSuggestionsScriptFilename) {
+    if (!search_suggest_service_) {
+      callback.Run(nullptr);
+      return;
+    }
+
+    MaybeServeSearchSuggestions(callback);
+
+    search_suggest_requests_.emplace_back(base::TimeTicks::Now());
+    search_suggest_service_->Refresh();
+
+    return;
+  }
+
   if (stripped_path == kDoodleScriptFilename) {
     if (!logo_service_) {
       callback.Run(nullptr);
@@ -1198,6 +1232,51 @@
   promo_service_ = nullptr;
 }
 
+void LocalNtpSource::OnSearchSuggestDataUpdated() {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+  bool result = search_suggest_service_->search_suggest_data().has_value();
+  base::TimeTicks now = base::TimeTicks::Now();
+  for (const auto& request : search_suggest_requests_) {
+    base::TimeDelta delta = now - request.start_time;
+    UMA_HISTOGRAM_MEDIUM_TIMES("NewTabPage.SearchSuggestions.RequestLatency",
+                               delta);
+    if (result) {
+      UMA_HISTOGRAM_MEDIUM_TIMES(
+          "NewTabPage.SearchSuggestions.RequestLatency.Success", delta);
+    } else {
+      UMA_HISTOGRAM_MEDIUM_TIMES(
+          "NewTabPage.SearchSuggestions.RequestLatency.Failure", delta);
+    }
+  }
+  search_suggest_requests_.clear();
+}
+
+void LocalNtpSource::OnSearchSuggestServiceShuttingDown() {
+  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
+
+  search_suggest_service_observer_.RemoveAll();
+  search_suggest_service_ = nullptr;
+}
+void LocalNtpSource::MaybeServeSearchSuggestions(
+    const content::URLDataSource::GotDataCallback& callback) {
+  base::Optional<SearchSuggestData> data =
+      search_suggest_service_->search_suggest_data();
+  if (!data.has_value()) {
+    callback.Run(nullptr);
+    return;
+  }
+
+  SearchSuggestData suggest_data = *data;
+  search_suggest_service_->ClearSearchSuggestData();
+  scoped_refptr<base::RefCountedString> result;
+  std::string js;
+  base::JSONWriter::Write(*ConvertSearchSuggestDataToDict(suggest_data), &js);
+  js = "var search_suggestions  = " + js + ";";
+  result = base::RefCountedString::TakeString(&js);
+  callback.Run(result);
+}
+
 void LocalNtpSource::ServeOneGoogleBar(
     const base::Optional<OneGoogleBarData>& data) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
@@ -1285,3 +1364,12 @@
 LocalNtpSource::PromoRequest::PromoRequest(const PromoRequest&) = default;
 
 LocalNtpSource::PromoRequest::~PromoRequest() = default;
+
+LocalNtpSource::SearchSuggestRequest::SearchSuggestRequest(
+    base::TimeTicks start_time)
+    : start_time(start_time) {}
+
+LocalNtpSource::SearchSuggestRequest::SearchSuggestRequest(
+    const SearchSuggestRequest&) = default;
+
+LocalNtpSource::SearchSuggestRequest::~SearchSuggestRequest() = default;
diff --git a/chrome/browser/search/local_ntp_source.h b/chrome/browser/search/local_ntp_source.h
index b7858d9..0715b220 100644
--- a/chrome/browser/search/local_ntp_source.h
+++ b/chrome/browser/search/local_ntp_source.h
@@ -18,6 +18,7 @@
 #include "chrome/browser/search/background/ntp_background_service_observer.h"
 #include "chrome/browser/search/one_google_bar/one_google_bar_service_observer.h"
 #include "chrome/browser/search/promos/promo_service_observer.h"
+#include "chrome/browser/search/search_suggest/search_suggest_service_observer.h"
 #include "components/prefs/pref_registry_simple.h"
 #include "content/public/browser/url_data_source.h"
 
@@ -30,6 +31,7 @@
 class NtpBackgroundService;
 class OneGoogleBarService;
 class PromoService;
+class SearchSuggestService;
 class Profile;
 
 namespace search_provider_logos {
@@ -46,7 +48,8 @@
 class LocalNtpSource : public content::URLDataSource,
                        public NtpBackgroundServiceObserver,
                        public OneGoogleBarServiceObserver,
-                       public PromoServiceObserver {
+                       public PromoServiceObserver,
+                       public SearchSuggestServiceObserver {
  public:
   explicit LocalNtpSource(Profile* profile);
   ~LocalNtpSource() override;
@@ -87,6 +90,14 @@
     content::URLDataSource::GotDataCallback callback;
   };
 
+  struct SearchSuggestRequest {
+    explicit SearchSuggestRequest(base::TimeTicks start_time);
+    explicit SearchSuggestRequest(const SearchSuggestRequest&);
+    ~SearchSuggestRequest();
+
+    base::TimeTicks start_time;
+  };
+
   // Overridden from content::URLDataSource:
   std::string GetSource() const override;
   void StartDataRequest(
@@ -118,10 +129,17 @@
   void OnPromoDataUpdated() override;
   void OnPromoServiceShuttingDown() override;
 
+  // Overridden from SearchSuggestServiceObserver:
+  void OnSearchSuggestDataUpdated() override;
+  void OnSearchSuggestServiceShuttingDown() override;
+
   void ServeOneGoogleBar(const base::Optional<OneGoogleBarData>& data);
 
   void ServePromo(const base::Optional<PromoData>& data);
 
+  void MaybeServeSearchSuggestions(
+      const content::URLDataSource::GotDataCallback& callback);
+
   Profile* const profile_;
 
   std::vector<NtpBackgroundRequest> ntp_background_collections_requests_;
@@ -147,6 +165,13 @@
 
   ScopedObserver<PromoService, PromoServiceObserver> promo_service_observer_;
 
+  std::vector<SearchSuggestRequest> search_suggest_requests_;
+
+  SearchSuggestService* search_suggest_service_;
+
+  ScopedObserver<SearchSuggestService, SearchSuggestServiceObserver>
+      search_suggest_service_observer_;
+
   search_provider_logos::LogoService* logo_service_;
   std::unique_ptr<DesktopLogoObserver> logo_observer_;
 
diff --git a/chrome/browser/search/search_suggest/search_suggest_data.cc b/chrome/browser/search/search_suggest/search_suggest_data.cc
new file mode 100644
index 0000000..329f5b24
--- /dev/null
+++ b/chrome/browser/search/search_suggest/search_suggest_data.cc
@@ -0,0 +1,22 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/search/search_suggest/search_suggest_data.h"
+
+SearchSuggestData::SearchSuggestData() = default;
+SearchSuggestData::SearchSuggestData(const SearchSuggestData&) = default;
+SearchSuggestData::SearchSuggestData(SearchSuggestData&&) = default;
+SearchSuggestData::~SearchSuggestData() = default;
+
+SearchSuggestData& SearchSuggestData::operator=(const SearchSuggestData&) =
+    default;
+SearchSuggestData& SearchSuggestData::operator=(SearchSuggestData&&) = default;
+
+bool operator==(const SearchSuggestData& lhs, const SearchSuggestData& rhs) {
+  return lhs.suggestions_html == rhs.suggestions_html;
+}
+
+bool operator!=(const SearchSuggestData& lhs, const SearchSuggestData& rhs) {
+  return !(lhs == rhs);
+}
diff --git a/chrome/browser/search/search_suggest/search_suggest_data.h b/chrome/browser/search/search_suggest/search_suggest_data.h
new file mode 100644
index 0000000..f756324
--- /dev/null
+++ b/chrome/browser/search/search_suggest/search_suggest_data.h
@@ -0,0 +1,28 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_DATA_H_
+#define CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_DATA_H_
+
+#include <string>
+
+// This struct contains all the data needed to inject search suggestions into a
+// page.
+struct SearchSuggestData {
+  SearchSuggestData();
+  SearchSuggestData(const SearchSuggestData&);
+  SearchSuggestData(SearchSuggestData&&);
+  ~SearchSuggestData();
+
+  SearchSuggestData& operator=(const SearchSuggestData&);
+  SearchSuggestData& operator=(SearchSuggestData&&);
+
+  // The HTML for the search suggestions.
+  std::string suggestions_html;
+};
+
+bool operator==(const SearchSuggestData& lhs, const SearchSuggestData& rhs);
+bool operator!=(const SearchSuggestData& lhs, const SearchSuggestData& rhs);
+
+#endif  // CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_DATA_H_
diff --git a/chrome/browser/search/search_suggest/search_suggest_loader.h b/chrome/browser/search/search_suggest/search_suggest_loader.h
new file mode 100644
index 0000000..94c93a56
--- /dev/null
+++ b/chrome/browser/search/search_suggest/search_suggest_loader.h
@@ -0,0 +1,44 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_LOADER_H_
+#define CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_LOADER_H_
+
+#include "base/callback_forward.h"
+#include "base/optional.h"
+
+class GURL;
+struct SearchSuggestData;
+
+// Interface for loading SearchSuggestData over the network.
+class SearchSuggestLoader {
+ public:
+  enum class Status {
+    // Received a valid response.
+    OK,
+    // User is signed out, no request was made.
+    SIGNED_OUT,
+    // Some transient error occurred, e.g. the network request failed because
+    // there is no network connectivity. A previously cached response may still
+    // be used.
+    TRANSIENT_ERROR,
+    // A fatal error occurred, such as the server responding with an error code
+    // or with invalid data. Any previously cached response should be cleared.
+    FATAL_ERROR
+  };
+  using SearchSuggestionsCallback =
+      base::OnceCallback<void(Status,
+                              const base::Optional<SearchSuggestData>&)>;
+
+  virtual ~SearchSuggestLoader() = default;
+
+  // Initiates a load from the network. On completion (successful or not), the
+  // callback will be called with the result, which will be nullopt on failure.
+  virtual void Load(SearchSuggestionsCallback callback) = 0;
+
+  // Retrieves the URL from which SearchSuggestData will be loaded.
+  virtual GURL GetLoadURLForTesting() const = 0;
+};
+
+#endif  // CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_LOADER_H_
diff --git a/chrome/browser/search/search_suggest/search_suggest_loader_impl.cc b/chrome/browser/search/search_suggest/search_suggest_loader_impl.cc
new file mode 100644
index 0000000..ca80ed8
--- /dev/null
+++ b/chrome/browser/search/search_suggest/search_suggest_loader_impl.cc
@@ -0,0 +1,250 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/search/search_suggest/search_suggest_loader_impl.h"
+
+#include <string>
+#include <utility>
+
+#include "base/callback.h"
+#include "base/json/json_writer.h"
+#include "base/strings/string_util.h"
+#include "base/values.h"
+#include "chrome/browser/search/search_suggest/search_suggest_data.h"
+#include "chrome/common/chrome_content_client.h"
+#include "chrome/common/webui_url_constants.h"
+#include "components/google/core/browser/google_url_tracker.h"
+#include "components/google/core/common/google_util.h"
+#include "components/signin/core/browser/chrome_connected_header_helper.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "components/variations/net/variations_http_headers.h"
+#include "content/public/common/service_manager_connection.h"
+#include "net/base/load_flags.h"
+#include "net/base/url_util.h"
+#include "net/http/http_status_code.h"
+#include "net/traffic_annotation/network_traffic_annotation.h"
+#include "services/data_decoder/public/cpp/safe_json_parser.h"
+#include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/cpp/shared_url_loader_factory.h"
+#include "services/network/public/cpp/simple_url_loader.h"
+#include "url/gurl.h"
+
+namespace {
+
+const char kNewTabSearchSuggestionsApiPath[] = "/async/newtab_suggestions";
+
+const char kSearchSuggestResponsePreamble[] = ")]}'";
+
+base::Optional<SearchSuggestData> JsonToSearchSuggestionData(
+    const base::Value& value) {
+  const base::DictionaryValue* dict = nullptr;
+  if (!value.GetAsDictionary(&dict)) {
+    DLOG(WARNING) << "Parse error: top-level dictionary not found";
+    return base::nullopt;
+  }
+
+  const base::DictionaryValue* update = nullptr;
+  if (!dict->GetDictionary("update", &update)) {
+    DLOG(WARNING) << "Parse error: no update";
+    return base::nullopt;
+  }
+
+  // TODO(crbug.com/919905): Investigate if SafeHtml should be used here.
+  std::string search_suggestions = std::string();
+  if (!update->GetString("search_suggestions", &search_suggestions)) {
+    DLOG(WARNING) << "Parse error: no search_suggestions";
+    return base::nullopt;
+  }
+
+  SearchSuggestData result;
+  result.suggestions_html = search_suggestions;
+
+  return result;
+}
+
+}  // namespace
+
+class SearchSuggestLoaderImpl::AuthenticatedURLLoader {
+ public:
+  using LoadDoneCallback =
+      base::OnceCallback<void(const network::SimpleURLLoader* simple_loader,
+                              std::unique_ptr<std::string> response_body)>;
+
+  AuthenticatedURLLoader(
+      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
+      GURL api_url,
+      LoadDoneCallback callback);
+  ~AuthenticatedURLLoader() = default;
+
+  void Start();
+
+ private:
+  net::HttpRequestHeaders GetRequestHeaders() const;
+
+  void OnURLLoaderComplete(std::unique_ptr<std::string> response_body);
+
+  scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
+  const GURL api_url_;
+
+  LoadDoneCallback callback_;
+
+  // The underlying SimpleURLLoader which does the actual load.
+  std::unique_ptr<network::SimpleURLLoader> simple_loader_;
+};
+
+SearchSuggestLoaderImpl::AuthenticatedURLLoader::AuthenticatedURLLoader(
+    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
+    GURL api_url,
+    LoadDoneCallback callback)
+    : url_loader_factory_(url_loader_factory),
+      api_url_(std::move(api_url)),
+      callback_(std::move(callback)) {}
+
+net::HttpRequestHeaders
+SearchSuggestLoaderImpl::AuthenticatedURLLoader::GetRequestHeaders() const {
+  net::HttpRequestHeaders headers;
+  variations::AppendVariationHeadersUnknownSignedIn(
+      api_url_, variations::InIncognito::kNo, &headers);
+  return headers;
+}
+
+void SearchSuggestLoaderImpl::AuthenticatedURLLoader::Start() {
+  net::NetworkTrafficAnnotationTag traffic_annotation =
+      net::DefineNetworkTrafficAnnotation("search_suggest_service", R"(
+        semantics {
+          sender: "Search Suggestion Service"
+          description:
+            "Downloads search suggestions to be shown on the New Tab Page to "
+            "logged-in users based on their previous search history."
+          trigger:
+            "Displaying the new tab page, if Google is the "
+            "configured search provider, and the user is signed in."
+          data: "Google credentials if user is signed in."
+          destination: GOOGLE_OWNED_SERVICE
+        }
+        policy {
+          cookies_allowed: YES
+          cookies_store: "user"
+          setting:
+            "Users can control this feature via selecting a non-Google default "
+            "search engine in Chrome settings under 'Search Engine'. Users can "
+            "opt out of this feature using a button attached to the suggestions."
+          chrome_policy {
+            DefaultSearchProviderEnabled {
+              policy_options {mode: MANDATORY}
+              DefaultSearchProviderEnabled: false
+            }
+          }
+        })");
+
+  auto resource_request = std::make_unique<network::ResourceRequest>();
+  resource_request->url = api_url_;
+  resource_request->headers = GetRequestHeaders();
+  resource_request->request_initiator =
+      url::Origin::Create(GURL(chrome::kChromeUINewTabURL));
+
+  simple_loader_ = network::SimpleURLLoader::Create(std::move(resource_request),
+                                                    traffic_annotation);
+  simple_loader_->DownloadToString(
+      url_loader_factory_.get(),
+      base::BindOnce(
+          &SearchSuggestLoaderImpl::AuthenticatedURLLoader::OnURLLoaderComplete,
+          base::Unretained(this)),
+      1024 * 1024);
+}
+
+void SearchSuggestLoaderImpl::AuthenticatedURLLoader::OnURLLoaderComplete(
+    std::unique_ptr<std::string> response_body) {
+  std::move(callback_).Run(simple_loader_.get(), std::move(response_body));
+}
+
+SearchSuggestLoaderImpl::SearchSuggestLoaderImpl(
+    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
+    GoogleURLTracker* google_url_tracker,
+    const std::string& application_locale)
+    : url_loader_factory_(url_loader_factory),
+      google_url_tracker_(google_url_tracker),
+      application_locale_(application_locale),
+      weak_ptr_factory_(this) {}
+
+SearchSuggestLoaderImpl::~SearchSuggestLoaderImpl() = default;
+
+void SearchSuggestLoaderImpl::Load(SearchSuggestionsCallback callback) {
+  callbacks_.push_back(std::move(callback));
+
+  // Note: If there is an ongoing request, abandon it. It's possible that
+  // something has changed in the meantime (e.g. signin state) that would make
+  // the result obsolete.
+  pending_request_ = std::make_unique<AuthenticatedURLLoader>(
+      url_loader_factory_, GetApiUrl(),
+      base::BindOnce(&SearchSuggestLoaderImpl::LoadDone,
+                     base::Unretained(this)));
+  pending_request_->Start();
+}
+
+GURL SearchSuggestLoaderImpl::GetLoadURLForTesting() const {
+  return GetApiUrl();
+}
+
+GURL SearchSuggestLoaderImpl::GetApiUrl() const {
+  GURL google_base_url = google_util::CommandLineGoogleBaseURL();
+  if (!google_base_url.is_valid()) {
+    google_base_url = google_url_tracker_->google_url();
+  }
+
+  GURL api_url = google_base_url.Resolve(kNewTabSearchSuggestionsApiPath);
+
+  return api_url;
+}
+
+void SearchSuggestLoaderImpl::LoadDone(
+    const network::SimpleURLLoader* simple_loader,
+    std::unique_ptr<std::string> response_body) {
+  // The loader will be deleted when the request is handled.
+  std::unique_ptr<AuthenticatedURLLoader> deleter(std::move(pending_request_));
+
+  if (!response_body) {
+    // This represents network errors (i.e. the server did not provide a
+    // response).
+    DLOG(WARNING) << "Request failed with error: " << simple_loader->NetError();
+    Respond(Status::TRANSIENT_ERROR, base::nullopt);
+    return;
+  }
+
+  std::string response;
+  response.swap(*response_body);
+
+  // The response may start with )]}'. Ignore this.
+  if (base::StartsWith(response, kSearchSuggestResponsePreamble,
+                       base::CompareCase::SENSITIVE)) {
+    response = response.substr(strlen(kSearchSuggestResponsePreamble));
+  }
+
+  data_decoder::SafeJsonParser::Parse(
+      content::ServiceManagerConnection::GetForProcess()->GetConnector(),
+      response,
+      base::BindRepeating(&SearchSuggestLoaderImpl::JsonParsed,
+                          weak_ptr_factory_.GetWeakPtr()),
+      base::BindRepeating(&SearchSuggestLoaderImpl::JsonParseFailed,
+                          weak_ptr_factory_.GetWeakPtr()));
+}
+
+void SearchSuggestLoaderImpl::JsonParsed(std::unique_ptr<base::Value> value) {
+  base::Optional<SearchSuggestData> result = JsonToSearchSuggestionData(*value);
+  Respond(result.has_value() ? Status::OK : Status::FATAL_ERROR, result);
+}
+
+void SearchSuggestLoaderImpl::JsonParseFailed(const std::string& message) {
+  DLOG(WARNING) << "Parsing JSON failed: " << message;
+  Respond(Status::FATAL_ERROR, base::nullopt);
+}
+
+void SearchSuggestLoaderImpl::Respond(
+    Status status,
+    const base::Optional<SearchSuggestData>& data) {
+  for (auto& callback : callbacks_) {
+    std::move(callback).Run(status, data);
+  }
+  callbacks_.clear();
+}
diff --git a/chrome/browser/search/search_suggest/search_suggest_loader_impl.h b/chrome/browser/search/search_suggest/search_suggest_loader_impl.h
new file mode 100644
index 0000000..d3438a4
--- /dev/null
+++ b/chrome/browser/search/search_suggest/search_suggest_loader_impl.h
@@ -0,0 +1,67 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_LOADER_IMPL_H_
+#define CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_LOADER_IMPL_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "base/macros.h"
+#include "base/memory/weak_ptr.h"
+#include "base/optional.h"
+#include "chrome/browser/search/search_suggest/search_suggest_loader.h"
+
+class GoogleURLTracker;
+
+namespace base {
+class Value;
+}
+
+namespace network {
+class SimpleURLLoader;
+class SharedURLLoaderFactory;
+}  // namespace network
+
+struct SearchSuggestData;
+
+class SearchSuggestLoaderImpl : public SearchSuggestLoader {
+ public:
+  SearchSuggestLoaderImpl(
+      scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
+      GoogleURLTracker* google_url_tracker,
+      const std::string& application_locale);
+  ~SearchSuggestLoaderImpl() override;
+
+  void Load(SearchSuggestionsCallback callback) override;
+
+  GURL GetLoadURLForTesting() const override;
+
+ private:
+  class AuthenticatedURLLoader;
+
+  GURL GetApiUrl() const;
+
+  void LoadDone(const network::SimpleURLLoader* simple_loader,
+                std::unique_ptr<std::string> response_body);
+
+  void JsonParsed(std::unique_ptr<base::Value> value);
+  void JsonParseFailed(const std::string& message);
+
+  void Respond(Status status, const base::Optional<SearchSuggestData>& data);
+
+  scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory_;
+  GoogleURLTracker* google_url_tracker_;
+  const std::string application_locale_;
+
+  std::vector<SearchSuggestionsCallback> callbacks_;
+  std::unique_ptr<AuthenticatedURLLoader> pending_request_;
+
+  base::WeakPtrFactory<SearchSuggestLoaderImpl> weak_ptr_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(SearchSuggestLoaderImpl);
+};
+
+#endif  // CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_LOADER_IMPL_H_
diff --git a/chrome/browser/search/search_suggest/search_suggest_loader_impl_unittest.cc b/chrome/browser/search/search_suggest/search_suggest_loader_impl_unittest.cc
new file mode 100644
index 0000000..fa810df
--- /dev/null
+++ b/chrome/browser/search/search_suggest/search_suggest_loader_impl_unittest.cc
@@ -0,0 +1,250 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/search/search_suggest/search_suggest_loader_impl.h"
+
+#include "base/macros.h"
+#include "base/memory/ref_counted.h"
+#include "base/optional.h"
+#include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
+#include "base/test/bind_test_util.h"
+#include "base/test/mock_callback.h"
+#include "base/test/test_simple_task_runner.h"
+#include "base/time/time.h"
+#include "chrome/browser/search/search_suggest/search_suggest_data.h"
+#include "components/google/core/browser/google_url_tracker.h"
+#include "components/signin/core/browser/signin_header_helper.h"
+#include "content/public/test/test_browser_thread_bundle.h"
+#include "content/public/test/test_service_manager_context.h"
+#include "net/http/http_request_headers.h"
+#include "net/http/http_status_code.h"
+#include "services/data_decoder/public/cpp/testing_json_parser.h"
+#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
+#include "services/network/public/mojom/url_loader_factory.mojom.h"
+#include "services/network/test/test_network_connection_tracker.h"
+#include "services/network/test/test_url_loader_factory.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using testing::_;
+using testing::Eq;
+using testing::IsEmpty;
+using testing::SaveArg;
+using testing::StartsWith;
+
+namespace {
+
+const char kApplicationLocale[] = "us";
+
+const char kMinimalValidResponse[] = R"json({"update": { "search_suggestions":""
+}})json";
+
+// Required to instantiate a GoogleUrlTracker in UNIT_TEST_MODE.
+class GoogleURLTrackerClientStub : public GoogleURLTrackerClient {
+ public:
+  GoogleURLTrackerClientStub() {}
+  ~GoogleURLTrackerClientStub() override {}
+
+  bool IsBackgroundNetworkingEnabled() override { return true; }
+  PrefService* GetPrefs() override { return nullptr; }
+  network::SharedURLLoaderFactory* GetURLLoaderFactory() override {
+    return nullptr;
+  }
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(GoogleURLTrackerClientStub);
+};
+
+}  // namespace
+
+ACTION_P(Quit, run_loop) {
+  run_loop->Quit();
+}
+
+class SearchSuggestLoaderImplTest : public testing::Test {
+ public:
+  SearchSuggestLoaderImplTest()
+      : SearchSuggestLoaderImplTest(
+            /*account_consistency_mirror_required=*/false) {}
+
+  explicit SearchSuggestLoaderImplTest(bool account_consistency_mirror_required)
+      : thread_bundle_(content::TestBrowserThreadBundle::IO_MAINLOOP),
+        google_url_tracker_(
+            std::make_unique<GoogleURLTrackerClientStub>(),
+            GoogleURLTracker::ALWAYS_DOT_COM_MODE,
+            network::TestNetworkConnectionTracker::GetInstance()),
+        test_shared_loader_factory_(
+            base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(
+                &test_url_loader_factory_)) {}
+
+  ~SearchSuggestLoaderImplTest() override {
+    static_cast<KeyedService&>(google_url_tracker_).Shutdown();
+  }
+
+  void SetUp() override {
+    testing::Test::SetUp();
+
+    search_suggest_loader_ = std::make_unique<SearchSuggestLoaderImpl>(
+        test_shared_loader_factory_, &google_url_tracker_, kApplicationLocale);
+  }
+
+  void SetUpResponseWithData(const std::string& response) {
+    test_url_loader_factory_.SetInterceptor(base::BindLambdaForTesting(
+        [&](const network::ResourceRequest& request) {
+          last_request_url_ = request.url;
+          last_request_headers_ = request.headers;
+        }));
+    test_url_loader_factory_.AddResponse(
+        search_suggest_loader_->GetLoadURLForTesting().spec(), response);
+  }
+
+  void SetUpResponseWithNetworkError() {
+    test_url_loader_factory_.AddResponse(
+        search_suggest_loader_->GetLoadURLForTesting(),
+        network::ResourceResponseHead(), std::string(),
+        network::URLLoaderCompletionStatus(net::HTTP_NOT_FOUND));
+  }
+
+  SearchSuggestLoaderImpl* search_suggest_loader() {
+    return search_suggest_loader_.get();
+  }
+
+  GURL last_request_url() { return last_request_url_; }
+  net::HttpRequestHeaders last_request_headers() {
+    return last_request_headers_;
+  }
+
+ private:
+  // variations::AppendVariationHeaders and SafeJsonParser require a
+  // thread and a ServiceManagerConnection to be set.
+  content::TestBrowserThreadBundle thread_bundle_;
+  content::TestServiceManagerContext service_manager_context_;
+
+  data_decoder::TestingJsonParser::ScopedFactoryOverride factory_override_;
+
+  GoogleURLTracker google_url_tracker_;
+  network::TestURLLoaderFactory test_url_loader_factory_;
+  scoped_refptr<network::SharedURLLoaderFactory> test_shared_loader_factory_;
+
+  GURL last_request_url_;
+  net::HttpRequestHeaders last_request_headers_;
+
+  std::unique_ptr<SearchSuggestLoaderImpl> search_suggest_loader_;
+};
+
+TEST_F(SearchSuggestLoaderImplTest, RequestReturns) {
+  SetUpResponseWithData(kMinimalValidResponse);
+
+  base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
+  search_suggest_loader()->Load(callback.Get());
+
+  base::Optional<SearchSuggestData> data;
+  base::RunLoop loop;
+  EXPECT_CALL(callback, Run(SearchSuggestLoader::Status::OK, _))
+      .WillOnce(DoAll(SaveArg<1>(&data), Quit(&loop)));
+  loop.Run();
+
+  EXPECT_TRUE(data.has_value());
+}
+
+TEST_F(SearchSuggestLoaderImplTest, HandlesResponsePreamble) {
+  // The response may contain a ")]}'" prefix. The loader should ignore that
+  // during parsing.
+  SetUpResponseWithData(std::string(")]}'") + kMinimalValidResponse);
+
+  base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
+  search_suggest_loader()->Load(callback.Get());
+
+  base::Optional<SearchSuggestData> data;
+  base::RunLoop loop;
+  EXPECT_CALL(callback, Run(SearchSuggestLoader::Status::OK, _))
+      .WillOnce(DoAll(SaveArg<1>(&data), Quit(&loop)));
+  loop.Run();
+
+  EXPECT_TRUE(data.has_value());
+}
+
+TEST_F(SearchSuggestLoaderImplTest, ParsesFullResponse) {
+  SetUpResponseWithData(
+      R"json({"update": { "search_suggestions" : "<div></div>"}})json");
+
+  base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
+  search_suggest_loader()->Load(callback.Get());
+
+  base::Optional<SearchSuggestData> data;
+  base::RunLoop loop;
+  EXPECT_CALL(callback, Run(SearchSuggestLoader::Status::OK, _))
+      .WillOnce(DoAll(SaveArg<1>(&data), Quit(&loop)));
+  loop.Run();
+
+  ASSERT_TRUE(data.has_value());
+  EXPECT_THAT(data->suggestions_html, Eq("<div></div>"));
+}
+
+TEST_F(SearchSuggestLoaderImplTest, CoalescesMultipleRequests) {
+  SetUpResponseWithData(kMinimalValidResponse);
+
+  // Trigger two requests.
+  base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback>
+      first_callback;
+  search_suggest_loader()->Load(first_callback.Get());
+  base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback>
+      second_callback;
+  search_suggest_loader()->Load(second_callback.Get());
+
+  // Make sure that a single response causes both callbacks to be called.
+  base::Optional<SearchSuggestData> first_data;
+  base::Optional<SearchSuggestData> second_data;
+
+  base::RunLoop loop;
+  EXPECT_CALL(first_callback, Run(SearchSuggestLoader::Status::OK, _))
+      .WillOnce(SaveArg<1>(&first_data));
+  EXPECT_CALL(second_callback, Run(SearchSuggestLoader::Status::OK, _))
+      .WillOnce(DoAll(SaveArg<1>(&second_data), Quit(&loop)));
+  loop.Run();
+
+  // Ensure that both requests received a response.
+  EXPECT_TRUE(first_data.has_value());
+  EXPECT_TRUE(second_data.has_value());
+}
+
+TEST_F(SearchSuggestLoaderImplTest, NetworkErrorIsTransient) {
+  SetUpResponseWithNetworkError();
+
+  base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
+  search_suggest_loader()->Load(callback.Get());
+
+  base::RunLoop loop;
+  EXPECT_CALL(callback, Run(SearchSuggestLoader::Status::TRANSIENT_ERROR,
+                            Eq(base::nullopt)))
+      .WillOnce(Quit(&loop));
+  loop.Run();
+}
+
+TEST_F(SearchSuggestLoaderImplTest, InvalidJsonErrorIsFatal) {
+  SetUpResponseWithData(kMinimalValidResponse + std::string(")"));
+
+  base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
+  search_suggest_loader()->Load(callback.Get());
+
+  base::RunLoop loop;
+  EXPECT_CALL(callback,
+              Run(SearchSuggestLoader::Status::FATAL_ERROR, Eq(base::nullopt)))
+      .WillOnce(Quit(&loop));
+  loop.Run();
+}
+
+TEST_F(SearchSuggestLoaderImplTest, IncompleteJsonErrorIsFatal) {
+  SetUpResponseWithData(R"json({"update": {}})json");
+
+  base::MockCallback<SearchSuggestLoader::SearchSuggestionsCallback> callback;
+  search_suggest_loader()->Load(callback.Get());
+
+  base::RunLoop loop;
+  EXPECT_CALL(callback,
+              Run(SearchSuggestLoader::Status::FATAL_ERROR, Eq(base::nullopt)))
+      .WillOnce(Quit(&loop));
+  loop.Run();
+}
diff --git a/chrome/browser/search/search_suggest/search_suggest_service.cc b/chrome/browser/search/search_suggest/search_suggest_service.cc
new file mode 100644
index 0000000..8eec6f6
--- /dev/null
+++ b/chrome/browser/search/search_suggest/search_suggest_service.cc
@@ -0,0 +1,108 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/search/search_suggest/search_suggest_service.h"
+
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "chrome/browser/search/search_suggest/search_suggest_loader.h"
+#include "services/identity/public/cpp/identity_manager.h"
+
+class SearchSuggestService::SigninObserver
+    : public identity::IdentityManager::Observer {
+ public:
+  using SigninStatusChangedCallback = base::RepeatingClosure;
+
+  SigninObserver(identity::IdentityManager* identity_manager,
+                 const SigninStatusChangedCallback& callback)
+      : identity_manager_(identity_manager), callback_(callback) {
+    identity_manager_->AddObserver(this);
+  }
+
+  ~SigninObserver() override { identity_manager_->RemoveObserver(this); }
+
+  bool SignedIn() {
+    return !identity_manager_->GetAccountsInCookieJar()
+                .signed_in_accounts.empty();
+  }
+
+ private:
+  // IdentityManager::Observer implementation.
+  void OnAccountsInCookieUpdated(
+      const identity::AccountsInCookieJarInfo& accounts_in_cookie_jar_info,
+      const GoogleServiceAuthError& error) override {
+    callback_.Run();
+  }
+
+  identity::IdentityManager* const identity_manager_;
+  SigninStatusChangedCallback callback_;
+};
+
+SearchSuggestService::SearchSuggestService(
+    identity::IdentityManager* identity_manager,
+    std::unique_ptr<SearchSuggestLoader> loader)
+    : loader_(std::move(loader)),
+      signin_observer_(std::make_unique<SigninObserver>(
+          identity_manager,
+          base::BindRepeating(&SearchSuggestService::SigninStatusChanged,
+                              base::Unretained(this)))) {}
+
+SearchSuggestService::~SearchSuggestService() = default;
+
+void SearchSuggestService::Shutdown() {
+  for (auto& observer : observers_) {
+    observer.OnSearchSuggestServiceShuttingDown();
+  }
+
+  signin_observer_.reset();
+  DCHECK(!observers_.might_have_observers());
+}
+
+void SearchSuggestService::Refresh() {
+  if (signin_observer_->SignedIn()) {
+    loader_->Load(base::BindOnce(&SearchSuggestService::SearchSuggestDataLoaded,
+                                 base::Unretained(this)));
+  } else {
+    SearchSuggestDataLoaded(SearchSuggestLoader::Status::SIGNED_OUT,
+                            base::nullopt);
+  }
+}
+
+void SearchSuggestService::AddObserver(SearchSuggestServiceObserver* observer) {
+  observers_.AddObserver(observer);
+}
+
+void SearchSuggestService::RemoveObserver(
+    SearchSuggestServiceObserver* observer) {
+  observers_.RemoveObserver(observer);
+}
+
+void SearchSuggestService::SigninStatusChanged() {
+  // If we have cached data, clear it.
+  if (search_suggest_data_.has_value()) {
+    search_suggest_data_ = base::nullopt;
+  }
+}
+
+void SearchSuggestService::SearchSuggestDataLoaded(
+    SearchSuggestLoader::Status status,
+    const base::Optional<SearchSuggestData>& data) {
+  // In case of transient errors, keep our cached data (if any), but still
+  // notify observers of the finished load (attempt).
+  if (status != SearchSuggestLoader::Status::TRANSIENT_ERROR) {
+    // TODO(crbug/904565): Verify that cached data is also cleared when the
+    // impression cap is reached. Including the response from the request made
+    // on the same load that the cap was hit.
+    search_suggest_data_ = data;
+  }
+  NotifyObservers();
+}
+
+void SearchSuggestService::NotifyObservers() {
+  for (auto& observer : observers_) {
+    observer.OnSearchSuggestDataUpdated();
+  }
+}
diff --git a/chrome/browser/search/search_suggest/search_suggest_service.h b/chrome/browser/search/search_suggest/search_suggest_service.h
new file mode 100644
index 0000000..715b347
--- /dev/null
+++ b/chrome/browser/search/search_suggest/search_suggest_service.h
@@ -0,0 +1,71 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_SERVICE_H_
+#define CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_SERVICE_H_
+
+#include <memory>
+
+#include "base/observer_list.h"
+#include "base/optional.h"
+#include "chrome/browser/search/search_suggest/search_suggest_data.h"
+#include "chrome/browser/search/search_suggest/search_suggest_loader.h"
+#include "chrome/browser/search/search_suggest/search_suggest_service_observer.h"
+#include "components/keyed_service/core/keyed_service.h"
+
+namespace identity {
+class IdentityManager;
+}  // namespace identity
+
+// A service that downloads, caches, and hands out SearchSuggestData. It never
+// initiates a download automatically, only when Refresh is called. When the
+// user signs in or out, the cached value is cleared.
+class SearchSuggestService : public KeyedService {
+ public:
+  SearchSuggestService(identity::IdentityManager* identity_manager,
+                       std::unique_ptr<SearchSuggestLoader> loader);
+  ~SearchSuggestService() override;
+
+  // KeyedService implementation.
+  void Shutdown() override;
+
+  // Returns the currently cached SearchSuggestData, if any.
+  const base::Optional<SearchSuggestData>& search_suggest_data() const {
+    return search_suggest_data_;
+  }
+
+  // Clears any currently cached search suggest data.
+  void ClearSearchSuggestData() { search_suggest_data_ = base::nullopt; }
+
+  // Requests an asynchronous refresh from the network. After the update
+  // completes, OnSearchSuggestDataUpdated will be called on the observers.
+  void Refresh();
+
+  // Add/remove observers. All observers must unregister themselves before the
+  // SearchSuggestService is destroyed.
+  void AddObserver(SearchSuggestServiceObserver* observer);
+  void RemoveObserver(SearchSuggestServiceObserver* observer);
+
+  SearchSuggestLoader* loader_for_testing() { return loader_.get(); }
+
+ private:
+  class SigninObserver;
+
+  void SigninStatusChanged();
+
+  void SearchSuggestDataLoaded(SearchSuggestLoader::Status status,
+                               const base::Optional<SearchSuggestData>& data);
+
+  void NotifyObservers();
+
+  std::unique_ptr<SearchSuggestLoader> loader_;
+
+  std::unique_ptr<SigninObserver> signin_observer_;
+
+  base::ObserverList<SearchSuggestServiceObserver, true>::Unchecked observers_;
+
+  base::Optional<SearchSuggestData> search_suggest_data_;
+};
+
+#endif  // CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_SERVICE_H_
diff --git a/chrome/browser/search/search_suggest/search_suggest_service_factory.cc b/chrome/browser/search/search_suggest/search_suggest_service_factory.cc
new file mode 100644
index 0000000..31573d564
--- /dev/null
+++ b/chrome/browser/search/search_suggest/search_suggest_service_factory.cc
@@ -0,0 +1,68 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/search/search_suggest/search_suggest_service_factory.h"
+
+#include <string>
+
+#include "base/feature_list.h"
+#include "base/metrics/field_trial_params.h"
+#include "base/optional.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/content_settings/cookie_settings_factory.h"
+#include "chrome/browser/google/google_url_tracker_factory.h"
+#include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/search/ntp_features.h"
+#include "chrome/browser/search/search_suggest/search_suggest_loader_impl.h"
+#include "chrome/browser/search/search_suggest/search_suggest_service.h"
+#include "chrome/browser/signin/account_consistency_mode_manager.h"
+#include "chrome/browser/signin/identity_manager_factory.h"
+#include "chrome/common/chrome_features.h"
+#include "components/content_settings/core/browser/cookie_settings.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/signin/core/browser/cookie_settings_util.h"
+#include "content/public/browser/browser_context.h"
+#include "content/public/browser/storage_partition.h"
+
+// static
+SearchSuggestService* SearchSuggestServiceFactory::GetForProfile(
+    Profile* profile) {
+  return static_cast<SearchSuggestService*>(
+      GetInstance()->GetServiceForBrowserContext(profile, true));
+}
+
+// static
+SearchSuggestServiceFactory* SearchSuggestServiceFactory::GetInstance() {
+  return base::Singleton<SearchSuggestServiceFactory>::get();
+}
+
+SearchSuggestServiceFactory::SearchSuggestServiceFactory()
+    : BrowserContextKeyedServiceFactory(
+          "SearchSuggestService",
+          BrowserContextDependencyManager::GetInstance()) {
+  DependsOn(CookieSettingsFactory::GetInstance());
+  DependsOn(GoogleURLTrackerFactory::GetInstance());
+  DependsOn(IdentityManagerFactory::GetInstance());
+}
+
+SearchSuggestServiceFactory::~SearchSuggestServiceFactory() = default;
+
+KeyedService* SearchSuggestServiceFactory::BuildServiceInstanceFor(
+    content::BrowserContext* context) const {
+  if (!base::FeatureList::IsEnabled(features::kSearchSuggestionsOnLocalNtp)) {
+    return nullptr;
+  }
+  Profile* profile = Profile::FromBrowserContext(context);
+  identity::IdentityManager* identity_manager =
+      IdentityManagerFactory::GetForProfile(profile);
+  GoogleURLTracker* google_url_tracker =
+      GoogleURLTrackerFactory::GetForProfile(profile);
+  auto url_loader_factory =
+      content::BrowserContext::GetDefaultStoragePartition(context)
+          ->GetURLLoaderFactoryForBrowserProcess();
+  return new SearchSuggestService(
+      identity_manager, std::make_unique<SearchSuggestLoaderImpl>(
+                            url_loader_factory, google_url_tracker,
+                            g_browser_process->GetApplicationLocale()));
+}
diff --git a/chrome/browser/search/search_suggest/search_suggest_service_factory.h b/chrome/browser/search/search_suggest/search_suggest_service_factory.h
new file mode 100644
index 0000000..2114eab7
--- /dev/null
+++ b/chrome/browser/search/search_suggest/search_suggest_service_factory.h
@@ -0,0 +1,35 @@
+// Copyright 2019 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_SERVICE_FACTORY_H_
+#define CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_SERVICE_FACTORY_H_
+
+#include "base/macros.h"
+#include "base/memory/singleton.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+class SearchSuggestService;
+class Profile;
+
+class SearchSuggestServiceFactory : public BrowserContextKeyedServiceFactory {
+ public:
+  // Returns the SearchSuggestService for |profile|.
+  static SearchSuggestService* GetForProfile(Profile* profile);
+
+  static SearchSuggestServiceFactory* GetInstance();
+
+ private:
+  friend struct base::DefaultSingletonTraits<SearchSuggestServiceFactory>;
+
+  SearchSuggestServiceFactory();
+  ~SearchSuggestServiceFactory() override;
+
+  // Overridden from BrowserContextKeyedServiceFactory:
+  KeyedService* BuildServiceInstanceFor(
+      content::BrowserContext* profile) const override;
+
+  DISALLOW_COPY_AND_ASSIGN(SearchSuggestServiceFactory);
+};
+
+#endif  // CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_SERVICE_FACTORY_H_
diff --git a/chrome/browser/search/search_suggest/search_suggest_service_observer.h b/chrome/browser/search/search_suggest/search_suggest_service_observer.h
new file mode 100644
index 0000000..7e68596
--- /dev/null
+++ b/chrome/browser/search/search_suggest/search_suggest_service_observer.h
@@ -0,0 +1,24 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_SERVICE_OBSERVER_H_
+#define CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_SERVICE_OBSERVER_H_
+
+// Observer for SearchSuggestService.
+class SearchSuggestServiceObserver {
+ public:
+  // Called when the SearchSuggestData is updated, usually as the result of a
+  // Refresh() call on the service. Note that this is called after each
+  // Refresh(), even if the network request failed, or if it didn't result in an
+  // actual change to the cached data. You can get the new data via
+  // SearchSuggestService::search_suggest_service_data().
+  virtual void OnSearchSuggestDataUpdated() = 0;
+
+  // Called when the SearchSuggestService is shutting down. Observers that might
+  // outlive the service should use this to unregister themselves, and clear out
+  // any pointers to the service they might hold.
+  virtual void OnSearchSuggestServiceShuttingDown() {}
+};
+
+#endif  // CHROME_BROWSER_SEARCH_SEARCH_SUGGEST_SEARCH_SUGGEST_SERVICE_OBSERVER_H_
diff --git a/chrome/browser/search/search_suggest/search_suggest_service_unittest.cc b/chrome/browser/search/search_suggest/search_suggest_service_unittest.cc
new file mode 100644
index 0000000..2599500
--- /dev/null
+++ b/chrome/browser/search/search_suggest/search_suggest_service_unittest.cc
@@ -0,0 +1,189 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/search/search_suggest/search_suggest_service.h"
+
+#include <memory>
+#include <utility>
+#include <vector>
+
+#include "base/macros.h"
+#include "base/optional.h"
+#include "base/test/scoped_task_environment.h"
+#include "chrome/browser/search/search_suggest/search_suggest_data.h"
+#include "chrome/browser/search/search_suggest/search_suggest_loader.h"
+#include "components/signin/core/browser/account_tracker_service.h"
+#include "components/signin/core/browser/test_signin_client.h"
+#include "components/sync_preferences/testing_pref_service_syncable.h"
+#include "google_apis/gaia/fake_oauth2_token_service.h"
+#include "google_apis/gaia/gaia_constants.h"
+#include "services/identity/public/cpp/identity_test_environment.h"
+#include "services/identity/public/cpp/identity_test_utils.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+using testing::Eq;
+using testing::InSequence;
+using testing::StrictMock;
+
+class FakeSearchSuggestLoader : public SearchSuggestLoader {
+ public:
+  void Load(SearchSuggestionsCallback callback) override {
+    callbacks_.push_back(std::move(callback));
+  }
+
+  GURL GetLoadURLForTesting() const override { return GURL(); }
+
+  size_t GetCallbackCount() const { return callbacks_.size(); }
+
+  void RespondToAllCallbacks(Status status,
+                             const base::Optional<SearchSuggestData>& data) {
+    for (SearchSuggestionsCallback& callback : callbacks_) {
+      std::move(callback).Run(status, data);
+    }
+    callbacks_.clear();
+  }
+
+ private:
+  std::vector<SearchSuggestionsCallback> callbacks_;
+};
+
+class SearchSuggestServiceTest : public testing::Test {
+ public:
+  SearchSuggestServiceTest()
+      : signin_client_(&pref_service_),
+        identity_env_(&test_url_loader_factory_) {
+    // GaiaCookieManagerService calls static methods of AccountTrackerService
+    // which access prefs.
+    AccountTrackerService::RegisterPrefs(pref_service_.registry());
+
+    auto loader = std::make_unique<FakeSearchSuggestLoader>();
+    loader_ = loader.get();
+    service_ = std::make_unique<SearchSuggestService>(
+        identity_env_.identity_manager(), std::move(loader));
+
+    identity_env_.MakePrimaryAccountAvailable("[email protected]");
+    identity_env_.SetAutomaticIssueOfAccessTokens(true);
+  }
+
+  FakeSearchSuggestLoader* loader() { return loader_; }
+  SearchSuggestService* service() { return service_.get(); }
+
+  void SignIn() {
+    AccountInfo account_info =
+        identity_env_.MakeAccountAvailable("[email protected]");
+    identity_env_.SetCookieAccounts({{account_info.email, account_info.gaia}});
+    task_environment_.RunUntilIdle();
+  }
+
+  void SignOut() {
+    identity_env_.SetCookieAccounts({});
+    task_environment_.RunUntilIdle();
+  }
+
+ private:
+  base::test::ScopedTaskEnvironment task_environment_;
+
+  sync_preferences::TestingPrefServiceSyncable pref_service_;
+  network::TestURLLoaderFactory test_url_loader_factory_;
+  TestSigninClient signin_client_;
+  FakeOAuth2TokenService token_service_;
+  identity::IdentityTestEnvironment identity_env_;
+
+  // Owned by the service.
+  FakeSearchSuggestLoader* loader_;
+
+  std::unique_ptr<SearchSuggestService> service_;
+};
+
+TEST_F(SearchSuggestServiceTest, NoRefreshOnSignedOutRequest) {
+  ASSERT_THAT(service()->search_suggest_data(), Eq(base::nullopt));
+
+  // Request a refresh. That should do nothing as no user is signed-in.
+  service()->Refresh();
+  EXPECT_THAT(loader()->GetCallbackCount(), Eq(0u));
+  EXPECT_THAT(service()->search_suggest_data(), Eq(base::nullopt));
+}
+
+TEST_F(SearchSuggestServiceTest, RefreshesOnSignedInRequest) {
+  ASSERT_THAT(service()->search_suggest_data(), Eq(base::nullopt));
+  SignIn();
+
+  // Request a refresh. That should arrive at the loader.
+  service()->Refresh();
+  EXPECT_THAT(loader()->GetCallbackCount(), Eq(1u));
+
+  // Fulfill it.
+  SearchSuggestData data;
+  data.suggestions_html = "<div></div>";
+  loader()->RespondToAllCallbacks(SearchSuggestLoader::Status::OK, data);
+  EXPECT_THAT(service()->search_suggest_data(), Eq(data));
+
+  // Request another refresh.
+  service()->Refresh();
+  EXPECT_THAT(loader()->GetCallbackCount(), Eq(1u));
+
+  // For now, the old data should still be there.
+  EXPECT_THAT(service()->search_suggest_data(), Eq(data));
+
+  // Fulfill the second request.
+  SearchSuggestData other_data;
+  other_data.suggestions_html = "<div>Different!</div>";
+  loader()->RespondToAllCallbacks(SearchSuggestLoader::Status::OK, other_data);
+  EXPECT_THAT(service()->search_suggest_data(), Eq(other_data));
+}
+
+TEST_F(SearchSuggestServiceTest, KeepsCacheOnTransientError) {
+  ASSERT_THAT(service()->search_suggest_data(), Eq(base::nullopt));
+  SignIn();
+
+  // Load some data.
+  service()->Refresh();
+  SearchSuggestData data;
+  data.suggestions_html = "<div></div>";
+  loader()->RespondToAllCallbacks(SearchSuggestLoader::Status::OK, data);
+  ASSERT_THAT(service()->search_suggest_data(), Eq(data));
+
+  // Request a refresh and respond with a transient error.
+  service()->Refresh();
+  loader()->RespondToAllCallbacks(SearchSuggestLoader::Status::TRANSIENT_ERROR,
+                                  base::nullopt);
+  // Cached data should still be there.
+  EXPECT_THAT(service()->search_suggest_data(), Eq(data));
+}
+
+TEST_F(SearchSuggestServiceTest, ClearsCacheOnFatalError) {
+  ASSERT_THAT(service()->search_suggest_data(), Eq(base::nullopt));
+  SignIn();
+
+  // Load some data.
+  service()->Refresh();
+  SearchSuggestData data;
+  data.suggestions_html = "<div></div>";
+  loader()->RespondToAllCallbacks(SearchSuggestLoader::Status::OK, data);
+  ASSERT_THAT(service()->search_suggest_data(), Eq(data));
+
+  // Request a refresh and respond with a fatal error.
+  service()->Refresh();
+  loader()->RespondToAllCallbacks(SearchSuggestLoader::Status::FATAL_ERROR,
+                                  base::nullopt);
+  // Cached data should be gone now.
+  EXPECT_THAT(service()->search_suggest_data(), Eq(base::nullopt));
+}
+
+TEST_F(SearchSuggestServiceTest, ResetsOnSignOut) {
+  ASSERT_THAT(service()->search_suggest_data(), Eq(base::nullopt));
+  SignIn();
+
+  // Load some data.
+  service()->Refresh();
+  SearchSuggestData data;
+  data.suggestions_html = "<div></div>";
+  loader()->RespondToAllCallbacks(SearchSuggestLoader::Status::OK, data);
+  ASSERT_THAT(service()->search_suggest_data(), Eq(data));
+
+  // Sign out. This should clear the cached data and notify the observer.
+  SignOut();
+  EXPECT_THAT(service()->search_suggest_data(), Eq(base::nullopt));
+}