content_suggestions_service.cc 26 KB

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