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