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