remote_suggestions_provider_impl.cc 62 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657
  1. // Copyright 2015 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/remote/remote_suggestions_provider_impl.h"
  5. #include <algorithm>
  6. #include <iterator>
  7. #include <utility>
  8. #include "base/bind.h"
  9. #include "base/command_line.h"
  10. #include "base/containers/adapters.h"
  11. #include "base/containers/contains.h"
  12. #include "base/containers/cxx20_erase.h"
  13. #include "base/location.h"
  14. #include "base/metrics/field_trial_params.h"
  15. #include "base/metrics/histogram_functions.h"
  16. #include "base/metrics/histogram_macros.h"
  17. #include "base/strings/string_number_conversions.h"
  18. #include "base/strings/utf_string_conversions.h"
  19. #include "base/threading/thread_task_runner_handle.h"
  20. #include "base/time/default_clock.h"
  21. #include "base/time/time.h"
  22. #include "base/timer/timer.h"
  23. #include "base/values.h"
  24. #include "components/image_fetcher/core/image_fetcher.h"
  25. #include "components/ntp_snippets/category_rankers/category_ranker.h"
  26. #include "components/ntp_snippets/features.h"
  27. #include "components/ntp_snippets/pref_names.h"
  28. #include "components/ntp_snippets/remote/remote_suggestions_database.h"
  29. #include "components/ntp_snippets/remote/remote_suggestions_scheduler.h"
  30. #include "components/ntp_snippets/remote/remote_suggestions_status_service_impl.h"
  31. #include "components/ntp_snippets/switches.h"
  32. #include "components/ntp_snippets/time_serialization.h"
  33. #include "components/prefs/pref_registry_simple.h"
  34. #include "components/prefs/pref_service.h"
  35. #include "components/strings/grit/components_strings.h"
  36. #include "components/variations/variations_associated_data.h"
  37. #include "ui/gfx/image/image.h"
  38. namespace ntp_snippets {
  39. namespace {
  40. // Maximal number of suggestions we expect to receive from the server during a
  41. // normal (not fetch-more) fetch. Consider replacing sparse UMA histograms with
  42. // COUNTS() if this number increases beyond 50.
  43. // TODO(vitaliii): Either support requesting a given number of suggestions on
  44. // the server or delete this constant (this will require moving the UMA
  45. // reporting below to content_suggestions_metrics).
  46. const int kMaxNormalFetchSuggestionCount = 20;
  47. // Number of archived suggestions we keep around in memory.
  48. const int kMaxArchivedSuggestionCount = 200;
  49. // Maximal number of dismissed suggestions to exclude when fetching new
  50. // suggestions from the server. This limit exists to decrease data usage.
  51. const int kMaxExcludedDismissedIds = 100;
  52. // Keys for storing CategoryContent info in prefs.
  53. const char kCategoryContentId[] = "id";
  54. const char kCategoryContentTitle[] = "title";
  55. const char kCategoryContentProvidedByServer[] = "provided_by_server";
  56. const char kCategoryContentAllowFetchingMore[] = "allow_fetching_more";
  57. // Variation parameter for ordering new remote categories based on their
  58. // position in the response relative to "Article for you" category.
  59. const char kOrderNewRemoteCategoriesBasedOnArticlesCategory[] =
  60. "order_new_remote_categories_based_on_articles_category";
  61. bool IsOrderingNewRemoteCategoriesBasedOnArticlesCategoryEnabled() {
  62. // TODO(vitaliii): Use GetFieldTrialParamByFeature(As.*)? from
  63. // base/metrics/field_trial_params.h. GetVariationParamByFeature(As.*)? are
  64. // deprecated.
  65. return variations::GetVariationParamByFeatureAsBool(
  66. kArticleSuggestionsFeature,
  67. kOrderNewRemoteCategoriesBasedOnArticlesCategory,
  68. /*default_value=*/false);
  69. }
  70. void AddFetchedCategoriesToRankerBasedOnArticlesCategory(
  71. CategoryRanker* ranker,
  72. const FetchedCategoriesVector& fetched_categories,
  73. Category articles_category) {
  74. DCHECK(IsOrderingNewRemoteCategoriesBasedOnArticlesCategoryEnabled());
  75. // Insert categories which precede "Articles" in the response.
  76. for (const FetchedCategory& fetched_category : fetched_categories) {
  77. if (fetched_category.category == articles_category) {
  78. break;
  79. }
  80. ranker->InsertCategoryBeforeIfNecessary(fetched_category.category,
  81. articles_category);
  82. }
  83. // Insert categories which follow "Articles" in the response. Note that we
  84. // insert them in reversed order, because they are inserted right after
  85. // "Articles", which reverses the order.
  86. for (const FetchedCategory& fetched_category :
  87. base::Reversed(fetched_categories)) {
  88. if (fetched_category.category == articles_category) {
  89. return;
  90. }
  91. ranker->InsertCategoryAfterIfNecessary(fetched_category.category,
  92. articles_category);
  93. }
  94. NOTREACHED() << "Articles category was not found.";
  95. }
  96. // Whether notifications for fetched suggestions are enabled. Note that this
  97. // param does not overwrite other switches which could disable these
  98. // notifications.
  99. const bool kEnableFetchedSuggestionsNotificationsDefault = true;
  100. const char kEnableFetchedSuggestionsNotificationsParamName[] =
  101. "enable_fetched_suggestions_notifications";
  102. bool IsFetchedSuggestionsNotificationsEnabled() {
  103. return base::GetFieldTrialParamByFeatureAsBool(
  104. kNotificationsFeature, kEnableFetchedSuggestionsNotificationsParamName,
  105. kEnableFetchedSuggestionsNotificationsDefault);
  106. }
  107. // Whether notifications for pushed (prepended) suggestions are enabled. Note
  108. // that this param does not overwrite other switches which could disable these
  109. // notifications.
  110. const bool kEnablePushedSuggestionsNotificationsDefault = false;
  111. const char kEnablePushedSuggestionsNotificationsParamName[] =
  112. "enable_pushed_suggestions_notifications";
  113. bool IsPushedSuggestionsNotificationsEnabled() {
  114. return base::GetFieldTrialParamByFeatureAsBool(
  115. kNotificationsFeature, kEnablePushedSuggestionsNotificationsParamName,
  116. kEnablePushedSuggestionsNotificationsDefault);
  117. }
  118. // Whether notification info is overriden for fetched suggestions. Note that
  119. // this param does not overwrite other switches which could disable these
  120. // notifications.
  121. const bool kForceFetchedSuggestionsNotificationsDefault = false;
  122. const char kForceFetchedSuggestionsNotificationsParamName[] =
  123. "force_fetched_suggestions_notifications";
  124. bool ShouldForceFetchedSuggestionsNotifications() {
  125. return base::GetFieldTrialParamByFeatureAsBool(
  126. kNotificationsFeature, kForceFetchedSuggestionsNotificationsParamName,
  127. kForceFetchedSuggestionsNotificationsDefault);
  128. }
  129. // Variation parameter for number of suggestions to request when fetching more.
  130. const char kFetchMoreSuggestionsCountParamName[] =
  131. "fetch_more_suggestions_count";
  132. const int kFetchMoreSuggestionsCountDefault = 25;
  133. int GetFetchMoreSuggestionsCount() {
  134. return base::GetFieldTrialParamByFeatureAsInt(
  135. kArticleSuggestionsFeature, kFetchMoreSuggestionsCountParamName,
  136. kFetchMoreSuggestionsCountDefault);
  137. }
  138. // Variation parameter for the timeout when fetching suggestions with a loading
  139. // indicator. If the fetch takes too long and the timeout is over, the category
  140. // status is forced back to AVAILABLE. If there are existing (possibly stale)
  141. // suggestions, they get notified.
  142. // This timeout is not used for fetching more as the signature of Fetch()
  143. // provides no way to deliver results later after the timeout.
  144. const char kTimeoutForLoadingIndicatorSecondsParamName[] =
  145. "timeout_for_loading_indicator_seconds";
  146. const int kDefaultTimeoutForLoadingIndicatorSeconds = 5;
  147. base::TimeDelta GetTimeoutForLoadingIndicator() {
  148. return base::Seconds(base::GetFieldTrialParamByFeatureAsInt(
  149. ntp_snippets::kArticleSuggestionsFeature,
  150. kTimeoutForLoadingIndicatorSecondsParamName,
  151. kDefaultTimeoutForLoadingIndicatorSeconds));
  152. }
  153. template <typename SuggestionPtrContainer>
  154. std::unique_ptr<std::vector<std::string>> GetSuggestionIDVector(
  155. const SuggestionPtrContainer& suggestions) {
  156. auto result = std::make_unique<std::vector<std::string>>();
  157. for (const auto& suggestion : suggestions) {
  158. result->push_back(suggestion->id());
  159. }
  160. return result;
  161. }
  162. bool HasIntersection(const std::vector<std::string>& a,
  163. const std::set<std::string>& b) {
  164. for (const std::string& item : a) {
  165. if (b.count(item)) {
  166. return true;
  167. }
  168. }
  169. return false;
  170. }
  171. void EraseByPrimaryID(RemoteSuggestion::PtrVector* suggestions,
  172. const std::vector<std::string>& ids) {
  173. std::set<std::string> ids_lookup(ids.begin(), ids.end());
  174. base::EraseIf(
  175. *suggestions,
  176. [&ids_lookup](const std::unique_ptr<RemoteSuggestion>& suggestion) {
  177. return ids_lookup.count(suggestion->id());
  178. });
  179. }
  180. void EraseMatchingSuggestions(
  181. RemoteSuggestion::PtrVector* suggestions,
  182. const RemoteSuggestion::PtrVector& compare_against) {
  183. std::set<std::string> compare_against_ids;
  184. for (const std::unique_ptr<RemoteSuggestion>& suggestion : compare_against) {
  185. const std::vector<std::string>& suggestion_ids = suggestion->GetAllIDs();
  186. compare_against_ids.insert(suggestion_ids.begin(), suggestion_ids.end());
  187. }
  188. base::EraseIf(
  189. *suggestions, [&compare_against_ids](
  190. const std::unique_ptr<RemoteSuggestion>& suggestion) {
  191. return HasIntersection(suggestion->GetAllIDs(), compare_against_ids);
  192. });
  193. }
  194. void RemoveNullPointers(RemoteSuggestion::PtrVector* suggestions) {
  195. base::EraseIf(*suggestions,
  196. [](const std::unique_ptr<RemoteSuggestion>& suggestion) {
  197. return !suggestion;
  198. });
  199. }
  200. void RemoveIncompleteSuggestions(RemoteSuggestion::PtrVector* suggestions) {
  201. if (base::CommandLine::ForCurrentProcess()->HasSwitch(
  202. switches::kAddIncompleteSnippets)) {
  203. return;
  204. }
  205. int num_suggestions = suggestions->size();
  206. // Remove suggestions that do not have all the info we need to display it to
  207. // the user.
  208. base::EraseIf(*suggestions,
  209. [](const std::unique_ptr<RemoteSuggestion>& suggestion) {
  210. return !suggestion->is_complete();
  211. });
  212. int num_suggestions_removed = num_suggestions - suggestions->size();
  213. UMA_HISTOGRAM_BOOLEAN("NewTabPage.Snippets.IncompleteSnippetsAfterFetch",
  214. num_suggestions_removed > 0);
  215. if (num_suggestions_removed > 0) {
  216. base::UmaHistogramSparse("NewTabPage.Snippets.NumIncompleteSnippets",
  217. num_suggestions_removed);
  218. }
  219. }
  220. std::vector<ContentSuggestion> ConvertToContentSuggestions(
  221. Category category,
  222. const RemoteSuggestion::PtrVector& suggestions) {
  223. std::vector<ContentSuggestion> result;
  224. for (const std::unique_ptr<RemoteSuggestion>& suggestion : suggestions) {
  225. // TODO(sfiera): if a suggestion is not going to be displayed, move it
  226. // directly to content.dismissed on fetch. Otherwise, we might prune
  227. // other suggestions to get down to kMaxSuggestionCount, only to hide one of
  228. // the incomplete ones we kept.
  229. if (!suggestion->is_complete()) {
  230. continue;
  231. }
  232. result.emplace_back(suggestion->ToContentSuggestion(category));
  233. }
  234. return result;
  235. }
  236. void CallWithEmptyResults(FetchDoneCallback callback, const Status& status) {
  237. if (callback.is_null()) {
  238. return;
  239. }
  240. std::move(callback).Run(status, std::vector<ContentSuggestion>());
  241. }
  242. void AddDismissedIdsToRequest(const RemoteSuggestion::PtrVector& dismissed,
  243. RequestParams* request_params) {
  244. // The latest ids are added first, because they are more relevant.
  245. for (const std::unique_ptr<RemoteSuggestion>& suggestion :
  246. base::Reversed(dismissed)) {
  247. if (request_params->excluded_ids.size() == kMaxExcludedDismissedIds) {
  248. break;
  249. }
  250. request_params->excluded_ids.insert(suggestion->id());
  251. }
  252. }
  253. void AddDismissedArchivedIdsToRequest(
  254. const base::circular_deque<std::unique_ptr<RemoteSuggestion>>& archived,
  255. RequestParams* request_params) {
  256. // We add all archived, dismissed IDs to the request (the archive is limited
  257. // to kMaxArchivedSuggestionCount suggestions). They don't get persisted,
  258. // which means that the user very recently dismissed them and that they are
  259. // usually not many.
  260. for (auto it = archived.begin(); it != archived.end(); ++it) {
  261. const RemoteSuggestion& suggestion = **it;
  262. if (suggestion.is_dismissed()) {
  263. request_params->excluded_ids.insert(suggestion.id());
  264. }
  265. }
  266. }
  267. } // namespace
  268. RemoteSuggestionsProviderImpl::RemoteSuggestionsProviderImpl(
  269. Observer* observer,
  270. PrefService* pref_service,
  271. const std::string& application_language_code,
  272. CategoryRanker* category_ranker,
  273. RemoteSuggestionsScheduler* scheduler,
  274. std::unique_ptr<RemoteSuggestionsFetcher> suggestions_fetcher,
  275. std::unique_ptr<image_fetcher::ImageFetcher> image_fetcher,
  276. std::unique_ptr<RemoteSuggestionsDatabase> database,
  277. std::unique_ptr<RemoteSuggestionsStatusService> status_service,
  278. std::unique_ptr<base::OneShotTimer> fetch_timeout_timer)
  279. : RemoteSuggestionsProvider(observer),
  280. state_(State::NOT_INITED),
  281. pref_service_(pref_service),
  282. articles_category_(
  283. Category::FromKnownCategory(KnownCategories::ARTICLES)),
  284. application_language_code_(application_language_code),
  285. category_ranker_(category_ranker),
  286. remote_suggestions_scheduler_(scheduler),
  287. suggestions_fetcher_(std::move(suggestions_fetcher)),
  288. database_(std::move(database)),
  289. image_fetcher_(std::move(image_fetcher), pref_service, database_.get()),
  290. status_service_(std::move(status_service)),
  291. clear_history_dependent_state_when_initialized_(false),
  292. clear_cached_suggestions_when_initialized_(false),
  293. clock_(base::DefaultClock::GetInstance()),
  294. fetch_timeout_timer_(std::move(fetch_timeout_timer)),
  295. request_status_(FetchRequestStatus::NONE) {
  296. DCHECK(fetch_timeout_timer_);
  297. RestoreCategoriesFromPrefs();
  298. // The articles category always exists. Add it if we didn't get it from prefs.
  299. // TODO(treib): Rethink this.
  300. category_contents_.insert(
  301. std::make_pair(articles_category_,
  302. CategoryContent(BuildArticleCategoryInfo(absl::nullopt))));
  303. // Tell the observer about all the categories.
  304. for (const auto& entry : category_contents_) {
  305. observer->OnCategoryStatusChanged(this, entry.first, entry.second.status);
  306. }
  307. if (database_->IsErrorState()) {
  308. EnterState(State::ERROR_OCCURRED);
  309. UpdateAllCategoryStatus(CategoryStatus::LOADING_ERROR);
  310. return;
  311. }
  312. database_->SetErrorCallback(base::BindRepeating(
  313. &RemoteSuggestionsProviderImpl::OnDatabaseError, base::Unretained(this)));
  314. // We transition to other states while finalizing the initialization, when the
  315. // database is done loading.
  316. database_load_start_ = base::TimeTicks::Now();
  317. database_->LoadSnippets(
  318. base::BindOnce(&RemoteSuggestionsProviderImpl::OnDatabaseLoaded,
  319. base::Unretained(this)));
  320. }
  321. RemoteSuggestionsProviderImpl::~RemoteSuggestionsProviderImpl() {
  322. }
  323. // static
  324. void RemoteSuggestionsProviderImpl::RegisterProfilePrefs(
  325. PrefRegistrySimple* registry) {
  326. registry->RegisterListPref(prefs::kRemoteSuggestionCategories);
  327. registry->RegisterInt64Pref(prefs::kLastSuccessfulBackgroundFetchTime, 0);
  328. }
  329. void RemoteSuggestionsProviderImpl::ReloadSuggestions() {
  330. if (!remote_suggestions_scheduler_->AcquireQuotaForInteractiveFetch()) {
  331. return;
  332. }
  333. auto callback = base::BindOnce(
  334. [](RemoteSuggestionsScheduler* scheduler, Status status_code) {
  335. scheduler->OnInteractiveFetchFinished(status_code);
  336. },
  337. base::Unretained(remote_suggestions_scheduler_));
  338. if (AreArticlesEmpty()) {
  339. // No reason to use a timeout to hide the loading indicator before the fetch
  340. // definitely fails as we have nothing else to display.
  341. FetchSuggestionsWithLoadingIndicator(
  342. /*interactive_request=*/true, std::move(callback),
  343. /*enable_loading_indication_timeout=*/false);
  344. return;
  345. }
  346. FetchSuggestions(
  347. /*interactive_request=*/true, std::move(callback));
  348. }
  349. void RemoteSuggestionsProviderImpl::RefetchInTheBackground(
  350. FetchStatusCallback callback) {
  351. if (AreArticlesEmpty()) {
  352. // We want to have a loading indicator even for a background fetch as the
  353. // user might open an NTP before the fetch finishes.
  354. // No reason to use a timeout to hide the loading indicator before the fetch
  355. // definitely fails as we have nothing else to display.
  356. FetchSuggestionsWithLoadingIndicator(
  357. /*interactive_request=*/false, std::move(callback),
  358. /*enable_loading_indication_timeout=*/false);
  359. return;
  360. }
  361. FetchSuggestions(/*interactive_request=*/false, std::move(callback));
  362. }
  363. void RemoteSuggestionsProviderImpl::RefetchWhileDisplaying(
  364. FetchStatusCallback callback) {
  365. // We also have existing suggestions in place, so we want to use a timeout
  366. // after which we fall back to the existing suggestions. This way, we do not
  367. // annoy users by too much of waiting on poor connection.
  368. FetchSuggestionsWithLoadingIndicator(
  369. /*interactive_request=*/true, std::move(callback),
  370. /*enable_loading_indication_timeout=*/true);
  371. }
  372. const RemoteSuggestionsFetcher*
  373. RemoteSuggestionsProviderImpl::suggestions_fetcher_for_debugging() const {
  374. return suggestions_fetcher_.get();
  375. }
  376. GURL RemoteSuggestionsProviderImpl::GetUrlWithFavicon(
  377. const ContentSuggestion::ID& suggestion_id) const {
  378. DCHECK(base::Contains(category_contents_, suggestion_id.category()));
  379. const CategoryContent& content =
  380. category_contents_.at(suggestion_id.category());
  381. const RemoteSuggestion* suggestion =
  382. content.FindSuggestion(suggestion_id.id_within_category());
  383. if (!suggestion) {
  384. return GURL();
  385. }
  386. return ContentSuggestion::GetFaviconDomain(suggestion->url());
  387. }
  388. bool RemoteSuggestionsProviderImpl::IsDisabled() const {
  389. return state_ == State::DISABLED;
  390. }
  391. bool RemoteSuggestionsProviderImpl::ready() const {
  392. return state_ == State::READY;
  393. }
  394. void RemoteSuggestionsProviderImpl::FetchSuggestionsWithLoadingIndicator(
  395. bool interactive_request,
  396. FetchStatusCallback callback,
  397. bool enable_loading_indication_timeout) {
  398. if (!AreArticlesAvailable()) {
  399. // If the article section is not AVAILABLE, we cannot safely flip its status
  400. // to AVAILABLE_LOADING and back. Instead, we fallback to the standard
  401. // background refetch.
  402. FetchSuggestions(interactive_request, std::move(callback));
  403. return;
  404. }
  405. NotifyFetchWithLoadingIndicatorStarted();
  406. if (enable_loading_indication_timeout) {
  407. // |fetch_timeout_timer_| makes sure the UI stops waiting after a certain
  408. // time period (the UI also falls back to previous suggestions, if there are
  409. // any).
  410. fetch_timeout_timer_->Start(
  411. FROM_HERE, GetTimeoutForLoadingIndicator(),
  412. base::BindOnce(&RemoteSuggestionsProviderImpl::
  413. NotifyFetchWithLoadingIndicatorFailedOrTimeouted,
  414. base::Unretained(this)));
  415. }
  416. FetchStatusCallback callback_wrapped =
  417. base::BindOnce(&RemoteSuggestionsProviderImpl::
  418. OnFetchSuggestionsWithLoadingIndicatorFinished,
  419. base::Unretained(this), std::move(callback));
  420. FetchSuggestions(interactive_request, std::move(callback_wrapped));
  421. }
  422. void RemoteSuggestionsProviderImpl::
  423. OnFetchSuggestionsWithLoadingIndicatorFinished(FetchStatusCallback callback,
  424. Status status) {
  425. fetch_timeout_timer_->Stop();
  426. // If the fetch succeeds, it already notified new results.
  427. if (!status.IsSuccess()) {
  428. NotifyFetchWithLoadingIndicatorFailedOrTimeouted();
  429. }
  430. if (callback) {
  431. std::move(callback).Run(status);
  432. }
  433. }
  434. void RemoteSuggestionsProviderImpl::FetchSuggestions(
  435. bool interactive_request,
  436. FetchStatusCallback callback) {
  437. if (!ready()) {
  438. if (callback) {
  439. std::move(callback).Run(
  440. Status(StatusCode::TEMPORARY_ERROR,
  441. "RemoteSuggestionsProvider is not ready!"));
  442. }
  443. return;
  444. }
  445. if (request_status_ == FetchRequestStatus::NONE) {
  446. // We cannot rule out concurrent requests although they are rare as the user
  447. // has to trigger ReloadSuggestions() while the scheduler decides for a
  448. // background fetch. Although preventing concurrent fetches would be
  449. // desireable, it's not worth the effort (also see TODO() in
  450. // OnFetchFinished()).
  451. request_status_ = FetchRequestStatus::IN_PROGRESS;
  452. }
  453. // |count_to_fetch| is actually ignored, because the server does not support
  454. // this functionality.
  455. RequestParams params = BuildFetchParams(/*fetched_category=*/absl::nullopt,
  456. /*count_to_fetch=*/10);
  457. params.interactive_request = interactive_request;
  458. suggestions_fetcher_->FetchSnippets(
  459. params, base::BindOnce(&RemoteSuggestionsProviderImpl::OnFetchFinished,
  460. base::Unretained(this), std::move(callback),
  461. interactive_request));
  462. }
  463. void RemoteSuggestionsProviderImpl::Fetch(
  464. const Category& category,
  465. const std::set<std::string>& known_suggestion_ids,
  466. FetchDoneCallback callback) {
  467. if (!ready()) {
  468. CallWithEmptyResults(std::move(callback),
  469. Status(StatusCode::TEMPORARY_ERROR,
  470. "RemoteSuggestionsProvider is not ready!"));
  471. return;
  472. }
  473. if (!remote_suggestions_scheduler_->AcquireQuotaForInteractiveFetch()) {
  474. CallWithEmptyResults(
  475. std::move(callback),
  476. Status(StatusCode::TEMPORARY_ERROR, "Interactive quota exceeded!"));
  477. return;
  478. }
  479. // Make sure after the fetch, the scheduler is informed about the status.
  480. FetchDoneCallback callback_wrapper = base::BindOnce(
  481. [](RemoteSuggestionsScheduler* scheduler, FetchDoneCallback callback,
  482. Status status_code, std::vector<ContentSuggestion> suggestions) {
  483. scheduler->OnInteractiveFetchFinished(status_code);
  484. std::move(callback).Run(status_code, std::move(suggestions));
  485. },
  486. base::Unretained(remote_suggestions_scheduler_), std::move(callback));
  487. RequestParams params = BuildFetchParams(
  488. category, /*count_to_fetch=*/GetFetchMoreSuggestionsCount());
  489. params.excluded_ids.insert(known_suggestion_ids.begin(),
  490. known_suggestion_ids.end());
  491. params.interactive_request = true;
  492. params.exclusive_category = category;
  493. suggestions_fetcher_->FetchSnippets(
  494. params,
  495. base::BindOnce(&RemoteSuggestionsProviderImpl::OnFetchMoreFinished,
  496. base::Unretained(this), std::move(callback_wrapper)));
  497. }
  498. // Builds default fetcher params.
  499. RequestParams RemoteSuggestionsProviderImpl::BuildFetchParams(
  500. absl::optional<Category> fetched_category,
  501. int count_to_fetch) const {
  502. RequestParams result;
  503. result.language_code = application_language_code_;
  504. result.count_to_fetch = count_to_fetch;
  505. // If this is a fetch for a specific category, its dismissed suggestions are
  506. // added first to truncate them less.
  507. if (fetched_category.has_value()) {
  508. DCHECK(category_contents_.count(*fetched_category));
  509. const CategoryContent& content =
  510. category_contents_.find(*fetched_category)->second;
  511. AddDismissedIdsToRequest(content.dismissed, &result);
  512. AddDismissedArchivedIdsToRequest(content.archived, &result);
  513. }
  514. for (const auto& map_entry : category_contents_) {
  515. if (fetched_category.has_value() && map_entry.first == *fetched_category) {
  516. continue;
  517. }
  518. AddDismissedIdsToRequest(map_entry.second.dismissed, &result);
  519. AddDismissedArchivedIdsToRequest(map_entry.second.archived, &result);
  520. }
  521. return result;
  522. }
  523. bool RemoteSuggestionsProviderImpl::AreArticlesEmpty() const {
  524. if (!ready()) {
  525. return false;
  526. }
  527. auto articles_it = category_contents_.find(articles_category_);
  528. DCHECK(articles_it != category_contents_.end());
  529. const CategoryContent& content = articles_it->second;
  530. return content.suggestions.empty();
  531. }
  532. bool RemoteSuggestionsProviderImpl::AreArticlesAvailable() const {
  533. if (!ready()) {
  534. return false;
  535. }
  536. auto articles_it = category_contents_.find(articles_category_);
  537. DCHECK(articles_it != category_contents_.end());
  538. const CategoryContent& content = articles_it->second;
  539. return content.status == CategoryStatus::AVAILABLE;
  540. }
  541. void RemoteSuggestionsProviderImpl::NotifyFetchWithLoadingIndicatorStarted() {
  542. auto articles_it = category_contents_.find(articles_category_);
  543. DCHECK(articles_it != category_contents_.end());
  544. CategoryContent& content = articles_it->second;
  545. DCHECK_EQ(content.status, CategoryStatus::AVAILABLE);
  546. UpdateCategoryStatus(articles_it->first, CategoryStatus::AVAILABLE_LOADING);
  547. }
  548. void RemoteSuggestionsProviderImpl::
  549. NotifyFetchWithLoadingIndicatorFailedOrTimeouted() {
  550. auto articles_it = category_contents_.find(articles_category_);
  551. DCHECK(articles_it != category_contents_.end());
  552. CategoryContent& content = articles_it->second;
  553. DCHECK(content.status == CategoryStatus::AVAILABLE_LOADING ||
  554. content.status == CategoryStatus::CATEGORY_EXPLICITLY_DISABLED);
  555. UpdateCategoryStatus(articles_it->first, CategoryStatus::AVAILABLE);
  556. // TODO(jkrcal): Technically, we have no new suggestions; we should not
  557. // notify. This is a work-around before crbug.com/768410 gets fixed.
  558. NotifyNewSuggestions(articles_category_, content.suggestions);
  559. }
  560. CategoryStatus RemoteSuggestionsProviderImpl::GetCategoryStatus(
  561. Category category) {
  562. auto content_it = category_contents_.find(category);
  563. DCHECK(content_it != category_contents_.end());
  564. return content_it->second.status;
  565. }
  566. CategoryInfo RemoteSuggestionsProviderImpl::GetCategoryInfo(Category category) {
  567. auto content_it = category_contents_.find(category);
  568. DCHECK(content_it != category_contents_.end());
  569. return content_it->second.info;
  570. }
  571. void RemoteSuggestionsProviderImpl::DismissSuggestion(
  572. const ContentSuggestion::ID& suggestion_id) {
  573. if (!ready()) {
  574. return;
  575. }
  576. auto content_it = category_contents_.find(suggestion_id.category());
  577. DCHECK(content_it != category_contents_.end());
  578. CategoryContent* content = &content_it->second;
  579. DismissSuggestionFromCategoryContent(content,
  580. suggestion_id.id_within_category());
  581. }
  582. void RemoteSuggestionsProviderImpl::ClearHistory(
  583. base::Time begin,
  584. base::Time end,
  585. const base::RepeatingCallback<bool(const GURL& url)>& filter) {
  586. // Both time range and the filter are ignored and all suggestions are removed,
  587. // because it is not known which history entries were used for the suggestions
  588. // personalization.
  589. ClearHistoryDependentState();
  590. if (request_status_ == FetchRequestStatus::IN_PROGRESS ||
  591. request_status_ == FetchRequestStatus::IN_PROGRESS_NEEDS_REFETCH) {
  592. request_status_ = FetchRequestStatus::IN_PROGRESS_CANCELED;
  593. }
  594. }
  595. void RemoteSuggestionsProviderImpl::ClearCachedSuggestions() {
  596. ClearCachedSuggestionsImpl();
  597. if (request_status_ == FetchRequestStatus::IN_PROGRESS) {
  598. // Called by external cache-cleared trigger. As this can be caused by
  599. // language change, we need to refetch a potentiall ongoing fetch.
  600. request_status_ = FetchRequestStatus::IN_PROGRESS_NEEDS_REFETCH;
  601. }
  602. }
  603. void RemoteSuggestionsProviderImpl::OnSignInStateChanged(bool has_signed_in) {
  604. // Make sure the status service is registered and we already initialised its
  605. // start state.
  606. if (!initialized()) {
  607. return;
  608. }
  609. status_service_->OnSignInStateChanged(has_signed_in);
  610. }
  611. void RemoteSuggestionsProviderImpl::GetDismissedSuggestionsForDebugging(
  612. Category category,
  613. DismissedSuggestionsCallback callback) {
  614. auto content_it = category_contents_.find(category);
  615. DCHECK(content_it != category_contents_.end());
  616. std::move(callback).Run(
  617. ConvertToContentSuggestions(category, content_it->second.dismissed));
  618. }
  619. void RemoteSuggestionsProviderImpl::ClearDismissedSuggestionsForDebugging(
  620. Category category) {
  621. auto content_it = category_contents_.find(category);
  622. DCHECK(content_it != category_contents_.end());
  623. CategoryContent* content = &content_it->second;
  624. if (!initialized()) {
  625. return;
  626. }
  627. if (!content->dismissed.empty()) {
  628. database_->DeleteSnippets(GetSuggestionIDVector(content->dismissed));
  629. // The image got already deleted when the suggestion was dismissed.
  630. content->dismissed.clear();
  631. }
  632. // Update the archive.
  633. for (const auto& suggestion : content->archived) {
  634. suggestion->set_dismissed(false);
  635. }
  636. }
  637. // static
  638. int RemoteSuggestionsProviderImpl::
  639. GetMaxNormalFetchSuggestionCountForTesting() {
  640. return kMaxNormalFetchSuggestionCount;
  641. }
  642. ////////////////////////////////////////////////////////////////////////////////
  643. // Private methods
  644. GURL RemoteSuggestionsProviderImpl::FindSuggestionImageUrl(
  645. const ContentSuggestion::ID& suggestion_id) const {
  646. DCHECK(base::Contains(category_contents_, suggestion_id.category()));
  647. const CategoryContent& content =
  648. category_contents_.at(suggestion_id.category());
  649. const RemoteSuggestion* suggestion =
  650. content.FindSuggestion(suggestion_id.id_within_category());
  651. if (!suggestion) {
  652. return GURL();
  653. }
  654. return suggestion->salient_image_url();
  655. }
  656. void RemoteSuggestionsProviderImpl::OnDatabaseLoaded(
  657. RemoteSuggestion::PtrVector suggestions) {
  658. if (state_ == State::ERROR_OCCURRED) {
  659. return;
  660. }
  661. DCHECK(state_ == State::NOT_INITED);
  662. DCHECK(base::Contains(category_contents_, articles_category_));
  663. base::TimeDelta database_load_time =
  664. base::TimeTicks::Now() - database_load_start_;
  665. UMA_HISTOGRAM_MEDIUM_TIMES("NewTabPage.Snippets.DatabaseLoadTime",
  666. database_load_time);
  667. RemoteSuggestion::PtrVector to_delete;
  668. for (std::unique_ptr<RemoteSuggestion>& suggestion : suggestions) {
  669. Category suggestion_category =
  670. Category::FromRemoteCategory(suggestion->remote_category_id());
  671. auto content_it = category_contents_.find(suggestion_category);
  672. // We should already know about the category.
  673. if (content_it == category_contents_.end()) {
  674. DLOG(WARNING) << "Loaded a suggestion for unknown category "
  675. << suggestion_category << " from the DB; deleting";
  676. to_delete.emplace_back(std::move(suggestion));
  677. continue;
  678. }
  679. CategoryContent* content = &content_it->second;
  680. if (suggestion->is_dismissed()) {
  681. content->dismissed.emplace_back(std::move(suggestion));
  682. } else {
  683. content->suggestions.emplace_back(std::move(suggestion));
  684. }
  685. }
  686. if (!to_delete.empty()) {
  687. database_->DeleteSnippets(GetSuggestionIDVector(to_delete));
  688. database_->DeleteImages(GetSuggestionIDVector(to_delete));
  689. }
  690. // Sort the suggestions in each category.
  691. for (auto& entry : category_contents_) {
  692. CategoryContent* content = &entry.second;
  693. std::sort(content->suggestions.begin(), content->suggestions.end(),
  694. [](const std::unique_ptr<RemoteSuggestion>& lhs,
  695. const std::unique_ptr<RemoteSuggestion>& rhs) {
  696. if (lhs->rank() != rhs->rank()) {
  697. return lhs->rank() < rhs->rank();
  698. }
  699. // Suggestion created before the rank was introduced have rank
  700. // equal to INT_MAX by default. Sort them by score.
  701. // TODO(vitaliii): Remove this fallback (and its test) in M64.
  702. return lhs->score() > rhs->score();
  703. });
  704. }
  705. // TODO(tschumann): If I move ClearExpiredDismissedSuggestions() to the
  706. // beginning of the function, it essentially does nothing but tests are still
  707. // green. Fix this!
  708. ClearExpiredDismissedSuggestions();
  709. ClearOrphanedImages();
  710. FinishInitialization();
  711. }
  712. void RemoteSuggestionsProviderImpl::OnDatabaseError() {
  713. EnterState(State::ERROR_OCCURRED);
  714. UpdateAllCategoryStatus(CategoryStatus::LOADING_ERROR);
  715. }
  716. void RemoteSuggestionsProviderImpl::OnFetchMoreFinished(
  717. FetchDoneCallback fetching_callback,
  718. Status status,
  719. RemoteSuggestionsFetcher::OptionalFetchedCategories fetched_categories) {
  720. if (!fetched_categories) {
  721. DCHECK(!status.IsSuccess());
  722. CallWithEmptyResults(std::move(fetching_callback), status);
  723. return;
  724. }
  725. if (fetched_categories->size() != 1u) {
  726. LOG(DFATAL) << "Requested one exclusive category but received "
  727. << fetched_categories->size() << " categories.";
  728. CallWithEmptyResults(std::move(fetching_callback),
  729. Status(StatusCode::PERMANENT_ERROR,
  730. "RemoteSuggestionsProvider received more "
  731. "categories than requested."));
  732. return;
  733. }
  734. auto& fetched_category = (*fetched_categories)[0];
  735. Category category = fetched_category.category;
  736. CategoryContent* existing_content =
  737. UpdateCategoryInfo(category, fetched_category.info);
  738. SanitizeReceivedSuggestions(existing_content->dismissed,
  739. &fetched_category.suggestions);
  740. std::vector<ContentSuggestion> result =
  741. ConvertToContentSuggestions(category, fetched_category.suggestions);
  742. // Store the additional suggestions into the archive to be able to fetch
  743. // images and favicons for them. Note that ArchiveSuggestions clears
  744. // |fetched_category.suggestions|.
  745. ArchiveSuggestions(existing_content, &fetched_category.suggestions);
  746. std::move(fetching_callback).Run(Status::Success(), std::move(result));
  747. }
  748. void RemoteSuggestionsProviderImpl::OnFetchFinished(
  749. FetchStatusCallback callback,
  750. bool interactive_request,
  751. Status status,
  752. RemoteSuggestionsFetcher::OptionalFetchedCategories fetched_categories) {
  753. FetchRequestStatus request_status = request_status_;
  754. // TODO(jkrcal): This is potentially incorrect if there is another concurrent
  755. // request in progress; when it finishes we will treat it as a standard
  756. // request even though it may need to be refetched/disregarded. Even though
  757. // the scheduler never triggers two concurrent requests, the user can trigger
  758. // the second request via the UI. If cache/history gets cleared before neither
  759. // of the two finishes, we can get outdated results afterwards. Low chance &
  760. // low risk, feels safe to ignore.
  761. request_status_ = FetchRequestStatus::NONE;
  762. if (!ready()) {
  763. // TODO(tschumann): What happens if this was a user-triggered, interactive
  764. // request? Is the UI waiting indefinitely now?
  765. return;
  766. }
  767. if (request_status == FetchRequestStatus::IN_PROGRESS_NEEDS_REFETCH) {
  768. // Disregard the results and start a fetch again.
  769. FetchSuggestions(interactive_request, std::move(callback));
  770. return;
  771. } else if (request_status == FetchRequestStatus::IN_PROGRESS_CANCELED) {
  772. // Disregard the results.
  773. return;
  774. }
  775. if (!status.IsSuccess()) {
  776. if (callback) {
  777. std::move(callback).Run(status);
  778. }
  779. return;
  780. }
  781. if (fetched_categories) {
  782. for (FetchedCategory& fetched_category : *fetched_categories) {
  783. for (std::unique_ptr<RemoteSuggestion>& suggestion :
  784. fetched_category.suggestions) {
  785. if (ShouldForceFetchedSuggestionsNotifications() &&
  786. IsFetchedSuggestionsNotificationsEnabled()) {
  787. suggestion->set_should_notify(true);
  788. suggestion->set_notification_deadline(clock_->Now() + base::Days(7));
  789. }
  790. if (!IsFetchedSuggestionsNotificationsEnabled()) {
  791. suggestion->set_should_notify(false);
  792. }
  793. }
  794. }
  795. }
  796. // Record the fetch time of a successfull background fetch.
  797. if (!interactive_request) {
  798. pref_service_->SetInt64(prefs::kLastSuccessfulBackgroundFetchTime,
  799. SerializeTime(clock_->Now()));
  800. }
  801. // Mark all categories as not provided by the server in the latest fetch. The
  802. // ones we got will be marked again below.
  803. for (auto& item : category_contents_) {
  804. CategoryContent* content = &item.second;
  805. content->included_in_last_server_response = false;
  806. }
  807. // Clear up expired dismissed suggestions before we use them to filter new
  808. // ones.
  809. ClearExpiredDismissedSuggestions();
  810. // If suggestions were fetched successfully, update our |category_contents_|
  811. // from each category provided by the server.
  812. if (fetched_categories) {
  813. // TODO(treib): Reorder |category_contents_| to match the order we received
  814. // from the server. crbug.com/653816
  815. bool response_includes_article_category = false;
  816. for (FetchedCategory& fetched_category : *fetched_categories) {
  817. if (fetched_category.category == articles_category_) {
  818. base::UmaHistogramSparse(
  819. "NewTabPage.Snippets.NumArticlesFetched",
  820. std::min(fetched_category.suggestions.size(),
  821. static_cast<size_t>(kMaxNormalFetchSuggestionCount)));
  822. response_includes_article_category = true;
  823. }
  824. CategoryContent* content =
  825. UpdateCategoryInfo(fetched_category.category, fetched_category.info);
  826. content->included_in_last_server_response = true;
  827. SanitizeReceivedSuggestions(content->dismissed,
  828. &fetched_category.suggestions);
  829. IntegrateSuggestions(fetched_category.category, content,
  830. std::move(fetched_category.suggestions));
  831. }
  832. // Add new remote categories to the ranker.
  833. if (IsOrderingNewRemoteCategoriesBasedOnArticlesCategoryEnabled() &&
  834. response_includes_article_category) {
  835. AddFetchedCategoriesToRankerBasedOnArticlesCategory(
  836. category_ranker_, *fetched_categories, articles_category_);
  837. } else {
  838. for (const FetchedCategory& fetched_category : *fetched_categories) {
  839. category_ranker_->AppendCategoryIfNecessary(fetched_category.category);
  840. }
  841. }
  842. // Delete categories not present in this fetch.
  843. std::vector<Category> categories_to_delete;
  844. for (auto& item : category_contents_) {
  845. Category category = item.first;
  846. CategoryContent* content = &item.second;
  847. if (!content->included_in_last_server_response &&
  848. category != articles_category_) {
  849. categories_to_delete.push_back(category);
  850. }
  851. }
  852. DeleteCategories(categories_to_delete);
  853. }
  854. // We might have gotten new categories (or updated the titles of existing
  855. // ones), so update the pref.
  856. StoreCategoriesToPrefs();
  857. for (auto& item : category_contents_) {
  858. Category category = item.first;
  859. UpdateCategoryStatus(category, CategoryStatus::AVAILABLE);
  860. // TODO(sfiera): notify only when a category changed above.
  861. NotifyNewSuggestions(category, item.second.suggestions);
  862. // The suggestions may be reused (e.g. when prepending an article), avoid
  863. // trigering notifications for the second time.
  864. for (std::unique_ptr<RemoteSuggestion>& suggestion :
  865. item.second.suggestions) {
  866. suggestion->set_should_notify(false);
  867. }
  868. }
  869. // TODO(sfiera): equivalent metrics for non-articles.
  870. auto content_it = category_contents_.find(articles_category_);
  871. DCHECK(content_it != category_contents_.end());
  872. const CategoryContent& content = content_it->second;
  873. base::UmaHistogramSparse("NewTabPage.Snippets.NumArticles",
  874. content.suggestions.size());
  875. if (content.suggestions.empty() && !content.dismissed.empty()) {
  876. UMA_HISTOGRAM_COUNTS_1M("NewTabPage.Snippets.NumArticlesZeroDueToDiscarded",
  877. content.dismissed.size());
  878. }
  879. if (callback) {
  880. std::move(callback).Run(status);
  881. }
  882. }
  883. void RemoteSuggestionsProviderImpl::ArchiveSuggestions(
  884. CategoryContent* content,
  885. RemoteSuggestion::PtrVector* to_archive) {
  886. // Archive previous suggestions - move them at the beginning of the list.
  887. content->archived.insert(content->archived.begin(),
  888. std::make_move_iterator(to_archive->begin()),
  889. std::make_move_iterator(to_archive->end()));
  890. to_archive->clear();
  891. // If there are more archived suggestions than we want to keep, delete the
  892. // oldest ones by their fetch time (which are always in the back).
  893. if (content->archived.size() > kMaxArchivedSuggestionCount) {
  894. RemoteSuggestion::PtrVector to_delete(
  895. std::make_move_iterator(content->archived.begin() +
  896. kMaxArchivedSuggestionCount),
  897. std::make_move_iterator(content->archived.end()));
  898. content->archived.resize(kMaxArchivedSuggestionCount);
  899. database_->DeleteImages(GetSuggestionIDVector(to_delete));
  900. }
  901. }
  902. void RemoteSuggestionsProviderImpl::SanitizeReceivedSuggestions(
  903. const RemoteSuggestion::PtrVector& dismissed,
  904. RemoteSuggestion::PtrVector* suggestions) {
  905. DCHECK(ready());
  906. EraseMatchingSuggestions(suggestions, dismissed);
  907. RemoveIncompleteSuggestions(suggestions);
  908. }
  909. void RemoteSuggestionsProviderImpl::IntegrateSuggestions(
  910. Category category,
  911. CategoryContent* content,
  912. RemoteSuggestion::PtrVector new_suggestions) {
  913. DCHECK(ready());
  914. // It's entirely possible that the newly fetched suggestions contain articles
  915. // that have been present before.
  916. // We need to make sure to only delete and archive suggestions that don't
  917. // appear with the same ID in the new suggestions (it's fine for additional
  918. // IDs though).
  919. EraseByPrimaryID(&content->suggestions,
  920. *GetSuggestionIDVector(new_suggestions));
  921. // Do not delete the thumbnail images as they are still handy on open NTPs.
  922. database_->DeleteSnippets(GetSuggestionIDVector(content->suggestions));
  923. // Note, that ArchiveSuggestions will clear |content->suggestions|.
  924. ArchiveSuggestions(content, &content->suggestions);
  925. // TODO(vitaliii): Move rank logic into the database. It will set ranks and
  926. // return already sorted suggestions.
  927. for (size_t i = 0; i < new_suggestions.size(); ++i) {
  928. new_suggestions[i]->set_rank(i);
  929. }
  930. database_->SaveSnippets(new_suggestions);
  931. content->suggestions = std::move(new_suggestions);
  932. }
  933. void RemoteSuggestionsProviderImpl::PrependArticleSuggestion(
  934. std::unique_ptr<RemoteSuggestion> remote_suggestion) {
  935. if (!ready()) {
  936. return;
  937. }
  938. if (!IsPushedSuggestionsNotificationsEnabled()) {
  939. remote_suggestion->set_should_notify(false);
  940. }
  941. ClearExpiredDismissedSuggestions();
  942. DCHECK_EQ(articles_category_, Category::FromRemoteCategory(
  943. remote_suggestion->remote_category_id()));
  944. auto content_it = category_contents_.find(articles_category_);
  945. if (content_it == category_contents_.end()) {
  946. return;
  947. }
  948. CategoryContent* content = &content_it->second;
  949. std::vector<std::unique_ptr<RemoteSuggestion>> suggestions;
  950. suggestions.push_back(std::move(remote_suggestion));
  951. // Ignore the pushed suggestion if:
  952. //
  953. // - Incomplete.
  954. // - Has been dismissed.
  955. // - Present in the current suggestions.
  956. // - Possibly shown on older surfaces (i.e. archived).
  957. //
  958. // We do not check the database, because it just persists current suggestions.
  959. RemoveIncompleteSuggestions(&suggestions);
  960. EraseMatchingSuggestions(&suggestions, content->dismissed);
  961. EraseMatchingSuggestions(&suggestions, content->suggestions);
  962. // Check archived suggestions.
  963. base::EraseIf(
  964. suggestions,
  965. [&content](const std::unique_ptr<RemoteSuggestion>& suggestion) {
  966. const std::vector<std::string>& ids = suggestion->GetAllIDs();
  967. for (const auto& archived_suggestion : content->archived) {
  968. if (base::Contains(ids, archived_suggestion->id())) {
  969. return true;
  970. }
  971. }
  972. return false;
  973. });
  974. if (!suggestions.empty()) {
  975. content->suggestions.insert(content->suggestions.begin(),
  976. std::move(suggestions[0]));
  977. for (size_t i = 0; i < content->suggestions.size(); ++i) {
  978. content->suggestions[i]->set_rank(i);
  979. }
  980. if (IsCategoryStatusAvailable(content->status)) {
  981. NotifyNewSuggestions(articles_category_, content->suggestions);
  982. }
  983. // Avoid triggering the pushed suggestion notification for the second time
  984. // (e.g. when another suggestions is pushed).
  985. content->suggestions[0]->set_should_notify(false);
  986. database_->SaveSnippets(content->suggestions);
  987. }
  988. }
  989. void RemoteSuggestionsProviderImpl::
  990. RefreshSuggestionsUponPushToRefreshRequest() {
  991. RefetchInTheBackground({});
  992. }
  993. void RemoteSuggestionsProviderImpl::DismissSuggestionFromCategoryContent(
  994. CategoryContent* content,
  995. const std::string& id_within_category) {
  996. auto id_predicate = [&id_within_category](
  997. const std::unique_ptr<RemoteSuggestion>& suggestion) {
  998. return suggestion->id() == id_within_category;
  999. };
  1000. auto it = std::find_if(content->suggestions.begin(),
  1001. content->suggestions.end(), id_predicate);
  1002. if (it != content->suggestions.end()) {
  1003. (*it)->set_dismissed(true);
  1004. database_->SaveSnippet(**it);
  1005. content->dismissed.push_back(std::move(*it));
  1006. content->suggestions.erase(it);
  1007. } else {
  1008. // Check the archive.
  1009. auto archive_it = std::find_if(content->archived.begin(),
  1010. content->archived.end(), id_predicate);
  1011. if (archive_it != content->archived.end()) {
  1012. (*archive_it)->set_dismissed(true);
  1013. }
  1014. }
  1015. }
  1016. void RemoteSuggestionsProviderImpl::DeleteCategories(
  1017. const std::vector<Category>& categories) {
  1018. for (Category category : categories) {
  1019. auto it = category_contents_.find(category);
  1020. if (it == category_contents_.end()) {
  1021. continue;
  1022. }
  1023. const CategoryContent& content = it->second;
  1024. UpdateCategoryStatus(category, CategoryStatus::NOT_PROVIDED);
  1025. if (!content.suggestions.empty()) {
  1026. database_->DeleteImages(GetSuggestionIDVector(content.suggestions));
  1027. database_->DeleteSnippets(GetSuggestionIDVector(content.suggestions));
  1028. }
  1029. if (!content.dismissed.empty()) {
  1030. database_->DeleteImages(GetSuggestionIDVector(content.dismissed));
  1031. database_->DeleteSnippets(GetSuggestionIDVector(content.dismissed));
  1032. }
  1033. category_contents_.erase(it);
  1034. }
  1035. }
  1036. void RemoteSuggestionsProviderImpl::ClearExpiredDismissedSuggestions() {
  1037. std::vector<Category> categories_to_delete;
  1038. const base::Time now = base::Time::Now();
  1039. for (auto& item : category_contents_) {
  1040. Category category = item.first;
  1041. CategoryContent* content = &item.second;
  1042. RemoteSuggestion::PtrVector to_delete;
  1043. // Move expired dismissed suggestions over into |to_delete|.
  1044. for (std::unique_ptr<RemoteSuggestion>& suggestion : content->dismissed) {
  1045. if (suggestion->expiry_date() <= now) {
  1046. to_delete.emplace_back(std::move(suggestion));
  1047. }
  1048. }
  1049. RemoveNullPointers(&content->dismissed);
  1050. if (!to_delete.empty()) {
  1051. // Delete the images.
  1052. database_->DeleteImages(GetSuggestionIDVector(to_delete));
  1053. // Delete the removed article suggestions from the DB.
  1054. database_->DeleteSnippets(GetSuggestionIDVector(to_delete));
  1055. }
  1056. if (content->suggestions.empty() && content->dismissed.empty() &&
  1057. category != articles_category_ &&
  1058. !content->included_in_last_server_response) {
  1059. categories_to_delete.push_back(category);
  1060. }
  1061. }
  1062. DeleteCategories(categories_to_delete);
  1063. StoreCategoriesToPrefs();
  1064. }
  1065. void RemoteSuggestionsProviderImpl::ClearOrphanedImages() {
  1066. auto alive_suggestions = std::make_unique<std::set<std::string>>();
  1067. for (const auto& entry : category_contents_) {
  1068. const CategoryContent& content = entry.second;
  1069. for (const auto& suggestion_ptr : content.suggestions) {
  1070. alive_suggestions->insert(suggestion_ptr->id());
  1071. }
  1072. for (const auto& suggestion_ptr : content.dismissed) {
  1073. alive_suggestions->insert(suggestion_ptr->id());
  1074. }
  1075. }
  1076. database_->GarbageCollectImages(std::move(alive_suggestions));
  1077. }
  1078. void RemoteSuggestionsProviderImpl::ClearHistoryDependentState() {
  1079. if (!initialized()) {
  1080. clear_history_dependent_state_when_initialized_ = true;
  1081. return;
  1082. }
  1083. NukeAllSuggestions();
  1084. remote_suggestions_scheduler_->OnHistoryCleared();
  1085. }
  1086. void RemoteSuggestionsProviderImpl::ClearCachedSuggestionsImpl() {
  1087. if (!initialized()) {
  1088. clear_cached_suggestions_when_initialized_ = true;
  1089. return;
  1090. }
  1091. NukeAllSuggestions();
  1092. remote_suggestions_scheduler_->OnSuggestionsCleared();
  1093. }
  1094. void RemoteSuggestionsProviderImpl::NukeAllSuggestions() {
  1095. DCHECK(initialized());
  1096. // TODO(tschumann): Should Nuke also cancel outstanding requests? Or should we
  1097. // only block the results of such outstanding requests?
  1098. for (auto& item : category_contents_) {
  1099. Category category = item.first;
  1100. CategoryContent* content = &item.second;
  1101. // TODO(tschumann): We do the unnecessary checks for .empty() in many places
  1102. // before calling database methods. Change the RemoteSuggestionsDatabase to
  1103. // return early for those and remove the many if statements in this file.
  1104. if (!content->suggestions.empty()) {
  1105. database_->DeleteSnippets(GetSuggestionIDVector(content->suggestions));
  1106. database_->DeleteImages(GetSuggestionIDVector(content->suggestions));
  1107. content->suggestions.clear();
  1108. }
  1109. if (!content->archived.empty()) {
  1110. database_->DeleteSnippets(GetSuggestionIDVector(content->archived));
  1111. database_->DeleteImages(GetSuggestionIDVector(content->archived));
  1112. content->archived.clear();
  1113. }
  1114. // Update listeners about the new (empty) state.
  1115. if (IsCategoryStatusAvailable(content->status)) {
  1116. NotifyNewSuggestions(category, content->suggestions);
  1117. }
  1118. // TODO(tschumann): We should not call debug code from production code.
  1119. ClearDismissedSuggestionsForDebugging(category);
  1120. }
  1121. StoreCategoriesToPrefs();
  1122. }
  1123. GURL RemoteSuggestionsProviderImpl::GetImageURLToFetch(
  1124. const ContentSuggestion::ID& suggestion_id) const {
  1125. if (!base::Contains(category_contents_, suggestion_id.category())) {
  1126. return GURL();
  1127. }
  1128. return FindSuggestionImageUrl(suggestion_id);
  1129. }
  1130. void RemoteSuggestionsProviderImpl::FetchSuggestionImage(
  1131. const ContentSuggestion::ID& suggestion_id,
  1132. ImageFetchedCallback callback) {
  1133. GURL image_url = GetImageURLToFetch(suggestion_id);
  1134. if (image_url.is_empty()) {
  1135. // As we don't know the corresponding suggestion anymore, we don't expect to
  1136. // find it in the database (and also can't fetch it remotely). Cut the
  1137. // lookup short and return directly.
  1138. base::ThreadTaskRunnerHandle::Get()->PostTask(
  1139. FROM_HERE, base::BindOnce(std::move(callback), gfx::Image()));
  1140. return;
  1141. }
  1142. image_fetcher_.FetchSuggestionImage(suggestion_id, image_url,
  1143. ImageDataFetchedCallback(),
  1144. std::move(callback));
  1145. }
  1146. void RemoteSuggestionsProviderImpl::FetchSuggestionImageData(
  1147. const ContentSuggestion::ID& suggestion_id,
  1148. ImageDataFetchedCallback callback) {
  1149. GURL image_url = GetImageURLToFetch(suggestion_id);
  1150. if (image_url.is_empty()) {
  1151. // As we don't know the corresponding suggestion anymore, we don't expect to
  1152. // find it in the database (and also can't fetch it remotely). Cut the
  1153. // lookup short and return directly.
  1154. base::ThreadTaskRunnerHandle::Get()->PostTask(
  1155. FROM_HERE, base::BindOnce(std::move(callback), std::string()));
  1156. return;
  1157. }
  1158. image_fetcher_.FetchSuggestionImage(suggestion_id, image_url,
  1159. std::move(callback),
  1160. ntp_snippets::ImageFetchedCallback());
  1161. }
  1162. void RemoteSuggestionsProviderImpl::FinishInitialization() {
  1163. // Note: Initializing the status service will run the callback right away with
  1164. // the current state.
  1165. status_service_->Init(base::BindRepeating(
  1166. &RemoteSuggestionsProviderImpl::OnStatusChanged, base::Unretained(this)));
  1167. // Always notify here even if we got nothing from the database, because we
  1168. // don't know how long the fetch will take or if it will even complete.
  1169. for (const auto& item : category_contents_) {
  1170. Category category = item.first;
  1171. const CategoryContent& content = item.second;
  1172. // Note: We might be in a non-available status here, e.g. DISABLED due to
  1173. // enterprise policy.
  1174. if (IsCategoryStatusAvailable(content.status)) {
  1175. NotifyNewSuggestions(category, content.suggestions);
  1176. }
  1177. }
  1178. }
  1179. void RemoteSuggestionsProviderImpl::OnStatusChanged(
  1180. RemoteSuggestionsStatus old_status,
  1181. RemoteSuggestionsStatus new_status) {
  1182. switch (new_status) {
  1183. case RemoteSuggestionsStatus::ENABLED_AND_SIGNED_IN:
  1184. if (old_status == RemoteSuggestionsStatus::ENABLED_AND_SIGNED_OUT) {
  1185. DCHECK(state_ == State::READY);
  1186. // Clear nonpersonalized suggestions (and notify the scheduler there are
  1187. // no suggestions).
  1188. ClearCachedSuggestionsImpl();
  1189. if (request_status_ == FetchRequestStatus::IN_PROGRESS) {
  1190. request_status_ = FetchRequestStatus::IN_PROGRESS_NEEDS_REFETCH;
  1191. }
  1192. } else {
  1193. EnterState(State::READY);
  1194. }
  1195. break;
  1196. case RemoteSuggestionsStatus::ENABLED_AND_SIGNED_OUT:
  1197. if (old_status == RemoteSuggestionsStatus::ENABLED_AND_SIGNED_IN) {
  1198. DCHECK(state_ == State::READY);
  1199. // Clear personalized suggestions (and notify the scheduler there are
  1200. // no suggestions).
  1201. ClearCachedSuggestionsImpl();
  1202. if (request_status_ == FetchRequestStatus::IN_PROGRESS) {
  1203. request_status_ = FetchRequestStatus::IN_PROGRESS_NEEDS_REFETCH;
  1204. }
  1205. } else {
  1206. EnterState(State::READY);
  1207. }
  1208. break;
  1209. case RemoteSuggestionsStatus::EXPLICITLY_DISABLED:
  1210. EnterState(State::DISABLED);
  1211. break;
  1212. }
  1213. }
  1214. void RemoteSuggestionsProviderImpl::EnterState(State state) {
  1215. if (state == state_) {
  1216. return;
  1217. }
  1218. UMA_HISTOGRAM_ENUMERATION("NewTabPage.Snippets.EnteredState",
  1219. static_cast<int>(state),
  1220. static_cast<int>(State::COUNT));
  1221. switch (state) {
  1222. case State::NOT_INITED:
  1223. // Initial state, it should not be possible to get back there.
  1224. NOTREACHED();
  1225. break;
  1226. case State::READY:
  1227. DCHECK(state_ == State::NOT_INITED || state_ == State::DISABLED);
  1228. DVLOG(1) << "Entering state: READY";
  1229. state_ = State::READY;
  1230. DCHECK(category_contents_.find(articles_category_) !=
  1231. category_contents_.end());
  1232. UpdateAllCategoryStatus(CategoryStatus::AVAILABLE);
  1233. if (clear_cached_suggestions_when_initialized_) {
  1234. ClearCachedSuggestionsImpl();
  1235. clear_cached_suggestions_when_initialized_ = false;
  1236. }
  1237. if (clear_history_dependent_state_when_initialized_) {
  1238. clear_history_dependent_state_when_initialized_ = false;
  1239. ClearHistoryDependentState();
  1240. }
  1241. // This notification may cause the scheduler to ask the provider to do a
  1242. // refetch. We want to do it as the last step, when the state change here
  1243. // in the provider is completed.
  1244. NotifyStateChanged();
  1245. break;
  1246. case State::DISABLED:
  1247. DCHECK(state_ == State::NOT_INITED || state_ == State::READY);
  1248. DVLOG(1) << "Entering state: DISABLED";
  1249. state_ = State::DISABLED;
  1250. // Notify the state change to disable the scheduler. Clearing history /
  1251. // suggestions below tells the scheduler to fetch them again if the
  1252. // scheduler is not disabled. It is disabled; thus the calls are ignored.
  1253. NotifyStateChanged();
  1254. if (clear_history_dependent_state_when_initialized_) {
  1255. clear_history_dependent_state_when_initialized_ = false;
  1256. ClearHistoryDependentState();
  1257. }
  1258. ClearCachedSuggestionsImpl();
  1259. clear_cached_suggestions_when_initialized_ = false;
  1260. UpdateAllCategoryStatus(CategoryStatus::CATEGORY_EXPLICITLY_DISABLED);
  1261. break;
  1262. case State::ERROR_OCCURRED:
  1263. DVLOG(1) << "Entering state: ERROR_OCCURRED";
  1264. state_ = State::ERROR_OCCURRED;
  1265. NotifyStateChanged();
  1266. status_service_.reset();
  1267. break;
  1268. case State::COUNT:
  1269. NOTREACHED();
  1270. break;
  1271. }
  1272. }
  1273. void RemoteSuggestionsProviderImpl::NotifyStateChanged() {
  1274. switch (state_) {
  1275. case State::NOT_INITED:
  1276. // Initial state, not sure yet whether active or not.
  1277. break;
  1278. case State::READY:
  1279. remote_suggestions_scheduler_->OnProviderActivated();
  1280. break;
  1281. case State::DISABLED:
  1282. remote_suggestions_scheduler_->OnProviderDeactivated();
  1283. break;
  1284. case State::ERROR_OCCURRED:
  1285. remote_suggestions_scheduler_->OnProviderDeactivated();
  1286. break;
  1287. case State::COUNT:
  1288. NOTREACHED();
  1289. break;
  1290. }
  1291. }
  1292. void RemoteSuggestionsProviderImpl::NotifyNewSuggestions(
  1293. Category category,
  1294. const RemoteSuggestion::PtrVector& suggestions) {
  1295. DCHECK(category_contents_.find(category) != category_contents_.end());
  1296. DCHECK(IsCategoryStatusAvailable(
  1297. category_contents_.find(category)->second.status));
  1298. std::vector<ContentSuggestion> result =
  1299. ConvertToContentSuggestions(category, suggestions);
  1300. DVLOG(1) << "NotifyNewSuggestions(): " << result.size()
  1301. << " items in category " << category;
  1302. observer()->OnNewSuggestions(this, category, std::move(result));
  1303. }
  1304. void RemoteSuggestionsProviderImpl::UpdateCategoryStatus(
  1305. Category category,
  1306. CategoryStatus status) {
  1307. auto content_it = category_contents_.find(category);
  1308. DCHECK(content_it != category_contents_.end());
  1309. CategoryContent& content = content_it->second;
  1310. if (status == content.status) {
  1311. return;
  1312. }
  1313. DVLOG(1) << "UpdateCategoryStatus(): " << category.id() << ": "
  1314. << static_cast<int>(content.status) << " -> "
  1315. << static_cast<int>(status);
  1316. content.status = status;
  1317. observer()->OnCategoryStatusChanged(this, category, content.status);
  1318. }
  1319. void RemoteSuggestionsProviderImpl::UpdateAllCategoryStatus(
  1320. CategoryStatus status) {
  1321. for (const auto& category : category_contents_) {
  1322. UpdateCategoryStatus(category.first, status);
  1323. }
  1324. }
  1325. namespace {
  1326. template <typename T>
  1327. typename T::const_iterator FindSuggestionInContainer(
  1328. const T& container,
  1329. const std::string& id_within_category) {
  1330. return std::find_if(container.begin(), container.end(),
  1331. [&id_within_category](
  1332. const std::unique_ptr<RemoteSuggestion>& suggestion) {
  1333. return suggestion->id() == id_within_category;
  1334. });
  1335. }
  1336. } // namespace
  1337. const RemoteSuggestion*
  1338. RemoteSuggestionsProviderImpl::CategoryContent::FindSuggestion(
  1339. const std::string& id_within_category) const {
  1340. // Search for the suggestion in current and archived suggestions.
  1341. auto it = FindSuggestionInContainer(suggestions, id_within_category);
  1342. if (it != suggestions.end()) {
  1343. return it->get();
  1344. }
  1345. auto archived_it = FindSuggestionInContainer(archived, id_within_category);
  1346. if (archived_it != archived.end()) {
  1347. return archived_it->get();
  1348. }
  1349. auto dismissed_it = FindSuggestionInContainer(dismissed, id_within_category);
  1350. if (dismissed_it != dismissed.end()) {
  1351. return dismissed_it->get();
  1352. }
  1353. return nullptr;
  1354. }
  1355. RemoteSuggestionsProviderImpl::CategoryContent*
  1356. RemoteSuggestionsProviderImpl::UpdateCategoryInfo(Category category,
  1357. const CategoryInfo& info) {
  1358. auto content_it = category_contents_.find(category);
  1359. if (content_it == category_contents_.end()) {
  1360. content_it = category_contents_
  1361. .insert(std::make_pair(category, CategoryContent(info)))
  1362. .first;
  1363. } else {
  1364. content_it->second.info = info;
  1365. }
  1366. return &content_it->second;
  1367. }
  1368. void RemoteSuggestionsProviderImpl::RestoreCategoriesFromPrefs() {
  1369. // This must only be called at startup, before there are any categories.
  1370. DCHECK(category_contents_.empty());
  1371. const base::Value::List& list =
  1372. pref_service_->GetValueList(prefs::kRemoteSuggestionCategories);
  1373. for (const base::Value& entry : list) {
  1374. if (!entry.is_dict()) {
  1375. DLOG(WARNING) << "Invalid category pref value: " << entry;
  1376. continue;
  1377. }
  1378. const base::Value::Dict& dict = entry.GetDict();
  1379. absl::optional<int> id = dict.FindInt(kCategoryContentId);
  1380. if (!id) {
  1381. DLOG(WARNING) << "Invalid category pref value, missing '"
  1382. << kCategoryContentId << "': " << entry;
  1383. continue;
  1384. }
  1385. const std::string* title = dict.FindString(kCategoryContentTitle);
  1386. if (!title) {
  1387. DLOG(WARNING) << "Invalid category pref value, missing '"
  1388. << kCategoryContentTitle << "': " << entry;
  1389. continue;
  1390. }
  1391. absl::optional<bool> included_in_last_server_response =
  1392. dict.FindBool(kCategoryContentProvidedByServer);
  1393. if (!included_in_last_server_response) {
  1394. DLOG(WARNING) << "Invalid category pref value, missing '"
  1395. << kCategoryContentProvidedByServer << "': " << entry;
  1396. continue;
  1397. }
  1398. // This wasn't always around, so it's okay if it's missing.
  1399. bool allow_fetching_more_results =
  1400. dict.FindBool(kCategoryContentAllowFetchingMore).value_or(false);
  1401. Category category = Category::FromIDValue(*id);
  1402. // The ranker may not persist the order of remote categories.
  1403. category_ranker_->AppendCategoryIfNecessary(category);
  1404. // TODO(tschumann): The following has a bad smell that category
  1405. // serialization / deserialization should not be done inside this
  1406. // class. We should move that into a central place that also knows how to
  1407. // parse data we received from remote backends.
  1408. // We don't want to use the restored title for BuildArticleCategoryInfo to
  1409. // avoid using a title that was calculated for a stale locale.
  1410. CategoryInfo info =
  1411. category == articles_category_
  1412. ? BuildArticleCategoryInfo(absl::nullopt)
  1413. : BuildRemoteCategoryInfo(base::UTF8ToUTF16(*title),
  1414. allow_fetching_more_results);
  1415. CategoryContent* content = UpdateCategoryInfo(category, info);
  1416. content->included_in_last_server_response =
  1417. included_in_last_server_response.value();
  1418. }
  1419. }
  1420. void RemoteSuggestionsProviderImpl::StoreCategoriesToPrefs() {
  1421. // Collect all the CategoryContents.
  1422. std::vector<std::pair<Category, const CategoryContent*>> to_store;
  1423. for (const auto& entry : category_contents_) {
  1424. to_store.emplace_back(entry.first, &entry.second);
  1425. }
  1426. // The ranker may not persist the order, thus, it is stored by the provider.
  1427. std::sort(to_store.begin(), to_store.end(),
  1428. [this](const std::pair<Category, const CategoryContent*>& left,
  1429. const std::pair<Category, const CategoryContent*>& right) {
  1430. return category_ranker_->Compare(left.first, right.first);
  1431. });
  1432. // Convert the relevant info into a base::ListValue for storage.
  1433. base::Value::List list;
  1434. for (const auto& entry : to_store) {
  1435. const Category& category = entry.first;
  1436. const CategoryContent& content = *entry.second;
  1437. base::Value::Dict dict;
  1438. dict.Set(kCategoryContentId, category.id());
  1439. // TODO(tschumann): Persist other properties of the CategoryInfo.
  1440. dict.Set(kCategoryContentTitle, content.info.title());
  1441. dict.Set(kCategoryContentProvidedByServer,
  1442. content.included_in_last_server_response);
  1443. bool has_fetch_action = content.info.additional_action() ==
  1444. ContentSuggestionsAdditionalAction::FETCH;
  1445. dict.Set(kCategoryContentAllowFetchingMore, has_fetch_action);
  1446. list.Append(std::move(dict));
  1447. }
  1448. // Finally, store the result in the pref service.
  1449. pref_service_->Set(prefs::kRemoteSuggestionCategories,
  1450. base::Value(std::move(list)));
  1451. }
  1452. RemoteSuggestionsProviderImpl::CategoryContent::CategoryContent(
  1453. const CategoryInfo& info)
  1454. : info(info) {}
  1455. RemoteSuggestionsProviderImpl::CategoryContent::CategoryContent(
  1456. CategoryContent&&) = default;
  1457. RemoteSuggestionsProviderImpl::CategoryContent::~CategoryContent() = default;
  1458. RemoteSuggestionsProviderImpl::CategoryContent&
  1459. RemoteSuggestionsProviderImpl::CategoryContent::operator=(CategoryContent&&) =
  1460. default;
  1461. } // namespace ntp_snippets