blob: 71b3f5620f84aea37a3dec85e95aafa5d436412c [file] [log] [blame]
pke6dbb90af2016-07-08 14:00:461// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "components/ntp_snippets/content_suggestions_service.h"
6
7#include <algorithm>
8#include <iterator>
vitaliii45941152016-09-05 08:58:139#include <set>
jkrcale13510e2016-09-08 17:56:2010#include <utility>
pke6dbb90af2016-07-08 14:00:4611
12#include "base/bind.h"
pke1da90602016-08-05 14:20:2713#include "base/location.h"
jkrcal08d79b52017-04-13 14:02:1114#include "base/metrics/histogram_macros.h"
vitaliii69689f312017-10-25 17:36:0615#include "base/stl_util.h"
pke6dbb90af2016-07-08 14:00:4616#include "base/strings/string_number_conversions.h"
vitaliii69689f312017-10-25 17:36:0617#include "base/strings/stringprintf.h"
pke1da90602016-08-05 14:20:2718#include "base/threading/thread_task_runner_handle.h"
jkrcal27b02c12017-03-21 11:18:2619#include "base/time/default_clock.h"
dgn52914722016-10-18 10:28:4220#include "base/values.h"
Chris Luf0b870f2018-11-14 00:31:3721#include "components/favicon/core/large_icon_service.h"
jkrcal7b7e71f12017-04-06 13:12:4022#include "components/favicon_base/fallback_icon_style.h"
23#include "components/favicon_base/favicon_types.h"
dgnf6500c12017-05-09 17:05:3124#include "components/ntp_snippets/content_suggestions_metrics.h"
dgn52914722016-10-18 10:28:4225#include "components/ntp_snippets/pref_names.h"
jkrcal8083bc52017-04-24 16:34:0226#include "components/ntp_snippets/remote/remote_suggestions_provider.h"
dgn52914722016-10-18 10:28:4227#include "components/prefs/pref_registry_simple.h"
28#include "components/prefs/pref_service.h"
Ramin Halavati4bf78aa2017-06-01 04:53:4529#include "net/traffic_annotation/network_traffic_annotation.h"
pke6dbb90af2016-07-08 14:00:4630#include "ui/gfx/image/image.h"
31
32namespace ntp_snippets {
33
jkrcal08d79b52017-04-13 14:02:1134namespace {
35
36// Enumeration listing all possible outcomes for fetch attempts of favicons for
37// content suggestions. Used for UMA histograms, so do not change existing
38// values. Insert new values at the end, and update the histogram definition.
39// GENERATED_JAVA_ENUM_PACKAGE: org.chromium.chrome.browser.ntp.snippets
40enum class FaviconFetchResult {
41 SUCCESS_CACHED = 0,
42 SUCCESS_FETCHED = 1,
43 FAILURE = 2,
44 COUNT = 3
45};
46
jkrcal08d79b52017-04-13 14:02:1147} // namespace
48
vitaliii45941152016-09-05 08:58:1349ContentSuggestionsService::ContentSuggestionsService(
50 State state,
Miyoung Shin23737f62019-07-23 15:43:3151 signin::IdentityManager* identity_manager,
jkrcale13510e2016-09-08 17:56:2052 history::HistoryService* history_service,
jkrcal7b7e71f12017-04-06 13:12:4053 favicon::LargeIconService* large_icon_service,
vitaliii7456f5a2016-12-19 11:13:2554 PrefService* pref_service,
jkrcalf9966462017-03-29 16:25:2155 std::unique_ptr<CategoryRanker> category_ranker,
56 std::unique_ptr<UserClassifier> user_classifier,
Natalie Chouinard50024212019-05-06 17:38:5957 std::unique_ptr<RemoteSuggestionsScheduler> remote_suggestions_scheduler)
jkrcale13510e2016-09-08 17:56:2058 : state_(state),
Colin Blundell076714ff2018-02-12 17:26:1459 identity_manager_observer_(this),
jkrcale13510e2016-09-08 17:56:2060 history_service_observer_(this),
jkrcal093410c2016-12-21 16:13:5561 remote_suggestions_provider_(nullptr),
jkrcal7b7e71f12017-04-06 13:12:4062 large_icon_service_(large_icon_service),
dgn52914722016-10-18 10:28:4263 pref_service_(pref_service),
jkrcalf9966462017-03-29 16:25:2164 remote_suggestions_scheduler_(std::move(remote_suggestions_scheduler)),
65 user_classifier_(std::move(user_classifier)),
Natalie Chouinard50024212019-05-06 17:38:5966 category_ranker_(std::move(category_ranker)) {
vitaliii45941152016-09-05 08:58:1367 // Can be null in tests.
Colin Blundell076714ff2018-02-12 17:26:1468 if (identity_manager) {
69 identity_manager_observer_.Add(identity_manager);
dgnf5708892016-11-22 10:36:2270 }
71
vitaliii4408d6c2016-11-21 14:16:2472 if (history_service) {
vitaliii45941152016-09-05 08:58:1373 history_service_observer_.Add(history_service);
vitaliii4408d6c2016-11-21 14:16:2474 }
dgn52914722016-10-18 10:28:4275
dgn52914722016-10-18 10:28:4276 RestoreDismissedCategoriesFromPrefs();
vitaliii45941152016-09-05 08:58:1377}
pke6dbb90af2016-07-08 14:00:4678
treib62e819e2016-09-27 11:47:3479ContentSuggestionsService::~ContentSuggestionsService() = default;
pke6dbb90af2016-07-08 14:00:4680
81void ContentSuggestionsService::Shutdown() {
jkrcal093410c2016-12-21 16:13:5582 remote_suggestions_provider_ = nullptr;
83 remote_suggestions_scheduler_ = nullptr;
pke5728f082016-08-03 17:27:3584 suggestions_by_category_.clear();
85 providers_by_category_.clear();
86 categories_.clear();
87 providers_.clear();
pke6dbb90af2016-07-08 14:00:4688 state_ = State::DISABLED;
vitaliii4408d6c2016-11-21 14:16:2489 for (Observer& observer : observers_) {
ericwilligers42b92c12016-10-24 20:21:1390 observer.ContentSuggestionsServiceShutdown();
vitaliii4408d6c2016-11-21 14:16:2491 }
pke6dbb90af2016-07-08 14:00:4692}
93
dgn52914722016-10-18 10:28:4294// static
95void ContentSuggestionsService::RegisterProfilePrefs(
96 PrefRegistrySimple* registry) {
97 registry->RegisterListPref(prefs::kDismissedCategories);
98}
99
vitaliii8b5ab282016-12-20 11:06:22100std::vector<Category> ContentSuggestionsService::GetCategories() const {
101 std::vector<Category> sorted_categories = categories_;
102 std::sort(sorted_categories.begin(), sorted_categories.end(),
103 [this](const Category& left, const Category& right) {
104 return category_ranker_->Compare(left, right);
105 });
106 return sorted_categories;
107}
108
pke9c5095ac2016-08-01 13:53:12109CategoryStatus ContentSuggestionsService::GetCategoryStatus(
110 Category category) const {
pke6dbb90af2016-07-08 14:00:46111 if (state_ == State::DISABLED) {
pke9c5095ac2016-08-01 13:53:12112 return CategoryStatus::ALL_SUGGESTIONS_EXPLICITLY_DISABLED;
pke6dbb90af2016-07-08 14:00:46113 }
114
pke4d3a4d62016-08-02 09:06:21115 auto iterator = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24116 if (iterator == providers_by_category_.end()) {
pke9c5095ac2016-08-01 13:53:12117 return CategoryStatus::NOT_PROVIDED;
vitaliii4408d6c2016-11-21 14:16:24118 }
pke6dbb90af2016-07-08 14:00:46119
120 return iterator->second->GetCategoryStatus(category);
121}
122
pkebd2f650a2016-08-09 14:53:45123base::Optional<CategoryInfo> ContentSuggestionsService::GetCategoryInfo(
124 Category category) const {
125 auto iterator = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24126 if (iterator == providers_by_category_.end()) {
pkebd2f650a2016-08-09 14:53:45127 return base::Optional<CategoryInfo>();
vitaliii4408d6c2016-11-21 14:16:24128 }
pkebd2f650a2016-08-09 14:53:45129 return iterator->second->GetCategoryInfo(category);
130}
131
pke6dbb90af2016-07-08 14:00:46132const std::vector<ContentSuggestion>&
pke9c5095ac2016-08-01 13:53:12133ContentSuggestionsService::GetSuggestionsForCategory(Category category) const {
pke6dbb90af2016-07-08 14:00:46134 auto iterator = suggestions_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24135 if (iterator == suggestions_by_category_.end()) {
pke6dbb90af2016-07-08 14:00:46136 return no_suggestions_;
vitaliii4408d6c2016-11-21 14:16:24137 }
pke6dbb90af2016-07-08 14:00:46138 return iterator->second;
139}
140
141void ContentSuggestionsService::FetchSuggestionImage(
treib4bbc54922016-09-28 17:26:44142 const ContentSuggestion::ID& suggestion_id,
gaschler42544c12017-08-03 15:43:26143 ImageFetchedCallback callback) {
treib4bbc54922016-09-28 17:26:44144 if (!providers_by_category_.count(suggestion_id.category())) {
pke6dbb90af2016-07-08 14:00:46145 LOG(WARNING) << "Requested image for suggestion " << suggestion_id
treib4bbc54922016-09-28 17:26:44146 << " for unavailable category " << suggestion_id.category();
pke1da90602016-08-05 14:20:27147 base::ThreadTaskRunnerHandle::Get()->PostTask(
gaschler42544c12017-08-03 15:43:26148 FROM_HERE, base::BindOnce(std::move(callback), gfx::Image()));
pke6dbb90af2016-07-08 14:00:46149 return;
150 }
treib4bbc54922016-09-28 17:26:44151 providers_by_category_[suggestion_id.category()]->FetchSuggestionImage(
gaschler42544c12017-08-03 15:43:26152 suggestion_id, std::move(callback));
pke6dbb90af2016-07-08 14:00:46153}
154
Dan Harrington6eb74dc82018-03-20 17:39:18155void ContentSuggestionsService::FetchSuggestionImageData(
156 const ContentSuggestion::ID& suggestion_id,
157 ImageDataFetchedCallback callback) {
158 if (!providers_by_category_.count(suggestion_id.category())) {
159 LOG(WARNING) << "Requested image for suggestion " << suggestion_id
160 << " for unavailable category " << suggestion_id.category();
161 base::ThreadTaskRunnerHandle::Get()->PostTask(
162 FROM_HERE, base::BindOnce(std::move(callback), std::string()));
163 return;
164 }
165 providers_by_category_[suggestion_id.category()]->FetchSuggestionImageData(
166 suggestion_id, std::move(callback));
167}
168
jkrcalcd011682017-04-13 05:41:16169// TODO(jkrcal): Split the favicon fetching into a separate class.
jkrcal10004602017-03-29 07:44:28170void ContentSuggestionsService::FetchSuggestionFavicon(
171 const ContentSuggestion::ID& suggestion_id,
172 int minimum_size_in_pixel,
173 int desired_size_in_pixel,
gaschler42544c12017-08-03 15:43:26174 ImageFetchedCallback callback) {
jkrcal8083bc52017-04-24 16:34:02175 const GURL& domain_with_favicon = GetFaviconDomain(suggestion_id);
176 if (!domain_with_favicon.is_valid() || !large_icon_service_) {
jkrcal7b7e71f12017-04-06 13:12:40177 base::ThreadTaskRunnerHandle::Get()->PostTask(
gaschler42544c12017-08-03 15:43:26178 FROM_HERE, base::BindOnce(std::move(callback), gfx::Image()));
jkrcal7b7e71f12017-04-06 13:12:40179 return;
180 }
181
jkrcal95acdc5a2017-05-19 15:34:37182 GetFaviconFromCache(domain_with_favicon, minimum_size_in_pixel,
gaschler42544c12017-08-03 15:43:26183 desired_size_in_pixel, std::move(callback),
jkrcal95acdc5a2017-05-19 15:34:37184 /*continue_to_google_server=*/true);
jkrcal7b7e71f12017-04-06 13:12:40185}
186
jkrcal8083bc52017-04-24 16:34:02187GURL ContentSuggestionsService::GetFaviconDomain(
188 const ContentSuggestion::ID& suggestion_id) {
189 const std::vector<ContentSuggestion>& suggestions =
190 suggestions_by_category_[suggestion_id.category()];
191 auto position =
192 std::find_if(suggestions.begin(), suggestions.end(),
193 [&suggestion_id](const ContentSuggestion& suggestion) {
194 return suggestion_id == suggestion.id();
195 });
196 if (position != suggestions.end()) {
197 return position->url_with_favicon();
198 }
199
200 // Look up the URL in the archive of |remote_suggestions_provider_|.
201 // TODO(jkrcal): Fix how Fetch more works or find other ways to remove this
202 // hack. crbug.com/714031
203 if (providers_by_category_[suggestion_id.category()] ==
204 remote_suggestions_provider_) {
205 return remote_suggestions_provider_->GetUrlWithFavicon(suggestion_id);
206 }
207 return GURL();
208}
209
jkrcal95acdc5a2017-05-19 15:34:37210void ContentSuggestionsService::GetFaviconFromCache(
211 const GURL& publisher_url,
212 int minimum_size_in_pixel,
213 int desired_size_in_pixel,
gaschler42544c12017-08-03 15:43:26214 ImageFetchedCallback callback,
jkrcal95acdc5a2017-05-19 15:34:37215 bool continue_to_google_server) {
216 // TODO(jkrcal): Create a general wrapper function in LargeIconService that
217 // does handle the get-from-cache-and-fallback-to-google-server functionality
218 // in one shot (for all clients that do not need to react in between).
Jan Krcalb0cbad92017-06-01 09:03:47219
220 // Use desired_size = 0 for getting the icon from the cache (so that the icon
221 // is not poorly rescaled by LargeIconService).
Yi Su4903ce52019-01-24 19:27:02222 large_icon_service_->GetLargeIconImageOrFallbackStyleForPageUrl(
Jan Krcalb0cbad92017-06-01 09:03:47223 publisher_url, minimum_size_in_pixel, /*desired_size_in_pixel=*/0,
Ken Rockotebba67882019-12-20 18:22:24224 base::BindOnce(&ContentSuggestionsService::OnGetFaviconFromCacheFinished,
225 base::Unretained(this), publisher_url,
226 minimum_size_in_pixel, desired_size_in_pixel,
227 std::move(callback), continue_to_google_server),
jkrcal95acdc5a2017-05-19 15:34:37228 &favicons_task_tracker_);
229}
230
jkrcal7b7e71f12017-04-06 13:12:40231void ContentSuggestionsService::OnGetFaviconFromCacheFinished(
232 const GURL& publisher_url,
233 int minimum_size_in_pixel,
234 int desired_size_in_pixel,
gaschler42544c12017-08-03 15:43:26235 ImageFetchedCallback callback,
jkrcal7b7e71f12017-04-06 13:12:40236 bool continue_to_google_server,
237 const favicon_base::LargeIconImageResult& result) {
238 if (!result.image.IsEmpty()) {
gaschler42544c12017-08-03 15:43:26239 std::move(callback).Run(result.image);
Jan Krcal5ade20c2017-07-11 13:03:00240 // Update the time when the icon was last requested - postpone thus the
241 // automatic eviction of the favicon from the favicon database.
242 large_icon_service_->TouchIconFromGoogleServer(result.icon_url);
jkrcal7b7e71f12017-04-06 13:12:40243 return;
244 }
245
246 if (!continue_to_google_server ||
247 (result.fallback_icon_style &&
248 !result.fallback_icon_style->is_default_background_color)) {
249 // We cannot download from the server if there is some small icon in the
jkrcal08d79b52017-04-13 14:02:11250 // cache (resulting in non-default background color) or if we already did
251 // so.
gaschler42544c12017-08-03 15:43:26252 std::move(callback).Run(gfx::Image());
jkrcal7b7e71f12017-04-06 13:12:40253 return;
254 }
255
256 // Try to fetch the favicon from a Google favicon server.
jkrcal5de6dff2017-05-24 09:59:47257 // TODO(jkrcal): Currently used only for Articles for you which have public
258 // URLs. Let the provider decide whether |publisher_url| may be private or
259 // not.
Ramin Halavati4bf78aa2017-06-01 04:53:45260 net::NetworkTrafficAnnotationTag traffic_annotation =
261 net::DefineNetworkTrafficAnnotation("content_suggestion_get_favicon", R"(
262 semantics {
263 sender: "Content Suggestion"
264 description:
265 "Sends a request to a Google server to retrieve the favicon bitmap "
266 "for an article suggestion on the new tab page (URLs are public "
267 "and provided by Google)."
268 trigger:
269 "A request can be sent if Chrome does not have a favicon for a "
270 "particular page."
271 data: "Page URL and desired icon size."
272 destination: GOOGLE_OWNED_SERVICE
273 }
274 policy {
Ramin Halavati3b979782017-07-21 11:40:26275 cookies_allowed: NO
Ramin Halavati4bf78aa2017-06-01 04:53:45276 setting: "This feature cannot be disabled by settings."
277 policy_exception_justification: "Not implemented."
278 })");
jkrcal7b7e71f12017-04-06 13:12:40279 large_icon_service_
280 ->GetLargeIconOrFallbackStyleFromGoogleServerSkippingLocalCache(
Victor Hugo Vianna Silva6438571b2019-07-31 18:05:42281 publisher_url,
Victor Hugo Vianna Silvaf969d2a42019-05-27 13:18:32282 /*may_page_url_be_private=*/false,
283 /*should_trim_page_url_path=*/false, traffic_annotation,
Ken Rockotebba67882019-12-20 18:22:24284 base::BindOnce(
jkrcal7b7e71f12017-04-06 13:12:40285 &ContentSuggestionsService::OnGetFaviconFromGoogleServerFinished,
286 base::Unretained(this), publisher_url, minimum_size_in_pixel,
Ken Rockotebba67882019-12-20 18:22:24287 desired_size_in_pixel, std::move(callback)));
jkrcal7b7e71f12017-04-06 13:12:40288}
289
290void ContentSuggestionsService::OnGetFaviconFromGoogleServerFinished(
291 const GURL& publisher_url,
292 int minimum_size_in_pixel,
293 int desired_size_in_pixel,
gaschler42544c12017-08-03 15:43:26294 ImageFetchedCallback callback,
Jan Krcalf3e5733e2017-07-10 09:06:11295 favicon_base::GoogleFaviconServerRequestStatus status) {
296 if (status != favicon_base::GoogleFaviconServerRequestStatus::SUCCESS) {
gaschler42544c12017-08-03 15:43:26297 std::move(callback).Run(gfx::Image());
jkrcal7b7e71f12017-04-06 13:12:40298 return;
299 }
300
jkrcal95acdc5a2017-05-19 15:34:37301 GetFaviconFromCache(publisher_url, minimum_size_in_pixel,
gaschler42544c12017-08-03 15:43:26302 desired_size_in_pixel, std::move(callback),
jkrcal95acdc5a2017-05-19 15:34:37303 /*continue_to_google_server=*/false);
jkrcal10004602017-03-29 07:44:28304}
305
vitaliii685fdfaa2016-08-31 11:25:46306void ContentSuggestionsService::ClearHistory(
307 base::Time begin,
308 base::Time end,
Ken Rockotebba67882019-12-20 18:22:24309 const base::RepeatingCallback<bool(const GURL& url)>& filter) {
vitaliii685fdfaa2016-08-31 11:25:46310 for (const auto& provider : providers_) {
311 provider->ClearHistory(begin, end, filter);
312 }
vitaliii6343b2c2017-01-04 07:57:11313 category_ranker_->ClearHistory(begin, end);
tschumann5829c3412017-01-09 21:45:43314 // This potentially removed personalized data which we shouldn't display
315 // anymore.
316 for (Observer& observer : observers_) {
317 observer.OnFullRefreshRequired();
318 }
vitaliii685fdfaa2016-08-31 11:25:46319}
320
treib7d1d7a52016-08-24 14:04:55321void ContentSuggestionsService::ClearAllCachedSuggestions() {
pke6dbb90af2016-07-08 14:00:46322 suggestions_by_category_.clear();
Jan Krcal33ed7512017-11-29 12:45:09323 for (const auto& provider : providers_) {
324 provider->ClearCachedSuggestions();
pke6dbb90af2016-07-08 14:00:46325 }
Jan Krcal33ed7512017-11-29 12:45:09326 for (Observer& observer : observers_) {
327 observer.OnFullRefreshRequired();
vitaliii4408d6c2016-11-21 14:16:24328 }
pke151b5502016-08-09 12:15:13329}
330
pkede0dd9f2016-08-23 09:18:11331void ContentSuggestionsService::GetDismissedSuggestionsForDebugging(
332 Category category,
gaschlerdf40dcc2017-08-04 14:06:37333 DismissedSuggestionsCallback callback) {
pke151b5502016-08-09 12:15:13334 auto iterator = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24335 if (iterator != providers_by_category_.end()) {
gaschlerdf40dcc2017-08-04 14:06:37336 iterator->second->GetDismissedSuggestionsForDebugging(category,
337 std::move(callback));
vitaliii4408d6c2016-11-21 14:16:24338 } else {
gaschlerdf40dcc2017-08-04 14:06:37339 std::move(callback).Run(std::vector<ContentSuggestion>());
vitaliii4408d6c2016-11-21 14:16:24340 }
pke151b5502016-08-09 12:15:13341}
342
343void ContentSuggestionsService::ClearDismissedSuggestionsForDebugging(
344 Category category) {
345 auto iterator = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24346 if (iterator != providers_by_category_.end()) {
pke151b5502016-08-09 12:15:13347 iterator->second->ClearDismissedSuggestionsForDebugging(category);
vitaliii4408d6c2016-11-21 14:16:24348 }
pke6dbb90af2016-07-08 14:00:46349}
350
pke2646c95b2016-07-25 12:18:44351void ContentSuggestionsService::DismissSuggestion(
treib4bbc54922016-09-28 17:26:44352 const ContentSuggestion::ID& suggestion_id) {
353 if (!providers_by_category_.count(suggestion_id.category())) {
pke2646c95b2016-07-25 12:18:44354 LOG(WARNING) << "Dismissed suggestion " << suggestion_id
treib4bbc54922016-09-28 17:26:44355 << " for unavailable category " << suggestion_id.category();
pke6dbb90af2016-07-08 14:00:46356 return;
357 }
dgnf6500c12017-05-09 17:05:31358
359 metrics::RecordContentSuggestionDismissed();
360
treib4bbc54922016-09-28 17:26:44361 providers_by_category_[suggestion_id.category()]->DismissSuggestion(
362 suggestion_id);
pke6dbb90af2016-07-08 14:00:46363
vitaliii2600e8e02016-12-09 17:23:36364 // Remove the suggestion locally if it is present. A suggestion may be missing
365 // localy e.g. if it was sent to UI through |Fetch| or it has been dismissed
366 // from a different NTP.
367 RemoveSuggestionByID(suggestion_id);
pke6dbb90af2016-07-08 14:00:46368}
369
dgn212feea3b2016-09-16 15:08:20370void ContentSuggestionsService::DismissCategory(Category category) {
371 auto providers_it = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24372 if (providers_it == providers_by_category_.end()) {
dgn212feea3b2016-09-16 15:08:20373 return;
vitaliii4408d6c2016-11-21 14:16:24374 }
dgn212feea3b2016-09-16 15:08:20375
dgnf6500c12017-05-09 17:05:31376 metrics::RecordCategoryDismissed();
377
dgn52914722016-10-18 10:28:42378 ContentSuggestionsProvider* provider = providers_it->second;
379 UnregisterCategory(category, provider);
380
381 dismissed_providers_by_category_[category] = provider;
382 StoreDismissedCategoriesToPrefs();
vitaliii3d9423e2017-01-03 17:08:13383
384 category_ranker_->OnCategoryDismissed(category);
dgn212feea3b2016-09-16 15:08:20385}
386
mvanouwerkerk52783d92016-10-12 11:03:40387void ContentSuggestionsService::RestoreDismissedCategories() {
388 // Make a copy as the original will be modified during iteration.
389 auto dismissed_providers_by_category_copy = dismissed_providers_by_category_;
390 for (const auto& category_provider_pair :
391 dismissed_providers_by_category_copy) {
dgn52914722016-10-18 10:28:42392 RestoreDismissedCategory(category_provider_pair.first);
mvanouwerkerk52783d92016-10-12 11:03:40393 }
dgn52914722016-10-18 10:28:42394 StoreDismissedCategoriesToPrefs();
mvanouwerkerk52783d92016-10-12 11:03:40395 DCHECK(dismissed_providers_by_category_.empty());
396}
397
pke6dbb90af2016-07-08 14:00:46398void ContentSuggestionsService::AddObserver(Observer* observer) {
399 observers_.AddObserver(observer);
400}
401
402void ContentSuggestionsService::RemoveObserver(Observer* observer) {
403 observers_.RemoveObserver(observer);
404}
405
406void ContentSuggestionsService::RegisterProvider(
pke5728f082016-08-03 17:27:35407 std::unique_ptr<ContentSuggestionsProvider> provider) {
408 DCHECK(state_ == State::ENABLED);
pke5728f082016-08-03 17:27:35409 providers_.push_back(std::move(provider));
pke6dbb90af2016-07-08 14:00:46410}
411
tschumann83578aa2016-11-03 13:18:32412void ContentSuggestionsService::Fetch(
413 const Category& category,
414 const std::set<std::string>& known_suggestion_ids,
gaschlerdf40dcc2017-08-04 14:06:37415 FetchDoneCallback callback) {
tschumann83578aa2016-11-03 13:18:32416 auto providers_it = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24417 if (providers_it == providers_by_category_.end()) {
tschumann83578aa2016-11-03 13:18:32418 return;
vitaliii4408d6c2016-11-21 14:16:24419 }
tschumann83578aa2016-11-03 13:18:32420
dgnf6500c12017-05-09 17:05:31421 metrics::RecordFetchAction();
422
gaschlerdf40dcc2017-08-04 14:06:37423 providers_it->second->Fetch(category, known_suggestion_ids,
424 std::move(callback));
tschumann83578aa2016-11-03 13:18:32425}
426
jkrcal093410c2016-12-21 16:13:55427void ContentSuggestionsService::ReloadSuggestions() {
428 for (const auto& provider : providers_) {
429 provider->ReloadSuggestions();
430 }
431}
432
dgn65d6cd82017-04-11 00:36:36433bool ContentSuggestionsService::AreRemoteSuggestionsEnabled() const {
Nicolas Dossou-gbete18994652017-07-19 16:32:17434 return remote_suggestions_provider_ &&
435 !remote_suggestions_provider_->IsDisabled();
dgnb8a8a5c2017-03-31 12:35:36436}
437
pke6dbb90af2016-07-08 14:00:46438////////////////////////////////////////////////////////////////////////////////
439// Private methods
440
441void ContentSuggestionsService::OnNewSuggestions(
pke4d3a4d62016-08-02 09:06:21442 ContentSuggestionsProvider* provider,
443 Category category,
treib62e819e2016-09-27 11:47:34444 std::vector<ContentSuggestion> suggestions) {
treib534523b2016-10-20 16:19:44445 // Providers shouldn't call this when they're in a non-available state.
446 DCHECK(
447 IsCategoryStatusInitOrAvailable(provider->GetCategoryStatus(category)));
448
dgn52914722016-10-18 10:28:42449 if (TryRegisterProviderForCategory(provider, category)) {
pke4d3a4d62016-08-02 09:06:21450 NotifyCategoryStatusChanged(category);
dgn52914722016-10-18 10:28:42451 } else if (IsCategoryDismissed(category)) {
452 // The category has been registered as a dismissed one. We need to
453 // check if the dismissal can be cleared now that we received new data.
vitaliii4408d6c2016-11-21 14:16:24454 if (suggestions.empty()) {
dgn52914722016-10-18 10:28:42455 return;
vitaliii4408d6c2016-11-21 14:16:24456 }
dgn52914722016-10-18 10:28:42457
458 RestoreDismissedCategory(category);
459 StoreDismissedCategoriesToPrefs();
460
461 NotifyCategoryStatusChanged(category);
462 }
pke3b2e3632016-08-12 12:52:53463
treib39fffa12016-10-14 14:59:10464 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) {
465 // A provider shouldn't send us suggestions while it's not available.
466 DCHECK(suggestions.empty());
pke3b2e3632016-08-12 12:52:53467 return;
treib39fffa12016-10-14 14:59:10468 }
pke6dbb90af2016-07-08 14:00:46469
treib62e819e2016-09-27 11:47:34470 suggestions_by_category_[category] = std::move(suggestions);
pke6dbb90af2016-07-08 14:00:46471
vitaliii4408d6c2016-11-21 14:16:24472 for (Observer& observer : observers_) {
ericwilligers42b92c12016-10-24 20:21:13473 observer.OnNewSuggestions(category);
vitaliii4408d6c2016-11-21 14:16:24474 }
pke6dbb90af2016-07-08 14:00:46475}
476
477void ContentSuggestionsService::OnCategoryStatusChanged(
pke4d3a4d62016-08-02 09:06:21478 ContentSuggestionsProvider* provider,
479 Category category,
pke9c5095ac2016-08-01 13:53:12480 CategoryStatus new_status) {
pke4d3a4d62016-08-02 09:06:21481 if (new_status == CategoryStatus::NOT_PROVIDED) {
dgn52914722016-10-18 10:28:42482 UnregisterCategory(category, provider);
pke4d3a4d62016-08-02 09:06:21483 } else {
vitaliii4408d6c2016-11-21 14:16:24484 if (!IsCategoryStatusAvailable(new_status)) {
dgn52914722016-10-18 10:28:42485 suggestions_by_category_.erase(category);
vitaliii4408d6c2016-11-21 14:16:24486 }
dgn52914722016-10-18 10:28:42487 TryRegisterProviderForCategory(provider, category);
pke4d3a4d62016-08-02 09:06:21488 DCHECK_EQ(new_status, provider->GetCategoryStatus(category));
489 }
dgn52914722016-10-18 10:28:42490
vitaliii4408d6c2016-11-21 14:16:24491 if (!IsCategoryDismissed(category)) {
dgn52914722016-10-18 10:28:42492 NotifyCategoryStatusChanged(category);
vitaliii4408d6c2016-11-21 14:16:24493 }
pke6dbb90af2016-07-08 14:00:46494}
495
pke2a48f852016-08-18 13:33:52496void ContentSuggestionsService::OnSuggestionInvalidated(
497 ContentSuggestionsProvider* provider,
treib4bbc54922016-09-28 17:26:44498 const ContentSuggestion::ID& suggestion_id) {
499 RemoveSuggestionByID(suggestion_id);
vitaliii4408d6c2016-11-21 14:16:24500 for (Observer& observer : observers_) {
ericwilligers42b92c12016-10-24 20:21:13501 observer.OnSuggestionInvalidated(suggestion_id);
vitaliii4408d6c2016-11-21 14:16:24502 }
pke2a48f852016-08-18 13:33:52503}
Miyoung Shin23737f62019-07-23 15:43:31504// signin::IdentityManager::Observer implementation
Colin Blundell076714ff2018-02-12 17:26:14505void ContentSuggestionsService::OnPrimaryAccountSet(
Sylvain Defresnebbed9512019-02-08 10:54:30506 const CoreAccountInfo& account_info) {
Colin Blundellce266a692018-01-16 11:07:16507 OnSignInStateChanged(/*has_signed_in=*/true);
dgnf5708892016-11-22 10:36:22508}
509
Colin Blundell076714ff2018-02-12 17:26:14510void ContentSuggestionsService::OnPrimaryAccountCleared(
Sylvain Defresne07a3bed2019-02-11 15:02:51511 const CoreAccountInfo& account_info) {
Colin Blundellce266a692018-01-16 11:07:16512 OnSignInStateChanged(/*has_signed_in=*/false);
dgnf5708892016-11-22 10:36:22513}
514
vitaliii45941152016-09-05 08:58:13515// history::HistoryServiceObserver implementation.
516void ContentSuggestionsService::OnURLsDeleted(
517 history::HistoryService* history_service,
Christian Dullweber34464892018-05-16 07:52:17518 const history::DeletionInfo& deletion_info) {
vitaliii45941152016-09-05 08:58:13519 // We don't care about expired entries.
Christian Dullweber34464892018-05-16 07:52:17520 if (deletion_info.is_from_expiration()) {
vitaliii45941152016-09-05 08:58:13521 return;
vitaliii4408d6c2016-11-21 14:16:24522 }
vitaliii45941152016-09-05 08:58:13523
Christian Dullweber34464892018-05-16 07:52:17524 if (deletion_info.IsAllHistory()) {
Ken Rockotebba67882019-12-20 18:22:24525 base::RepeatingCallback<bool(const GURL& url)> filter =
526 base::BindRepeating([](const GURL& url) { return true; });
tschumann5829c3412017-01-09 21:45:43527 ClearHistory(base::Time(), base::Time::Max(), filter);
vitaliii45941152016-09-05 08:58:13528 } else {
tschumann5829c3412017-01-09 21:45:43529 // If a user deletes a single URL, we don't consider this a clear user
530 // intend to clear our data.
531 // TODO(tschumann): Single URL deletions should be handled on a case-by-case
532 // basis. However this depends on the provider's details and thus cannot be
533 // done here. Introduce a OnURLsDeleted() method on the providers to move
534 // this decision further down.
Christian Dullweber34464892018-05-16 07:52:17535 if (deletion_info.deleted_rows().size() < 2) {
vitaliii45941152016-09-05 08:58:13536 return;
vitaliii4408d6c2016-11-21 14:16:24537 }
vitaliii45941152016-09-05 08:58:13538 std::set<GURL> deleted_urls;
Christian Dullweber34464892018-05-16 07:52:17539 for (const history::URLRow& row : deletion_info.deleted_rows()) {
vitaliii45941152016-09-05 08:58:13540 deleted_urls.insert(row.url());
541 }
Ken Rockotebba67882019-12-20 18:22:24542 base::RepeatingCallback<bool(const GURL& url)> filter =
543 base::BindRepeating([](const std::set<GURL>& set,
544 const GURL& url) { return set.count(url) != 0; },
545 deleted_urls);
tschumann5829c3412017-01-09 21:45:43546 // We usually don't have any time-related information (the URLRow objects
547 // usually don't provide a |last_visit()| timestamp. Hence we simply clear
548 // the whole history for the selected URLs.
549 ClearHistory(base::Time(), base::Time::Max(), filter);
vitaliii45941152016-09-05 08:58:13550 }
551}
552
553void ContentSuggestionsService::HistoryServiceBeingDeleted(
554 history::HistoryService* history_service) {
555 history_service_observer_.RemoveAll();
556}
557
dgn52914722016-10-18 10:28:42558bool ContentSuggestionsService::TryRegisterProviderForCategory(
pke4d3a4d62016-08-02 09:06:21559 ContentSuggestionsProvider* provider,
560 Category category) {
561 auto it = providers_by_category_.find(category);
562 if (it != providers_by_category_.end()) {
563 DCHECK_EQ(it->second, provider);
564 return false;
565 }
566
mvanouwerkerk52783d92016-10-12 11:03:40567 auto dismissed_it = dismissed_providers_by_category_.find(category);
568 if (dismissed_it != dismissed_providers_by_category_.end()) {
dgn52914722016-10-18 10:28:42569 // The initialisation of dismissed categories registers them with |nullptr|
570 // for providers, we need to check for that to see if the provider is
571 // already registered or not.
572 if (!dismissed_it->second) {
573 dismissed_it->second = provider;
574 } else {
575 DCHECK_EQ(dismissed_it->second, provider);
576 }
577 return false;
mvanouwerkerk52783d92016-10-12 11:03:40578 }
579
dgn52914722016-10-18 10:28:42580 RegisterCategory(category, provider);
581 return true;
582}
583
584void ContentSuggestionsService::RegisterCategory(
585 Category category,
586 ContentSuggestionsProvider* provider) {
Jan Wilken Dörrie45d34f42019-06-08 09:40:54587 DCHECK(!base::Contains(providers_by_category_, category));
dgn52914722016-10-18 10:28:42588 DCHECK(!IsCategoryDismissed(category));
589
pke4d3a4d62016-08-02 09:06:21590 providers_by_category_[category] = provider;
591 categories_.push_back(category);
pke4d3a4d62016-08-02 09:06:21592 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) {
593 suggestions_by_category_.insert(
594 std::make_pair(category, std::vector<ContentSuggestion>()));
595 }
dgn52914722016-10-18 10:28:42596}
597
598void ContentSuggestionsService::UnregisterCategory(
599 Category category,
600 ContentSuggestionsProvider* provider) {
601 auto providers_it = providers_by_category_.find(category);
602 if (providers_it == providers_by_category_.end()) {
603 DCHECK(IsCategoryDismissed(category));
604 return;
605 }
606
607 DCHECK_EQ(provider, providers_it->second);
608 providers_by_category_.erase(providers_it);
609 categories_.erase(
610 std::find(categories_.begin(), categories_.end(), category));
611 suggestions_by_category_.erase(category);
pke6dbb90af2016-07-08 14:00:46612}
613
pke2a48f852016-08-18 13:33:52614bool ContentSuggestionsService::RemoveSuggestionByID(
treib4bbc54922016-09-28 17:26:44615 const ContentSuggestion::ID& suggestion_id) {
pke2a48f852016-08-18 13:33:52616 std::vector<ContentSuggestion>* suggestions =
treib4bbc54922016-09-28 17:26:44617 &suggestions_by_category_[suggestion_id.category()];
pke2a48f852016-08-18 13:33:52618 auto position =
619 std::find_if(suggestions->begin(), suggestions->end(),
620 [&suggestion_id](const ContentSuggestion& suggestion) {
621 return suggestion_id == suggestion.id();
622 });
vitaliii4408d6c2016-11-21 14:16:24623 if (position == suggestions->end()) {
pke2a48f852016-08-18 13:33:52624 return false;
vitaliii4408d6c2016-11-21 14:16:24625 }
pke2a48f852016-08-18 13:33:52626 suggestions->erase(position);
treib063e6a62016-08-25 11:34:29627
pke2a48f852016-08-18 13:33:52628 return true;
629}
630
pke9c5095ac2016-08-01 13:53:12631void ContentSuggestionsService::NotifyCategoryStatusChanged(Category category) {
vitaliii4408d6c2016-11-21 14:16:24632 for (Observer& observer : observers_) {
ericwilligers42b92c12016-10-24 20:21:13633 observer.OnCategoryStatusChanged(category, GetCategoryStatus(category));
vitaliii4408d6c2016-11-21 14:16:24634 }
pke6dbb90af2016-07-08 14:00:46635}
636
Colin Blundellce266a692018-01-16 11:07:16637void ContentSuggestionsService::OnSignInStateChanged(bool has_signed_in) {
dgnf5708892016-11-22 10:36:22638 // First notify the providers, so they can make the required changes.
639 for (const auto& provider : providers_) {
Colin Blundellce266a692018-01-16 11:07:16640 provider->OnSignInStateChanged(has_signed_in);
dgnf5708892016-11-22 10:36:22641 }
642
643 // Finally notify the observers so they refresh only after the backend is
644 // ready.
645 for (Observer& observer : observers_) {
646 observer.OnFullRefreshRequired();
647 }
648}
649
dgn52914722016-10-18 10:28:42650bool ContentSuggestionsService::IsCategoryDismissed(Category category) const {
Jan Wilken Dörrie45d34f42019-06-08 09:40:54651 return base::Contains(dismissed_providers_by_category_, category);
dgn52914722016-10-18 10:28:42652}
653
654void ContentSuggestionsService::RestoreDismissedCategory(Category category) {
655 auto dismissed_it = dismissed_providers_by_category_.find(category);
Jan Wilken Dörrie45d34f42019-06-08 09:40:54656 DCHECK(base::Contains(dismissed_providers_by_category_, category));
dgn52914722016-10-18 10:28:42657
658 // Keep the reference to the provider and remove it from the dismissed ones,
659 // because the category registration enforces that it's not dismissed.
660 ContentSuggestionsProvider* provider = dismissed_it->second;
661 dismissed_providers_by_category_.erase(dismissed_it);
662
vitaliii4408d6c2016-11-21 14:16:24663 if (provider) {
dgn52914722016-10-18 10:28:42664 RegisterCategory(category, provider);
vitaliii4408d6c2016-11-21 14:16:24665 }
dgn52914722016-10-18 10:28:42666}
667
668void ContentSuggestionsService::RestoreDismissedCategoriesFromPrefs() {
669 // This must only be called at startup.
670 DCHECK(dismissed_providers_by_category_.empty());
671 DCHECK(providers_by_category_.empty());
672
673 const base::ListValue* list =
674 pref_service_->GetList(prefs::kDismissedCategories);
jdoerriea5676c62017-04-11 18:09:14675 for (const base::Value& entry : *list) {
dgn52914722016-10-18 10:28:42676 int id = 0;
jdoerriea5676c62017-04-11 18:09:14677 if (!entry.GetAsInteger(&id)) {
678 DLOG(WARNING) << "Invalid category pref value: " << entry;
dgn52914722016-10-18 10:28:42679 continue;
680 }
681
682 // When the provider is registered, it will be stored in this map.
vitaliii7456f5a2016-12-19 11:13:25683 dismissed_providers_by_category_[Category::FromIDValue(id)] = nullptr;
dgn52914722016-10-18 10:28:42684 }
685}
686
687void ContentSuggestionsService::StoreDismissedCategoriesToPrefs() {
688 base::ListValue list;
689 for (const auto& category_provider_pair : dismissed_providers_by_category_) {
690 list.AppendInteger(category_provider_pair.first.id());
691 }
692
693 pref_service_->Set(prefs::kDismissedCategories, list);
694}
695
vitaliii69689f312017-10-25 17:36:06696void ContentSuggestionsService::DestroyCategoryAndItsProvider(
697 Category category) {
698 // Destroying articles category is more complex and not implemented.
699 DCHECK_NE(category, Category::FromKnownCategory(KnownCategories::ARTICLES));
700
701 if (providers_by_category_.count(category) != 1) {
702 return;
703 }
704
705 { // Destroy the provider and delete its mentions.
706 ContentSuggestionsProvider* raw_provider = providers_by_category_[category];
707 base::EraseIf(
708 providers_,
709 [&raw_provider](
710 const std::unique_ptr<ContentSuggestionsProvider>& provider) {
711 return provider.get() == raw_provider;
712 });
713 providers_by_category_.erase(category);
714
715 if (dismissed_providers_by_category_.count(category) == 1) {
716 dismissed_providers_by_category_[category] = nullptr;
717 }
718 }
719
720 suggestions_by_category_.erase(category);
721
722 auto it = std::find(categories_.begin(), categories_.end(), category);
723 categories_.erase(it);
724
725 // Notify observers that the category is gone.
726 NotifyCategoryStatusChanged(category);
727}
728
pke6dbb90af2016-07-08 14:00:46729} // namespace ntp_snippets