[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));
+}