1 // 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>
9 #include <set>
10 #include <utility>
11
12 #include "base/bind.h"
13 #include "base/location.h"
14 #include "base/metrics/histogram_macros.h"
15 #include "base/stl_util.h"
16 #include "base/strings/string_number_conversions.h"
17 #include "base/strings/stringprintf.h"
18 #include "base/threading/thread_task_runner_handle.h"
19 #include "base/time/default_clock.h"
20 #include "base/values.h"
21 #include "components/favicon/core/large_icon_service.h"
22 #include "components/favicon_base/fallback_icon_style.h"
23 #include "components/favicon_base/favicon_types.h"
24 #include "components/ntp_snippets/content_suggestions_metrics.h"
25 #include "components/ntp_snippets/pref_names.h"
26 #include "components/ntp_snippets/remote/remote_suggestions_provider.h"
27 #include "components/prefs/pref_registry_simple.h"
28 #include "components/prefs/pref_service.h"
29 #include "net/traffic_annotation/network_traffic_annotation.h"
30 #include "ui/gfx/image/image.h"
31
32 namespace ntp_snippets {
33
34 namespace {
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
40 enum class FaviconFetchResult {
41 SUCCESS_CACHED = 0,
42 SUCCESS_FETCHED = 1,
43 FAILURE = 2,
44 COUNT = 3
45 };
46
RecordFaviconFetchResult(FaviconFetchResult result)47 void RecordFaviconFetchResult(FaviconFetchResult result) {
48 UMA_HISTOGRAM_ENUMERATION(
49 "NewTabPage.ContentSuggestions.ArticleFaviconFetchResult", result,
50 FaviconFetchResult::COUNT);
51 }
52
53 } // namespace
54
ContentSuggestionsService(State state,signin::IdentityManager * identity_manager,history::HistoryService * history_service,favicon::LargeIconService * large_icon_service,PrefService * pref_service,std::unique_ptr<CategoryRanker> category_ranker,std::unique_ptr<UserClassifier> user_classifier,std::unique_ptr<RemoteSuggestionsScheduler> remote_suggestions_scheduler)55 ContentSuggestionsService::ContentSuggestionsService(
56 State state,
57 signin::IdentityManager* identity_manager,
58 history::HistoryService* history_service,
59 favicon::LargeIconService* large_icon_service,
60 PrefService* pref_service,
61 std::unique_ptr<CategoryRanker> category_ranker,
62 std::unique_ptr<UserClassifier> user_classifier,
63 std::unique_ptr<RemoteSuggestionsScheduler> remote_suggestions_scheduler)
64 : state_(state),
65 identity_manager_observer_(this),
66 history_service_observer_(this),
67 remote_suggestions_provider_(nullptr),
68 large_icon_service_(large_icon_service),
69 pref_service_(pref_service),
70 remote_suggestions_scheduler_(std::move(remote_suggestions_scheduler)),
71 user_classifier_(std::move(user_classifier)),
72 category_ranker_(std::move(category_ranker)) {
73 // Can be null in tests.
74 if (identity_manager) {
75 identity_manager_observer_.Add(identity_manager);
76 }
77
78 if (history_service) {
79 history_service_observer_.Add(history_service);
80 }
81
82 RestoreDismissedCategoriesFromPrefs();
83 }
84
85 ContentSuggestionsService::~ContentSuggestionsService() = default;
86
Shutdown()87 void ContentSuggestionsService::Shutdown() {
88 remote_suggestions_provider_ = nullptr;
89 remote_suggestions_scheduler_ = nullptr;
90 suggestions_by_category_.clear();
91 providers_by_category_.clear();
92 categories_.clear();
93 providers_.clear();
94 state_ = State::DISABLED;
95 for (Observer& observer : observers_) {
96 observer.ContentSuggestionsServiceShutdown();
97 }
98 }
99
100 // static
RegisterProfilePrefs(PrefRegistrySimple * registry)101 void ContentSuggestionsService::RegisterProfilePrefs(
102 PrefRegistrySimple* registry) {
103 registry->RegisterListPref(prefs::kDismissedCategories);
104 }
105
GetCategories() const106 std::vector<Category> ContentSuggestionsService::GetCategories() const {
107 std::vector<Category> sorted_categories = categories_;
108 std::sort(sorted_categories.begin(), sorted_categories.end(),
109 [this](const Category& left, const Category& right) {
110 return category_ranker_->Compare(left, right);
111 });
112 return sorted_categories;
113 }
114
GetCategoryStatus(Category category) const115 CategoryStatus ContentSuggestionsService::GetCategoryStatus(
116 Category category) const {
117 if (state_ == State::DISABLED) {
118 return CategoryStatus::ALL_SUGGESTIONS_EXPLICITLY_DISABLED;
119 }
120
121 auto iterator = providers_by_category_.find(category);
122 if (iterator == providers_by_category_.end()) {
123 return CategoryStatus::NOT_PROVIDED;
124 }
125
126 return iterator->second->GetCategoryStatus(category);
127 }
128
GetCategoryInfo(Category category) const129 base::Optional<CategoryInfo> ContentSuggestionsService::GetCategoryInfo(
130 Category category) const {
131 auto iterator = providers_by_category_.find(category);
132 if (iterator == providers_by_category_.end()) {
133 return base::Optional<CategoryInfo>();
134 }
135 return iterator->second->GetCategoryInfo(category);
136 }
137
138 const std::vector<ContentSuggestion>&
GetSuggestionsForCategory(Category category) const139 ContentSuggestionsService::GetSuggestionsForCategory(Category category) const {
140 auto iterator = suggestions_by_category_.find(category);
141 if (iterator == suggestions_by_category_.end()) {
142 return no_suggestions_;
143 }
144 return iterator->second;
145 }
146
FetchSuggestionImage(const ContentSuggestion::ID & suggestion_id,ImageFetchedCallback callback)147 void ContentSuggestionsService::FetchSuggestionImage(
148 const ContentSuggestion::ID& suggestion_id,
149 ImageFetchedCallback callback) {
150 if (!providers_by_category_.count(suggestion_id.category())) {
151 LOG(WARNING) << "Requested image for suggestion " << suggestion_id
152 << " for unavailable category " << suggestion_id.category();
153 base::ThreadTaskRunnerHandle::Get()->PostTask(
154 FROM_HERE, base::BindOnce(std::move(callback), gfx::Image()));
155 return;
156 }
157 providers_by_category_[suggestion_id.category()]->FetchSuggestionImage(
158 suggestion_id, std::move(callback));
159 }
160
FetchSuggestionImageData(const ContentSuggestion::ID & suggestion_id,ImageDataFetchedCallback callback)161 void ContentSuggestionsService::FetchSuggestionImageData(
162 const ContentSuggestion::ID& suggestion_id,
163 ImageDataFetchedCallback callback) {
164 if (!providers_by_category_.count(suggestion_id.category())) {
165 LOG(WARNING) << "Requested image for suggestion " << suggestion_id
166 << " for unavailable category " << suggestion_id.category();
167 base::ThreadTaskRunnerHandle::Get()->PostTask(
168 FROM_HERE, base::BindOnce(std::move(callback), std::string()));
169 return;
170 }
171 providers_by_category_[suggestion_id.category()]->FetchSuggestionImageData(
172 suggestion_id, std::move(callback));
173 }
174
175 // TODO(jkrcal): Split the favicon fetching into a separate class.
FetchSuggestionFavicon(const ContentSuggestion::ID & suggestion_id,int minimum_size_in_pixel,int desired_size_in_pixel,ImageFetchedCallback callback)176 void ContentSuggestionsService::FetchSuggestionFavicon(
177 const ContentSuggestion::ID& suggestion_id,
178 int minimum_size_in_pixel,
179 int desired_size_in_pixel,
180 ImageFetchedCallback callback) {
181 const GURL& domain_with_favicon = GetFaviconDomain(suggestion_id);
182 if (!domain_with_favicon.is_valid() || !large_icon_service_) {
183 base::ThreadTaskRunnerHandle::Get()->PostTask(
184 FROM_HERE, base::BindOnce(std::move(callback), gfx::Image()));
185 RecordFaviconFetchResult(FaviconFetchResult::FAILURE);
186 return;
187 }
188
189 GetFaviconFromCache(domain_with_favicon, minimum_size_in_pixel,
190 desired_size_in_pixel, std::move(callback),
191 /*continue_to_google_server=*/true);
192 }
193
GetFaviconDomain(const ContentSuggestion::ID & suggestion_id)194 GURL ContentSuggestionsService::GetFaviconDomain(
195 const ContentSuggestion::ID& suggestion_id) {
196 const std::vector<ContentSuggestion>& suggestions =
197 suggestions_by_category_[suggestion_id.category()];
198 auto position =
199 std::find_if(suggestions.begin(), suggestions.end(),
200 [&suggestion_id](const ContentSuggestion& suggestion) {
201 return suggestion_id == suggestion.id();
202 });
203 if (position != suggestions.end()) {
204 return position->url_with_favicon();
205 }
206
207 // Look up the URL in the archive of |remote_suggestions_provider_|.
208 // TODO(jkrcal): Fix how Fetch more works or find other ways to remove this
209 // hack. crbug.com/714031
210 if (providers_by_category_[suggestion_id.category()] ==
211 remote_suggestions_provider_) {
212 return remote_suggestions_provider_->GetUrlWithFavicon(suggestion_id);
213 }
214 return GURL();
215 }
216
GetFaviconFromCache(const GURL & publisher_url,int minimum_size_in_pixel,int desired_size_in_pixel,ImageFetchedCallback callback,bool continue_to_google_server)217 void ContentSuggestionsService::GetFaviconFromCache(
218 const GURL& publisher_url,
219 int minimum_size_in_pixel,
220 int desired_size_in_pixel,
221 ImageFetchedCallback callback,
222 bool continue_to_google_server) {
223 // TODO(jkrcal): Create a general wrapper function in LargeIconService that
224 // does handle the get-from-cache-and-fallback-to-google-server functionality
225 // in one shot (for all clients that do not need to react in between).
226
227 // Use desired_size = 0 for getting the icon from the cache (so that the icon
228 // is not poorly rescaled by LargeIconService).
229 large_icon_service_->GetLargeIconImageOrFallbackStyleForPageUrl(
230 publisher_url, minimum_size_in_pixel, /*desired_size_in_pixel=*/0,
231 base::BindOnce(&ContentSuggestionsService::OnGetFaviconFromCacheFinished,
232 base::Unretained(this), publisher_url,
233 minimum_size_in_pixel, desired_size_in_pixel,
234 std::move(callback), continue_to_google_server),
235 &favicons_task_tracker_);
236 }
237
OnGetFaviconFromCacheFinished(const GURL & publisher_url,int minimum_size_in_pixel,int desired_size_in_pixel,ImageFetchedCallback callback,bool continue_to_google_server,const favicon_base::LargeIconImageResult & result)238 void ContentSuggestionsService::OnGetFaviconFromCacheFinished(
239 const GURL& publisher_url,
240 int minimum_size_in_pixel,
241 int desired_size_in_pixel,
242 ImageFetchedCallback callback,
243 bool continue_to_google_server,
244 const favicon_base::LargeIconImageResult& result) {
245 if (!result.image.IsEmpty()) {
246 std::move(callback).Run(result.image);
247 // The icon is from cache if we haven't gone to Google server yet. The icon
248 // is freshly fetched, otherwise.
249 RecordFaviconFetchResult(continue_to_google_server
250 ? FaviconFetchResult::SUCCESS_CACHED
251 : FaviconFetchResult::SUCCESS_FETCHED);
252 // Update the time when the icon was last requested - postpone thus the
253 // automatic eviction of the favicon from the favicon database.
254 large_icon_service_->TouchIconFromGoogleServer(result.icon_url);
255 return;
256 }
257
258 if (!continue_to_google_server ||
259 (result.fallback_icon_style &&
260 !result.fallback_icon_style->is_default_background_color)) {
261 // We cannot download from the server if there is some small icon in the
262 // cache (resulting in non-default background color) or if we already did
263 // so.
264 std::move(callback).Run(gfx::Image());
265 RecordFaviconFetchResult(FaviconFetchResult::FAILURE);
266 return;
267 }
268
269 // Try to fetch the favicon from a Google favicon server.
270 // TODO(jkrcal): Currently used only for Articles for you which have public
271 // URLs. Let the provider decide whether |publisher_url| may be private or
272 // not.
273 net::NetworkTrafficAnnotationTag traffic_annotation =
274 net::DefineNetworkTrafficAnnotation("content_suggestion_get_favicon", R"(
275 semantics {
276 sender: "Content Suggestion"
277 description:
278 "Sends a request to a Google server to retrieve the favicon bitmap "
279 "for an article suggestion on the new tab page (URLs are public "
280 "and provided by Google)."
281 trigger:
282 "A request can be sent if Chrome does not have a favicon for a "
283 "particular page."
284 data: "Page URL and desired icon size."
285 destination: GOOGLE_OWNED_SERVICE
286 }
287 policy {
288 cookies_allowed: NO
289 setting: "This feature cannot be disabled by settings."
290 policy_exception_justification: "Not implemented."
291 })");
292 large_icon_service_
293 ->GetLargeIconOrFallbackStyleFromGoogleServerSkippingLocalCache(
294 publisher_url,
295 /*may_page_url_be_private=*/false,
296 /*should_trim_page_url_path=*/false, traffic_annotation,
297 base::BindOnce(
298 &ContentSuggestionsService::OnGetFaviconFromGoogleServerFinished,
299 base::Unretained(this), publisher_url, minimum_size_in_pixel,
300 desired_size_in_pixel, std::move(callback)));
301 }
302
OnGetFaviconFromGoogleServerFinished(const GURL & publisher_url,int minimum_size_in_pixel,int desired_size_in_pixel,ImageFetchedCallback callback,favicon_base::GoogleFaviconServerRequestStatus status)303 void ContentSuggestionsService::OnGetFaviconFromGoogleServerFinished(
304 const GURL& publisher_url,
305 int minimum_size_in_pixel,
306 int desired_size_in_pixel,
307 ImageFetchedCallback callback,
308 favicon_base::GoogleFaviconServerRequestStatus status) {
309 if (status != favicon_base::GoogleFaviconServerRequestStatus::SUCCESS) {
310 std::move(callback).Run(gfx::Image());
311 RecordFaviconFetchResult(FaviconFetchResult::FAILURE);
312 return;
313 }
314
315 GetFaviconFromCache(publisher_url, minimum_size_in_pixel,
316 desired_size_in_pixel, std::move(callback),
317 /*continue_to_google_server=*/false);
318 }
319
ClearHistory(base::Time begin,base::Time end,const base::RepeatingCallback<bool (const GURL & url)> & filter)320 void ContentSuggestionsService::ClearHistory(
321 base::Time begin,
322 base::Time end,
323 const base::RepeatingCallback<bool(const GURL& url)>& filter) {
324 for (const auto& provider : providers_) {
325 provider->ClearHistory(begin, end, filter);
326 }
327 category_ranker_->ClearHistory(begin, end);
328 // This potentially removed personalized data which we shouldn't display
329 // anymore.
330 for (Observer& observer : observers_) {
331 observer.OnFullRefreshRequired();
332 }
333 }
334
ClearAllCachedSuggestions()335 void ContentSuggestionsService::ClearAllCachedSuggestions() {
336 suggestions_by_category_.clear();
337 for (const auto& provider : providers_) {
338 provider->ClearCachedSuggestions();
339 }
340 for (Observer& observer : observers_) {
341 observer.OnFullRefreshRequired();
342 }
343 }
344
GetDismissedSuggestionsForDebugging(Category category,DismissedSuggestionsCallback callback)345 void ContentSuggestionsService::GetDismissedSuggestionsForDebugging(
346 Category category,
347 DismissedSuggestionsCallback callback) {
348 auto iterator = providers_by_category_.find(category);
349 if (iterator != providers_by_category_.end()) {
350 iterator->second->GetDismissedSuggestionsForDebugging(category,
351 std::move(callback));
352 } else {
353 std::move(callback).Run(std::vector<ContentSuggestion>());
354 }
355 }
356
ClearDismissedSuggestionsForDebugging(Category category)357 void ContentSuggestionsService::ClearDismissedSuggestionsForDebugging(
358 Category category) {
359 auto iterator = providers_by_category_.find(category);
360 if (iterator != providers_by_category_.end()) {
361 iterator->second->ClearDismissedSuggestionsForDebugging(category);
362 }
363 }
364
DismissSuggestion(const ContentSuggestion::ID & suggestion_id)365 void ContentSuggestionsService::DismissSuggestion(
366 const ContentSuggestion::ID& suggestion_id) {
367 if (!providers_by_category_.count(suggestion_id.category())) {
368 LOG(WARNING) << "Dismissed suggestion " << suggestion_id
369 << " for unavailable category " << suggestion_id.category();
370 return;
371 }
372
373 metrics::RecordContentSuggestionDismissed();
374
375 providers_by_category_[suggestion_id.category()]->DismissSuggestion(
376 suggestion_id);
377
378 // Remove the suggestion locally if it is present. A suggestion may be missing
379 // localy e.g. if it was sent to UI through |Fetch| or it has been dismissed
380 // from a different NTP.
381 RemoveSuggestionByID(suggestion_id);
382 }
383
DismissCategory(Category category)384 void ContentSuggestionsService::DismissCategory(Category category) {
385 auto providers_it = providers_by_category_.find(category);
386 if (providers_it == providers_by_category_.end()) {
387 return;
388 }
389
390 metrics::RecordCategoryDismissed();
391
392 ContentSuggestionsProvider* provider = providers_it->second;
393 UnregisterCategory(category, provider);
394
395 dismissed_providers_by_category_[category] = provider;
396 StoreDismissedCategoriesToPrefs();
397
398 category_ranker_->OnCategoryDismissed(category);
399 }
400
RestoreDismissedCategories()401 void ContentSuggestionsService::RestoreDismissedCategories() {
402 // Make a copy as the original will be modified during iteration.
403 auto dismissed_providers_by_category_copy = dismissed_providers_by_category_;
404 for (const auto& category_provider_pair :
405 dismissed_providers_by_category_copy) {
406 RestoreDismissedCategory(category_provider_pair.first);
407 }
408 StoreDismissedCategoriesToPrefs();
409 DCHECK(dismissed_providers_by_category_.empty());
410 }
411
AddObserver(Observer * observer)412 void ContentSuggestionsService::AddObserver(Observer* observer) {
413 observers_.AddObserver(observer);
414 }
415
RemoveObserver(Observer * observer)416 void ContentSuggestionsService::RemoveObserver(Observer* observer) {
417 observers_.RemoveObserver(observer);
418 }
419
RegisterProvider(std::unique_ptr<ContentSuggestionsProvider> provider)420 void ContentSuggestionsService::RegisterProvider(
421 std::unique_ptr<ContentSuggestionsProvider> provider) {
422 DCHECK(state_ == State::ENABLED);
423 providers_.push_back(std::move(provider));
424 }
425
Fetch(const Category & category,const std::set<std::string> & known_suggestion_ids,FetchDoneCallback callback)426 void ContentSuggestionsService::Fetch(
427 const Category& category,
428 const std::set<std::string>& known_suggestion_ids,
429 FetchDoneCallback callback) {
430 auto providers_it = providers_by_category_.find(category);
431 if (providers_it == providers_by_category_.end()) {
432 return;
433 }
434
435 metrics::RecordFetchAction();
436
437 providers_it->second->Fetch(category, known_suggestion_ids,
438 std::move(callback));
439 }
440
ReloadSuggestions()441 void ContentSuggestionsService::ReloadSuggestions() {
442 for (const auto& provider : providers_) {
443 provider->ReloadSuggestions();
444 }
445 }
446
AreRemoteSuggestionsEnabled() const447 bool ContentSuggestionsService::AreRemoteSuggestionsEnabled() const {
448 return remote_suggestions_provider_ &&
449 !remote_suggestions_provider_->IsDisabled();
450 }
451
452 ////////////////////////////////////////////////////////////////////////////////
453 // Private methods
454
OnNewSuggestions(ContentSuggestionsProvider * provider,Category category,std::vector<ContentSuggestion> suggestions)455 void ContentSuggestionsService::OnNewSuggestions(
456 ContentSuggestionsProvider* provider,
457 Category category,
458 std::vector<ContentSuggestion> suggestions) {
459 // Providers shouldn't call this when they're in a non-available state.
460 DCHECK(
461 IsCategoryStatusInitOrAvailable(provider->GetCategoryStatus(category)));
462
463 if (TryRegisterProviderForCategory(provider, category)) {
464 NotifyCategoryStatusChanged(category);
465 } else if (IsCategoryDismissed(category)) {
466 // The category has been registered as a dismissed one. We need to
467 // check if the dismissal can be cleared now that we received new data.
468 if (suggestions.empty()) {
469 return;
470 }
471
472 RestoreDismissedCategory(category);
473 StoreDismissedCategoriesToPrefs();
474
475 NotifyCategoryStatusChanged(category);
476 }
477
478 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) {
479 // A provider shouldn't send us suggestions while it's not available.
480 DCHECK(suggestions.empty());
481 return;
482 }
483
484 suggestions_by_category_[category] = std::move(suggestions);
485
486 for (Observer& observer : observers_) {
487 observer.OnNewSuggestions(category);
488 }
489 }
490
OnCategoryStatusChanged(ContentSuggestionsProvider * provider,Category category,CategoryStatus new_status)491 void ContentSuggestionsService::OnCategoryStatusChanged(
492 ContentSuggestionsProvider* provider,
493 Category category,
494 CategoryStatus new_status) {
495 if (new_status == CategoryStatus::NOT_PROVIDED) {
496 UnregisterCategory(category, provider);
497 } else {
498 if (!IsCategoryStatusAvailable(new_status)) {
499 suggestions_by_category_.erase(category);
500 }
501 TryRegisterProviderForCategory(provider, category);
502 DCHECK_EQ(new_status, provider->GetCategoryStatus(category));
503 }
504
505 if (!IsCategoryDismissed(category)) {
506 NotifyCategoryStatusChanged(category);
507 }
508 }
509
OnSuggestionInvalidated(ContentSuggestionsProvider * provider,const ContentSuggestion::ID & suggestion_id)510 void ContentSuggestionsService::OnSuggestionInvalidated(
511 ContentSuggestionsProvider* provider,
512 const ContentSuggestion::ID& suggestion_id) {
513 RemoveSuggestionByID(suggestion_id);
514 for (Observer& observer : observers_) {
515 observer.OnSuggestionInvalidated(suggestion_id);
516 }
517 }
518 // signin::IdentityManager::Observer implementation
OnPrimaryAccountSet(const CoreAccountInfo & account_info)519 void ContentSuggestionsService::OnPrimaryAccountSet(
520 const CoreAccountInfo& account_info) {
521 OnSignInStateChanged(/*has_signed_in=*/true);
522 }
523
OnPrimaryAccountCleared(const CoreAccountInfo & account_info)524 void ContentSuggestionsService::OnPrimaryAccountCleared(
525 const CoreAccountInfo& account_info) {
526 OnSignInStateChanged(/*has_signed_in=*/false);
527 }
528
529 // history::HistoryServiceObserver implementation.
OnURLsDeleted(history::HistoryService * history_service,const history::DeletionInfo & deletion_info)530 void ContentSuggestionsService::OnURLsDeleted(
531 history::HistoryService* history_service,
532 const history::DeletionInfo& deletion_info) {
533 // We don't care about expired entries.
534 if (deletion_info.is_from_expiration()) {
535 return;
536 }
537
538 if (deletion_info.IsAllHistory()) {
539 base::RepeatingCallback<bool(const GURL& url)> filter =
540 base::BindRepeating([](const GURL& url) { return true; });
541 ClearHistory(base::Time(), base::Time::Max(), filter);
542 } else {
543 // If a user deletes a single URL, we don't consider this a clear user
544 // intend to clear our data.
545 // TODO(tschumann): Single URL deletions should be handled on a case-by-case
546 // basis. However this depends on the provider's details and thus cannot be
547 // done here. Introduce a OnURLsDeleted() method on the providers to move
548 // this decision further down.
549 if (deletion_info.deleted_rows().size() < 2) {
550 return;
551 }
552 std::set<GURL> deleted_urls;
553 for (const history::URLRow& row : deletion_info.deleted_rows()) {
554 deleted_urls.insert(row.url());
555 }
556 base::RepeatingCallback<bool(const GURL& url)> filter =
557 base::BindRepeating([](const std::set<GURL>& set,
558 const GURL& url) { return set.count(url) != 0; },
559 deleted_urls);
560 // We usually don't have any time-related information (the URLRow objects
561 // usually don't provide a |last_visit()| timestamp. Hence we simply clear
562 // the whole history for the selected URLs.
563 ClearHistory(base::Time(), base::Time::Max(), filter);
564 }
565 }
566
HistoryServiceBeingDeleted(history::HistoryService * history_service)567 void ContentSuggestionsService::HistoryServiceBeingDeleted(
568 history::HistoryService* history_service) {
569 history_service_observer_.RemoveAll();
570 }
571
TryRegisterProviderForCategory(ContentSuggestionsProvider * provider,Category category)572 bool ContentSuggestionsService::TryRegisterProviderForCategory(
573 ContentSuggestionsProvider* provider,
574 Category category) {
575 auto it = providers_by_category_.find(category);
576 if (it != providers_by_category_.end()) {
577 DCHECK_EQ(it->second, provider);
578 return false;
579 }
580
581 auto dismissed_it = dismissed_providers_by_category_.find(category);
582 if (dismissed_it != dismissed_providers_by_category_.end()) {
583 // The initialisation of dismissed categories registers them with |nullptr|
584 // for providers, we need to check for that to see if the provider is
585 // already registered or not.
586 if (!dismissed_it->second) {
587 dismissed_it->second = provider;
588 } else {
589 DCHECK_EQ(dismissed_it->second, provider);
590 }
591 return false;
592 }
593
594 RegisterCategory(category, provider);
595 return true;
596 }
597
RegisterCategory(Category category,ContentSuggestionsProvider * provider)598 void ContentSuggestionsService::RegisterCategory(
599 Category category,
600 ContentSuggestionsProvider* provider) {
601 DCHECK(!base::Contains(providers_by_category_, category));
602 DCHECK(!IsCategoryDismissed(category));
603
604 providers_by_category_[category] = provider;
605 categories_.push_back(category);
606 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) {
607 suggestions_by_category_.insert(
608 std::make_pair(category, std::vector<ContentSuggestion>()));
609 }
610 }
611
UnregisterCategory(Category category,ContentSuggestionsProvider * provider)612 void ContentSuggestionsService::UnregisterCategory(
613 Category category,
614 ContentSuggestionsProvider* provider) {
615 auto providers_it = providers_by_category_.find(category);
616 if (providers_it == providers_by_category_.end()) {
617 DCHECK(IsCategoryDismissed(category));
618 return;
619 }
620
621 DCHECK_EQ(provider, providers_it->second);
622 providers_by_category_.erase(providers_it);
623 categories_.erase(
624 std::find(categories_.begin(), categories_.end(), category));
625 suggestions_by_category_.erase(category);
626 }
627
RemoveSuggestionByID(const ContentSuggestion::ID & suggestion_id)628 bool ContentSuggestionsService::RemoveSuggestionByID(
629 const ContentSuggestion::ID& suggestion_id) {
630 std::vector<ContentSuggestion>* suggestions =
631 &suggestions_by_category_[suggestion_id.category()];
632 auto position =
633 std::find_if(suggestions->begin(), suggestions->end(),
634 [&suggestion_id](const ContentSuggestion& suggestion) {
635 return suggestion_id == suggestion.id();
636 });
637 if (position == suggestions->end()) {
638 return false;
639 }
640 suggestions->erase(position);
641
642 return true;
643 }
644
NotifyCategoryStatusChanged(Category category)645 void ContentSuggestionsService::NotifyCategoryStatusChanged(Category category) {
646 for (Observer& observer : observers_) {
647 observer.OnCategoryStatusChanged(category, GetCategoryStatus(category));
648 }
649 }
650
OnSignInStateChanged(bool has_signed_in)651 void ContentSuggestionsService::OnSignInStateChanged(bool has_signed_in) {
652 // First notify the providers, so they can make the required changes.
653 for (const auto& provider : providers_) {
654 provider->OnSignInStateChanged(has_signed_in);
655 }
656
657 // Finally notify the observers so they refresh only after the backend is
658 // ready.
659 for (Observer& observer : observers_) {
660 observer.OnFullRefreshRequired();
661 }
662 }
663
IsCategoryDismissed(Category category) const664 bool ContentSuggestionsService::IsCategoryDismissed(Category category) const {
665 return base::Contains(dismissed_providers_by_category_, category);
666 }
667
RestoreDismissedCategory(Category category)668 void ContentSuggestionsService::RestoreDismissedCategory(Category category) {
669 auto dismissed_it = dismissed_providers_by_category_.find(category);
670 DCHECK(base::Contains(dismissed_providers_by_category_, category));
671
672 // Keep the reference to the provider and remove it from the dismissed ones,
673 // because the category registration enforces that it's not dismissed.
674 ContentSuggestionsProvider* provider = dismissed_it->second;
675 dismissed_providers_by_category_.erase(dismissed_it);
676
677 if (provider) {
678 RegisterCategory(category, provider);
679 }
680 }
681
RestoreDismissedCategoriesFromPrefs()682 void ContentSuggestionsService::RestoreDismissedCategoriesFromPrefs() {
683 // This must only be called at startup.
684 DCHECK(dismissed_providers_by_category_.empty());
685 DCHECK(providers_by_category_.empty());
686
687 const base::ListValue* list =
688 pref_service_->GetList(prefs::kDismissedCategories);
689 for (const base::Value& entry : *list) {
690 int id = 0;
691 if (!entry.GetAsInteger(&id)) {
692 DLOG(WARNING) << "Invalid category pref value: " << entry;
693 continue;
694 }
695
696 // When the provider is registered, it will be stored in this map.
697 dismissed_providers_by_category_[Category::FromIDValue(id)] = nullptr;
698 }
699 }
700
StoreDismissedCategoriesToPrefs()701 void ContentSuggestionsService::StoreDismissedCategoriesToPrefs() {
702 base::ListValue list;
703 for (const auto& category_provider_pair : dismissed_providers_by_category_) {
704 list.AppendInteger(category_provider_pair.first.id());
705 }
706
707 pref_service_->Set(prefs::kDismissedCategories, list);
708 }
709
DestroyCategoryAndItsProvider(Category category)710 void ContentSuggestionsService::DestroyCategoryAndItsProvider(
711 Category category) {
712 // Destroying articles category is more complex and not implemented.
713 DCHECK_NE(category, Category::FromKnownCategory(KnownCategories::ARTICLES));
714
715 if (providers_by_category_.count(category) != 1) {
716 return;
717 }
718
719 { // Destroy the provider and delete its mentions.
720 ContentSuggestionsProvider* raw_provider = providers_by_category_[category];
721 base::EraseIf(
722 providers_,
723 [&raw_provider](
724 const std::unique_ptr<ContentSuggestionsProvider>& provider) {
725 return provider.get() == raw_provider;
726 });
727 providers_by_category_.erase(category);
728
729 if (dismissed_providers_by_category_.count(category) == 1) {
730 dismissed_providers_by_category_[category] = nullptr;
731 }
732 }
733
734 suggestions_by_category_.erase(category);
735
736 auto it = std::find(categories_.begin(), categories_.end(), category);
737 categories_.erase(it);
738
739 // Notify observers that the category is gone.
740 NotifyCategoryStatusChanged(category);
741 }
742
743 } // namespace ntp_snippets
744