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