Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(51)

Side by Side Diff: components/ntp_snippets/remote/remote_suggestions_provider.cc

Issue 2557363002: [NTP Snippets] Refactor background scheduling for remote suggestions (Closed)
Patch Set: Tim's comments and splitting RemoteSuggestionsProvider and RemoteSuggestionsProviderImpl Created 4 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2015 The Chromium Authors. All rights reserved. 1 // Copyright 2016 The Chromium Authors. All rights reserved.
Marc Treib 2016/12/19 12:59:23 It doesn't really matter, but we don't generally u
jkrcal 2016/12/20 16:39:46 I treat this as a new file (whereas I treat the Im
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "components/ntp_snippets/remote/remote_suggestions_provider.h" 5 #include "components/ntp_snippets/remote/remote_suggestions_provider.h"
6 6
7 #include <algorithm>
8 #include <iterator>
9 #include <utility>
10
11 #include "base/command_line.h"
12 #include "base/feature_list.h"
13 #include "base/location.h"
14 #include "base/memory/ptr_util.h"
15 #include "base/metrics/histogram_macros.h"
16 #include "base/metrics/sparse_histogram.h"
17 #include "base/path_service.h"
18 #include "base/stl_util.h"
19 #include "base/strings/string_number_conversions.h"
20 #include "base/strings/utf_string_conversions.h"
21 #include "base/time/default_clock.h"
22 #include "base/time/time.h"
23 #include "base/values.h"
24 #include "components/data_use_measurement/core/data_use_user_data.h"
25 #include "components/history/core/browser/history_service.h"
26 #include "components/image_fetcher/image_decoder.h"
27 #include "components/image_fetcher/image_fetcher.h"
28 #include "components/ntp_snippets/features.h"
29 #include "components/ntp_snippets/pref_names.h"
30 #include "components/ntp_snippets/remote/remote_suggestions_database.h"
31 #include "components/ntp_snippets/switches.h"
32 #include "components/ntp_snippets/user_classifier.h"
33 #include "components/prefs/pref_registry_simple.h"
34 #include "components/prefs/pref_service.h"
35 #include "components/variations/variations_associated_data.h"
36 #include "grit/components_strings.h"
37 #include "ui/base/l10n/l10n_util.h"
38 #include "ui/gfx/image/image.h"
39
40 namespace ntp_snippets { 7 namespace ntp_snippets {
41 8
42 namespace {
43
44 // Number of snippets requested to the server. Consider replacing sparse UMA
45 // histograms with COUNTS() if this number increases beyond 50.
46 const int kMaxSnippetCount = 10;
47
48 // Number of archived snippets we keep around in memory.
49 const int kMaxArchivedSnippetCount = 200;
50
51 // Default values for fetching intervals, fallback and wifi.
52 const double kDefaultFetchingIntervalRareNtpUser[] = {48.0, 24.0};
53 const double kDefaultFetchingIntervalActiveNtpUser[] = {24.0, 6.0};
54 const double kDefaultFetchingIntervalActiveSuggestionsConsumer[] = {24.0, 6.0};
55
56 // Variation parameters than can override the default fetching intervals.
57 const char* kFetchingIntervalParamNameRareNtpUser[] = {
58 "fetching_interval_hours-fallback-rare_ntp_user",
59 "fetching_interval_hours-wifi-rare_ntp_user"};
60 const char* kFetchingIntervalParamNameActiveNtpUser[] = {
61 "fetching_interval_hours-fallback-active_ntp_user",
62 "fetching_interval_hours-wifi-active_ntp_user"};
63 const char* kFetchingIntervalParamNameActiveSuggestionsConsumer[] = {
64 "fetching_interval_hours-fallback-active_suggestions_consumer",
65 "fetching_interval_hours-wifi-active_suggestions_consumer"};
66
67 // Keys for storing CategoryContent info in prefs.
68 const char kCategoryContentId[] = "id";
69 const char kCategoryContentTitle[] = "title";
70 const char kCategoryContentProvidedByServer[] = "provided_by_server";
71 const char kCategoryContentAllowFetchingMore[] = "allow_fetching_more";
72
73 // TODO(treib): Remove after M57.
74 const char kDeprecatedSnippetHostsPref[] = "ntp_snippets.hosts";
75
76 base::TimeDelta GetFetchingInterval(bool is_wifi,
77 UserClassifier::UserClass user_class) {
78 double value_hours = 0.0;
79
80 const int index = is_wifi ? 1 : 0;
81 const char* param_name = "";
82 switch (user_class) {
83 case UserClassifier::UserClass::RARE_NTP_USER:
84 value_hours = kDefaultFetchingIntervalRareNtpUser[index];
85 param_name = kFetchingIntervalParamNameRareNtpUser[index];
86 break;
87 case UserClassifier::UserClass::ACTIVE_NTP_USER:
88 value_hours = kDefaultFetchingIntervalActiveNtpUser[index];
89 param_name = kFetchingIntervalParamNameActiveNtpUser[index];
90 break;
91 case UserClassifier::UserClass::ACTIVE_SUGGESTIONS_CONSUMER:
92 value_hours = kDefaultFetchingIntervalActiveSuggestionsConsumer[index];
93 param_name = kFetchingIntervalParamNameActiveSuggestionsConsumer[index];
94 break;
95 }
96
97 // The default value can be overridden by a variation parameter.
98 std::string param_value_str = variations::GetVariationParamValueByFeature(
99 ntp_snippets::kArticleSuggestionsFeature, param_name);
100 if (!param_value_str.empty()) {
101 double param_value_hours = 0.0;
102 if (base::StringToDouble(param_value_str, &param_value_hours)) {
103 value_hours = param_value_hours;
104 } else {
105 LOG(WARNING) << "Invalid value for variation parameter " << param_name;
106 }
107 }
108
109 return base::TimeDelta::FromSecondsD(value_hours * 3600.0);
110 }
111
112 std::unique_ptr<std::vector<std::string>> GetSnippetIDVector(
113 const NTPSnippet::PtrVector& snippets) {
114 auto result = base::MakeUnique<std::vector<std::string>>();
115 for (const auto& snippet : snippets) {
116 result->push_back(snippet->id());
117 }
118 return result;
119 }
120
121 bool HasIntersection(const std::vector<std::string>& a,
122 const std::set<std::string>& b) {
123 for (const std::string& item : a) {
124 if (base::ContainsValue(b, item)) {
125 return true;
126 }
127 }
128 return false;
129 }
130
131 void EraseByPrimaryID(NTPSnippet::PtrVector* snippets,
132 const std::vector<std::string>& ids) {
133 std::set<std::string> ids_lookup(ids.begin(), ids.end());
134 snippets->erase(
135 std::remove_if(snippets->begin(), snippets->end(),
136 [&ids_lookup](const std::unique_ptr<NTPSnippet>& snippet) {
137 return base::ContainsValue(ids_lookup, snippet->id());
138 }),
139 snippets->end());
140 }
141
142 void EraseMatchingSnippets(NTPSnippet::PtrVector* snippets,
143 const NTPSnippet::PtrVector& compare_against) {
144 std::set<std::string> compare_against_ids;
145 for (const std::unique_ptr<NTPSnippet>& snippet : compare_against) {
146 const std::vector<std::string>& snippet_ids = snippet->GetAllIDs();
147 compare_against_ids.insert(snippet_ids.begin(), snippet_ids.end());
148 }
149 snippets->erase(
150 std::remove_if(
151 snippets->begin(), snippets->end(),
152 [&compare_against_ids](const std::unique_ptr<NTPSnippet>& snippet) {
153 return HasIntersection(snippet->GetAllIDs(), compare_against_ids);
154 }),
155 snippets->end());
156 }
157
158 void RemoveNullPointers(NTPSnippet::PtrVector* snippets) {
159 snippets->erase(
160 std::remove_if(
161 snippets->begin(), snippets->end(),
162 [](const std::unique_ptr<NTPSnippet>& snippet) { return !snippet; }),
163 snippets->end());
164 }
165
166 void RemoveIncompleteSnippets(NTPSnippet::PtrVector* snippets) {
167 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
168 switches::kAddIncompleteSnippets)) {
169 return;
170 }
171 int num_snippets = snippets->size();
172 // Remove snippets that do not have all the info we need to display it to
173 // the user.
174 snippets->erase(
175 std::remove_if(snippets->begin(), snippets->end(),
176 [](const std::unique_ptr<NTPSnippet>& snippet) {
177 return !snippet->is_complete();
178 }),
179 snippets->end());
180 int num_snippets_removed = num_snippets - snippets->size();
181 UMA_HISTOGRAM_BOOLEAN("NewTabPage.Snippets.IncompleteSnippetsAfterFetch",
182 num_snippets_removed > 0);
183 if (num_snippets_removed > 0) {
184 UMA_HISTOGRAM_SPARSE_SLOWLY("NewTabPage.Snippets.NumIncompleteSnippets",
185 num_snippets_removed);
186 }
187 }
188
189 std::vector<ContentSuggestion> ConvertToContentSuggestions(
190 Category category,
191 const NTPSnippet::PtrVector& snippets) {
192 std::vector<ContentSuggestion> result;
193 for (const std::unique_ptr<NTPSnippet>& snippet : snippets) {
194 // TODO(sfiera): if a snippet is not going to be displayed, move it
195 // directly to content.dismissed on fetch. Otherwise, we might prune
196 // other snippets to get down to kMaxSnippetCount, only to hide one of the
197 // incomplete ones we kept.
198 if (!snippet->is_complete()) {
199 continue;
200 }
201 ContentSuggestion suggestion(category, snippet->id(), snippet->url());
202 suggestion.set_amp_url(snippet->amp_url());
203 suggestion.set_title(base::UTF8ToUTF16(snippet->title()));
204 suggestion.set_snippet_text(base::UTF8ToUTF16(snippet->snippet()));
205 suggestion.set_publish_date(snippet->publish_date());
206 suggestion.set_publisher_name(base::UTF8ToUTF16(snippet->publisher_name()));
207 suggestion.set_score(snippet->score());
208 result.emplace_back(std::move(suggestion));
209 }
210 return result;
211 }
212
213 void CallWithEmptyResults(const FetchDoneCallback& callback,
214 const Status& status) {
215 if (callback.is_null()) {
216 return;
217 }
218 callback.Run(status, std::vector<ContentSuggestion>());
219 }
220
221 } // namespace
222
223 CachedImageFetcher::CachedImageFetcher(
224 std::unique_ptr<image_fetcher::ImageFetcher> image_fetcher,
225 std::unique_ptr<image_fetcher::ImageDecoder> image_decoder,
226 PrefService* pref_service,
227 RemoteSuggestionsDatabase* database)
228 : image_fetcher_(std::move(image_fetcher)),
229 image_decoder_(std::move(image_decoder)),
230 database_(database),
231 thumbnail_requests_throttler_(
232 pref_service,
233 RequestThrottler::RequestType::CONTENT_SUGGESTION_THUMBNAIL) {
234 // |image_fetcher_| can be null in tests.
235 if (image_fetcher_) {
236 image_fetcher_->SetImageFetcherDelegate(this);
237 image_fetcher_->SetDataUseServiceName(
238 data_use_measurement::DataUseUserData::NTP_SNIPPETS);
239 }
240 }
241
242 CachedImageFetcher::~CachedImageFetcher() {}
243
244 void CachedImageFetcher::FetchSuggestionImage(
245 const ContentSuggestion::ID& suggestion_id,
246 const GURL& url,
247 const ImageFetchedCallback& callback) {
248 database_->LoadImage(
249 suggestion_id.id_within_category(),
250 base::Bind(&CachedImageFetcher::OnSnippetImageFetchedFromDatabase,
251 base::Unretained(this), callback, suggestion_id, url));
252 }
253
254 // This function gets only called for caching the image data received from the
255 // network. The actual decoding is done in OnSnippetImageDecodedFromDatabase().
256 void CachedImageFetcher::OnImageDataFetched(
257 const std::string& id_within_category,
258 const std::string& image_data) {
259 if (image_data.empty()) {
260 return;
261 }
262 database_->SaveImage(id_within_category, image_data);
263 }
264
265 void CachedImageFetcher::OnImageDecodingDone(
266 const ImageFetchedCallback& callback,
267 const std::string& id_within_category,
268 const gfx::Image& image) {
269 callback.Run(image);
270 }
271
272 void CachedImageFetcher::OnSnippetImageFetchedFromDatabase(
273 const ImageFetchedCallback& callback,
274 const ContentSuggestion::ID& suggestion_id,
275 const GURL& url,
276 std::string data) { // SnippetImageCallback requires nonconst reference.
277 // |image_decoder_| is null in tests.
278 if (image_decoder_ && !data.empty()) {
279 image_decoder_->DecodeImage(
280 data, base::Bind(
281 &CachedImageFetcher::OnSnippetImageDecodedFromDatabase,
282 base::Unretained(this), callback, suggestion_id, url));
283 return;
284 }
285 // Fetching from the DB failed; start a network fetch.
286 FetchSnippetImageFromNetwork(suggestion_id, url, callback);
287 }
288
289 void CachedImageFetcher::OnSnippetImageDecodedFromDatabase(
290 const ImageFetchedCallback& callback,
291 const ContentSuggestion::ID& suggestion_id,
292 const GURL& url,
293 const gfx::Image& image) {
294 if (!image.IsEmpty()) {
295 callback.Run(image);
296 return;
297 }
298 // If decoding the image failed, delete the DB entry.
299 database_->DeleteImage(suggestion_id.id_within_category());
300 FetchSnippetImageFromNetwork(suggestion_id, url, callback);
301 }
302
303 void CachedImageFetcher::FetchSnippetImageFromNetwork(
304 const ContentSuggestion::ID& suggestion_id,
305 const GURL& url,
306 const ImageFetchedCallback& callback) {
307 if (url.is_empty() ||
308 !thumbnail_requests_throttler_.DemandQuotaForRequest(
309 /*interactive_request=*/true)) {
310 // Return an empty image. Directly, this is never synchronous with the
311 // original FetchSuggestionImage() call - an asynchronous database query has
312 // happened in the meantime.
313 callback.Run(gfx::Image());
314 return;
315 }
316
317 image_fetcher_->StartOrQueueNetworkRequest(
318 suggestion_id.id_within_category(), url,
319 base::Bind(&CachedImageFetcher::OnImageDecodingDone,
320 base::Unretained(this), callback));
321 }
322
323 RemoteSuggestionsProvider::RemoteSuggestionsProvider( 9 RemoteSuggestionsProvider::RemoteSuggestionsProvider(
324 Observer* observer, 10 Observer* observer,
325 CategoryFactory* category_factory, 11 CategoryFactory* category_factory)
326 PrefService* pref_service, 12 : ContentSuggestionsProvider(observer, category_factory) {}
327 const std::string& application_language_code,
328 const UserClassifier* user_classifier,
329 NTPSnippetsScheduler* scheduler,
330 std::unique_ptr<NTPSnippetsFetcher> snippets_fetcher,
331 std::unique_ptr<image_fetcher::ImageFetcher> image_fetcher,
332 std::unique_ptr<image_fetcher::ImageDecoder> image_decoder,
333 std::unique_ptr<RemoteSuggestionsDatabase> database,
334 std::unique_ptr<RemoteSuggestionsStatusService> status_service)
335 : ContentSuggestionsProvider(observer, category_factory),
336 state_(State::NOT_INITED),
337 pref_service_(pref_service),
338 articles_category_(
339 category_factory->FromKnownCategory(KnownCategories::ARTICLES)),
340 application_language_code_(application_language_code),
341 user_classifier_(user_classifier),
342 scheduler_(scheduler),
343 snippets_fetcher_(std::move(snippets_fetcher)),
344 database_(std::move(database)),
345 image_fetcher_(std::move(image_fetcher),
346 std::move(image_decoder),
347 pref_service,
348 database_.get()),
349 status_service_(std::move(status_service)),
350 fetch_when_ready_(false),
351 nuke_when_initialized_(false),
352 clock_(base::MakeUnique<base::DefaultClock>()) {
353 pref_service_->ClearPref(kDeprecatedSnippetHostsPref);
354
355 RestoreCategoriesFromPrefs();
356 // The articles category always exists. Add it if we didn't get it from prefs.
357 // TODO(treib): Rethink this.
358 category_contents_.insert(
359 std::make_pair(articles_category_,
360 CategoryContent(BuildArticleCategoryInfo(base::nullopt))));
361 // Tell the observer about all the categories.
362 for (const auto& entry : category_contents_) {
363 observer->OnCategoryStatusChanged(this, entry.first, entry.second.status);
364 }
365
366 if (database_->IsErrorState()) {
367 EnterState(State::ERROR_OCCURRED);
368 UpdateAllCategoryStatus(CategoryStatus::LOADING_ERROR);
369 return;
370 }
371
372 database_->SetErrorCallback(base::Bind(
373 &RemoteSuggestionsProvider::OnDatabaseError, base::Unretained(this)));
374
375 // We transition to other states while finalizing the initialization, when the
376 // database is done loading.
377 database_load_start_ = base::TimeTicks::Now();
378 database_->LoadSnippets(base::Bind(
379 &RemoteSuggestionsProvider::OnDatabaseLoaded, base::Unretained(this)));
380 }
381 13
382 RemoteSuggestionsProvider::~RemoteSuggestionsProvider() = default; 14 RemoteSuggestionsProvider::~RemoteSuggestionsProvider() = default;
383 15
384 // static
385 void RemoteSuggestionsProvider::RegisterProfilePrefs(
386 PrefRegistrySimple* registry) {
387 // TODO(treib): Remove after M57.
388 registry->RegisterListPref(kDeprecatedSnippetHostsPref);
389 registry->RegisterListPref(prefs::kRemoteSuggestionCategories);
390 registry->RegisterInt64Pref(prefs::kSnippetBackgroundFetchingIntervalWifi, 0);
391 registry->RegisterInt64Pref(prefs::kSnippetBackgroundFetchingIntervalFallback,
392 0);
393 registry->RegisterInt64Pref(prefs::kLastSuccessfulBackgroundFetchTime, 0);
394
395 RemoteSuggestionsStatusService::RegisterProfilePrefs(registry);
396 }
397
398 void RemoteSuggestionsProvider::FetchSnippetsInTheBackground() {
399 FetchSnippets(/*interactive_request=*/false);
400 }
401
402 void RemoteSuggestionsProvider::FetchSnippetsForAllCategories() {
403 // TODO(markusheintz): Investigate whether we can call the Fetch method
404 // instead of the FetchSnippets.
405 FetchSnippets(/*interactive_request=*/true);
406 }
407
408 void RemoteSuggestionsProvider::FetchSnippets(
409 bool interactive_request) {
410 if (!ready()) {
411 fetch_when_ready_ = true;
412 return;
413 }
414
415 MarkEmptyCategoriesAsLoading();
416
417 NTPSnippetsFetcher::Params params = BuildFetchParams();
418 params.interactive_request = interactive_request;
419 snippets_fetcher_->FetchSnippets(
420 params, base::BindOnce(&RemoteSuggestionsProvider::OnFetchFinished,
421 base::Unretained(this), interactive_request));
422 }
423
424 void RemoteSuggestionsProvider::Fetch(
425 const Category& category,
426 const std::set<std::string>& known_suggestion_ids,
427 const FetchDoneCallback& callback) {
428 if (!ready()) {
429 CallWithEmptyResults(callback,
430 Status(StatusCode::TEMPORARY_ERROR,
431 "RemoteSuggestionsProvider is not ready!"));
432 return;
433 }
434 NTPSnippetsFetcher::Params params = BuildFetchParams();
435 params.excluded_ids.insert(known_suggestion_ids.begin(),
436 known_suggestion_ids.end());
437 params.interactive_request = true;
438 params.exclusive_category = category;
439
440 snippets_fetcher_->FetchSnippets(
441 params, base::BindOnce(&RemoteSuggestionsProvider::OnFetchMoreFinished,
442 base::Unretained(this), callback));
443 }
444
445 // Builds default fetcher params.
446 NTPSnippetsFetcher::Params RemoteSuggestionsProvider::BuildFetchParams() const {
447 NTPSnippetsFetcher::Params result;
448 result.language_code = application_language_code_;
449 result.count_to_fetch = kMaxSnippetCount;
450 for (const auto& map_entry : category_contents_) {
451 const CategoryContent& content = map_entry.second;
452 for (const auto& dismissed_snippet : content.dismissed) {
453 result.excluded_ids.insert(dismissed_snippet->id());
454 }
455 }
456 return result;
457 }
458
459 void RemoteSuggestionsProvider::MarkEmptyCategoriesAsLoading() {
460 for (const auto& item : category_contents_) {
461 Category category = item.first;
462 const CategoryContent& content = item.second;
463 if (content.snippets.empty()) {
464 UpdateCategoryStatus(category, CategoryStatus::AVAILABLE_LOADING);
465 }
466 }
467 }
468
469 void RemoteSuggestionsProvider::RescheduleFetching(bool force) {
470 // The scheduler only exists on Android so far, it's null on other platforms.
471 if (!scheduler_) {
472 return;
473 }
474
475 if (ready()) {
476 base::TimeDelta old_interval_wifi = base::TimeDelta::FromInternalValue(
477 pref_service_->GetInt64(prefs::kSnippetBackgroundFetchingIntervalWifi));
478 base::TimeDelta old_interval_fallback =
479 base::TimeDelta::FromInternalValue(pref_service_->GetInt64(
480 prefs::kSnippetBackgroundFetchingIntervalFallback));
481 UserClassifier::UserClass user_class = user_classifier_->GetUserClass();
482 base::TimeDelta interval_wifi =
483 GetFetchingInterval(/*is_wifi=*/true, user_class);
484 base::TimeDelta interval_fallback =
485 GetFetchingInterval(/*is_wifi=*/false, user_class);
486 if (force || interval_wifi != old_interval_wifi ||
487 interval_fallback != old_interval_fallback) {
488 scheduler_->Schedule(interval_wifi, interval_fallback);
489 pref_service_->SetInt64(prefs::kSnippetBackgroundFetchingIntervalWifi,
490 interval_wifi.ToInternalValue());
491 pref_service_->SetInt64(prefs::kSnippetBackgroundFetchingIntervalFallback,
492 interval_fallback.ToInternalValue());
493 }
494 } else {
495 // If we're NOT_INITED, we don't know whether to schedule or unschedule.
496 // If |force| is false, all is well: We'll reschedule on the next state
497 // change anyway. If it's true, then unschedule here, to make sure that the
498 // next reschedule actually happens.
499 if (state_ != State::NOT_INITED || force) {
500 scheduler_->Unschedule();
501 pref_service_->ClearPref(prefs::kSnippetBackgroundFetchingIntervalWifi);
502 pref_service_->ClearPref(
503 prefs::kSnippetBackgroundFetchingIntervalFallback);
504 }
505 }
506 }
507
508 CategoryStatus RemoteSuggestionsProvider::GetCategoryStatus(Category category) {
509 auto content_it = category_contents_.find(category);
510 DCHECK(content_it != category_contents_.end());
511 return content_it->second.status;
512 }
513
514 CategoryInfo RemoteSuggestionsProvider::GetCategoryInfo(Category category) {
515 auto content_it = category_contents_.find(category);
516 DCHECK(content_it != category_contents_.end());
517 return content_it->second.info;
518 }
519
520 void RemoteSuggestionsProvider::DismissSuggestion(
521 const ContentSuggestion::ID& suggestion_id) {
522 if (!ready()) {
523 return;
524 }
525
526 auto content_it = category_contents_.find(suggestion_id.category());
527 DCHECK(content_it != category_contents_.end());
528 CategoryContent* content = &content_it->second;
529 DismissSuggestionFromCategoryContent(content,
530 suggestion_id.id_within_category());
531 }
532
533 void RemoteSuggestionsProvider::ClearHistory(
534 base::Time begin,
535 base::Time end,
536 const base::Callback<bool(const GURL& url)>& filter) {
537 // Both time range and the filter are ignored and all suggestions are removed,
538 // because it is not known which history entries were used for the suggestions
539 // personalization.
540 if (!ready()) {
541 nuke_when_initialized_ = true;
542 } else {
543 NukeAllSnippets();
544 }
545 }
546
547 void RemoteSuggestionsProvider::ClearCachedSuggestions(Category category) {
548 if (!initialized()) {
549 return;
550 }
551
552 auto content_it = category_contents_.find(category);
553 if (content_it == category_contents_.end()) {
554 return;
555 }
556 CategoryContent* content = &content_it->second;
557 if (content->snippets.empty()) {
558 return;
559 }
560
561 database_->DeleteSnippets(GetSnippetIDVector(content->snippets));
562 database_->DeleteImages(GetSnippetIDVector(content->snippets));
563 content->snippets.clear();
564
565 if (IsCategoryStatusAvailable(content->status)) {
566 NotifyNewSuggestions(category, *content);
567 }
568 }
569
570 void RemoteSuggestionsProvider::OnSignInStateChanged() {
571 // Make sure the status service is registered and we already initialised its
572 // start state.
573 if (!initialized()) {
574 return;
575 }
576
577 status_service_->OnSignInStateChanged();
578 }
579
580 void RemoteSuggestionsProvider::GetDismissedSuggestionsForDebugging(
581 Category category,
582 const DismissedSuggestionsCallback& callback) {
583 auto content_it = category_contents_.find(category);
584 DCHECK(content_it != category_contents_.end());
585 callback.Run(
586 ConvertToContentSuggestions(category, content_it->second.dismissed));
587 }
588
589 void RemoteSuggestionsProvider::ClearDismissedSuggestionsForDebugging(
590 Category category) {
591 auto content_it = category_contents_.find(category);
592 DCHECK(content_it != category_contents_.end());
593 CategoryContent* content = &content_it->second;
594
595 if (!initialized()) {
596 return;
597 }
598
599 if (content->dismissed.empty()) {
600 return;
601 }
602
603 database_->DeleteSnippets(GetSnippetIDVector(content->dismissed));
604 // The image got already deleted when the suggestion was dismissed.
605
606 content->dismissed.clear();
607 }
608
609 // static
610 int RemoteSuggestionsProvider::GetMaxSnippetCountForTesting() {
611 return kMaxSnippetCount;
612 }
613
614 ////////////////////////////////////////////////////////////////////////////////
615 // Private methods
616
617 GURL RemoteSuggestionsProvider::FindSnippetImageUrl(
618 const ContentSuggestion::ID& suggestion_id) const {
619 DCHECK(base::ContainsKey(category_contents_, suggestion_id.category()));
620
621 const CategoryContent& content =
622 category_contents_.at(suggestion_id.category());
623 const NTPSnippet* snippet =
624 content.FindSnippet(suggestion_id.id_within_category());
625 if (!snippet) {
626 return GURL();
627 }
628 return snippet->salient_image_url();
629 }
630
631 void RemoteSuggestionsProvider::OnDatabaseLoaded(
632 NTPSnippet::PtrVector snippets) {
633 if (state_ == State::ERROR_OCCURRED) {
634 return;
635 }
636 DCHECK(state_ == State::NOT_INITED);
637 DCHECK(base::ContainsKey(category_contents_, articles_category_));
638
639 base::TimeDelta database_load_time =
640 base::TimeTicks::Now() - database_load_start_;
641 UMA_HISTOGRAM_MEDIUM_TIMES("NewTabPage.Snippets.DatabaseLoadTime",
642 database_load_time);
643
644 NTPSnippet::PtrVector to_delete;
645 for (std::unique_ptr<NTPSnippet>& snippet : snippets) {
646 Category snippet_category =
647 category_factory()->FromRemoteCategory(snippet->remote_category_id());
648 auto content_it = category_contents_.find(snippet_category);
649 // We should already know about the category.
650 if (content_it == category_contents_.end()) {
651 DLOG(WARNING) << "Loaded a suggestion for unknown category "
652 << snippet_category << " from the DB; deleting";
653 to_delete.emplace_back(std::move(snippet));
654 continue;
655 }
656 CategoryContent* content = &content_it->second;
657 if (snippet->is_dismissed()) {
658 content->dismissed.emplace_back(std::move(snippet));
659 } else {
660 content->snippets.emplace_back(std::move(snippet));
661 }
662 }
663 if (!to_delete.empty()) {
664 database_->DeleteSnippets(GetSnippetIDVector(to_delete));
665 database_->DeleteImages(GetSnippetIDVector(to_delete));
666 }
667
668 // Sort the suggestions in each category.
669 // TODO(treib): Persist the actual order in the DB somehow? crbug.com/654409
670 for (auto& entry : category_contents_) {
671 CategoryContent* content = &entry.second;
672 std::sort(content->snippets.begin(), content->snippets.end(),
673 [](const std::unique_ptr<NTPSnippet>& lhs,
674 const std::unique_ptr<NTPSnippet>& rhs) {
675 return lhs->score() > rhs->score();
676 });
677 }
678
679 // TODO(tschumann): If I move ClearExpiredDismissedSnippets() to the beginning
680 // of the function, it essentially does nothing but tests are still green. Fix
681 // this!
682 ClearExpiredDismissedSnippets();
683 ClearOrphanedImages();
684 FinishInitialization();
685 }
686
687 void RemoteSuggestionsProvider::OnDatabaseError() {
688 EnterState(State::ERROR_OCCURRED);
689 UpdateAllCategoryStatus(CategoryStatus::LOADING_ERROR);
690 }
691
692 void RemoteSuggestionsProvider::OnFetchMoreFinished(
693 const FetchDoneCallback& fetching_callback,
694 Status status,
695 NTPSnippetsFetcher::OptionalFetchedCategories fetched_categories) {
696 if (!fetched_categories) {
697 DCHECK(!status.IsSuccess());
698 CallWithEmptyResults(fetching_callback, status);
699 return;
700 }
701 if (fetched_categories->size() != 1u) {
702 LOG(DFATAL) << "Requested one exclusive category but received "
703 << fetched_categories->size() << " categories.";
704 CallWithEmptyResults(fetching_callback,
705 Status(StatusCode::PERMANENT_ERROR,
706 "RemoteSuggestionsProvider received more "
707 "categories than requested."));
708 return;
709 }
710 auto& fetched_category = (*fetched_categories)[0];
711 Category category = fetched_category.category;
712 CategoryContent* existing_content =
713 UpdateCategoryInfo(category, fetched_category.info);
714 SanitizeReceivedSnippets(existing_content->dismissed,
715 &fetched_category.snippets);
716 // We compute the result now before modifying |fetched_category.snippets|.
717 // However, we wait with notifying the caller until the end of the method when
718 // all state is updated.
719 std::vector<ContentSuggestion> result =
720 ConvertToContentSuggestions(category, fetched_category.snippets);
721
722 // Fill up the newly fetched snippets with existing ones, store them, and
723 // notify observers about new data.
724 while (fetched_category.snippets.size() <
725 static_cast<size_t>(kMaxSnippetCount) &&
726 !existing_content->snippets.empty()) {
727 fetched_category.snippets.emplace(
728 fetched_category.snippets.begin(),
729 std::move(existing_content->snippets.back()));
730 existing_content->snippets.pop_back();
731 }
732 std::vector<std::string> to_dismiss =
733 *GetSnippetIDVector(existing_content->snippets);
734 for (const auto& id : to_dismiss) {
735 DismissSuggestionFromCategoryContent(existing_content, id);
736 }
737 DCHECK(existing_content->snippets.empty());
738
739 IntegrateSnippets(existing_content, std::move(fetched_category.snippets));
740
741 // TODO(tschumann): We should properly honor the existing category state,
742 // e.g. to make sure we don't serve results after the sign-out. Revisit this
743 // once the snippets fetcher supports concurrent requests. We can then see if
744 // Nuke should also cancel outstanding requests or we want to check the
745 // status.
746 UpdateCategoryStatus(category, CategoryStatus::AVAILABLE);
747 // Notify callers and observers.
748 fetching_callback.Run(Status::Success(), std::move(result));
749 NotifyNewSuggestions(category, *existing_content);
750 }
751
752 void RemoteSuggestionsProvider::OnFetchFinished(
753 bool interactive_request,
754 Status status,
755 NTPSnippetsFetcher::OptionalFetchedCategories fetched_categories) {
756 if (!ready()) {
757 // TODO(tschumann): What happens if this was a user-triggered, interactive
758 // request? Is the UI waiting indefinitely now?
759 return;
760 }
761
762 // Record the fetch time of a successfull background fetch.
763 if (!interactive_request && status.IsSuccess()) {
764 pref_service_->SetInt64(prefs::kLastSuccessfulBackgroundFetchTime,
765 clock_->Now().ToInternalValue());
766 }
767
768 // Mark all categories as not provided by the server in the latest fetch. The
769 // ones we got will be marked again below.
770 for (auto& item : category_contents_) {
771 CategoryContent* content = &item.second;
772 content->included_in_last_server_response = false;
773 }
774
775 // Clear up expired dismissed snippets before we use them to filter new ones.
776 ClearExpiredDismissedSnippets();
777
778 // If snippets were fetched successfully, update our |category_contents_| from
779 // each category provided by the server.
780 if (fetched_categories) {
781 // TODO(treib): Reorder |category_contents_| to match the order we received
782 // from the server. crbug.com/653816
783 for (NTPSnippetsFetcher::FetchedCategory& fetched_category :
784 *fetched_categories) {
785 // TODO(tschumann): Remove this histogram once we only talk to the content
786 // suggestions cloud backend.
787 if (fetched_category.category == articles_category_) {
788 UMA_HISTOGRAM_SPARSE_SLOWLY(
789 "NewTabPage.Snippets.NumArticlesFetched",
790 std::min(fetched_category.snippets.size(),
791 static_cast<size_t>(kMaxSnippetCount + 1)));
792 }
793 CategoryContent* content =
794 UpdateCategoryInfo(fetched_category.category, fetched_category.info);
795 content->included_in_last_server_response = true;
796 SanitizeReceivedSnippets(content->dismissed, &fetched_category.snippets);
797 IntegrateSnippets(content, std::move(fetched_category.snippets));
798 }
799 }
800
801 // TODO(tschumann): The snippets fetcher needs to signal errors so that we
802 // know why we received no data. If an error occured, none of the following
803 // should take place.
804
805 // We might have gotten new categories (or updated the titles of existing
806 // ones), so update the pref.
807 StoreCategoriesToPrefs();
808
809 for (const auto& item : category_contents_) {
810 Category category = item.first;
811 UpdateCategoryStatus(category, CategoryStatus::AVAILABLE);
812 // TODO(sfiera): notify only when a category changed above.
813 NotifyNewSuggestions(category, item.second);
814 }
815
816 // TODO(sfiera): equivalent metrics for non-articles.
817 auto content_it = category_contents_.find(articles_category_);
818 DCHECK(content_it != category_contents_.end());
819 const CategoryContent& content = content_it->second;
820 UMA_HISTOGRAM_SPARSE_SLOWLY("NewTabPage.Snippets.NumArticles",
821 content.snippets.size());
822 if (content.snippets.empty() && !content.dismissed.empty()) {
823 UMA_HISTOGRAM_COUNTS("NewTabPage.Snippets.NumArticlesZeroDueToDiscarded",
824 content.dismissed.size());
825 }
826
827 // Reschedule after a successful fetch. This resets all currently scheduled
828 // fetches, to make sure the fallback interval triggers only if no wifi fetch
829 // succeeded, and also that we don't do a background fetch immediately after
830 // a user-initiated one.
831 if (fetched_categories) {
832 RescheduleFetching(true);
833 }
834 }
835
836 void RemoteSuggestionsProvider::ArchiveSnippets(
837 CategoryContent* content,
838 NTPSnippet::PtrVector* to_archive) {
839 // Archive previous snippets - move them at the beginning of the list.
840 content->archived.insert(content->archived.begin(),
841 std::make_move_iterator(to_archive->begin()),
842 std::make_move_iterator(to_archive->end()));
843 to_archive->clear();
844
845 // If there are more archived snippets than we want to keep, delete the
846 // oldest ones by their fetch time (which are always in the back).
847 if (content->archived.size() > kMaxArchivedSnippetCount) {
848 NTPSnippet::PtrVector to_delete(
849 std::make_move_iterator(content->archived.begin() +
850 kMaxArchivedSnippetCount),
851 std::make_move_iterator(content->archived.end()));
852 content->archived.resize(kMaxArchivedSnippetCount);
853 database_->DeleteImages(GetSnippetIDVector(to_delete));
854 }
855 }
856
857 void RemoteSuggestionsProvider::SanitizeReceivedSnippets(
858 const NTPSnippet::PtrVector& dismissed,
859 NTPSnippet::PtrVector* snippets) {
860 DCHECK(ready());
861 EraseMatchingSnippets(snippets, dismissed);
862 RemoveIncompleteSnippets(snippets);
863 }
864
865 void RemoteSuggestionsProvider::IntegrateSnippets(
866 CategoryContent* content,
867 NTPSnippet::PtrVector new_snippets) {
868 DCHECK(ready());
869
870 // Do not touch the current set of snippets if the newly fetched one is empty.
871 // TODO(tschumann): This should go. If we get empty results we should update
872 // accordingly and remove the old one (only of course if this was not received
873 // through a fetch-more).
874 if (new_snippets.empty()) {
875 return;
876 }
877
878 // It's entirely possible that the newly fetched snippets contain articles
879 // that have been present before.
880 // We need to make sure to only delete and archive snippets that don't
881 // appear with the same ID in the new suggestions (it's fine for additional
882 // IDs though).
883 EraseByPrimaryID(&content->snippets, *GetSnippetIDVector(new_snippets));
884 // Do not delete the thumbnail images as they are still handy on open NTPs.
885 database_->DeleteSnippets(GetSnippetIDVector(content->snippets));
886 // Note, that ArchiveSnippets will clear |content->snippets|.
887 ArchiveSnippets(content, &content->snippets);
888
889 database_->SaveSnippets(new_snippets);
890
891 content->snippets = std::move(new_snippets);
892 }
893
894 void RemoteSuggestionsProvider::DismissSuggestionFromCategoryContent(
895 CategoryContent* content,
896 const std::string& id_within_category) {
897 auto it = std::find_if(
898 content->snippets.begin(), content->snippets.end(),
899 [&id_within_category](const std::unique_ptr<NTPSnippet>& snippet) {
900 return snippet->id() == id_within_category;
901 });
902 if (it == content->snippets.end()) {
903 return;
904 }
905
906 (*it)->set_dismissed(true);
907
908 database_->SaveSnippet(**it);
909
910 content->dismissed.push_back(std::move(*it));
911 content->snippets.erase(it);
912 }
913
914 void RemoteSuggestionsProvider::ClearExpiredDismissedSnippets() {
915 std::vector<Category> categories_to_erase;
916
917 const base::Time now = base::Time::Now();
918
919 for (auto& item : category_contents_) {
920 Category category = item.first;
921 CategoryContent* content = &item.second;
922
923 NTPSnippet::PtrVector to_delete;
924 // Move expired dismissed snippets over into |to_delete|.
925 for (std::unique_ptr<NTPSnippet>& snippet : content->dismissed) {
926 if (snippet->expiry_date() <= now) {
927 to_delete.emplace_back(std::move(snippet));
928 }
929 }
930 RemoveNullPointers(&content->dismissed);
931
932 // Delete the images.
933 database_->DeleteImages(GetSnippetIDVector(to_delete));
934 // Delete the removed article suggestions from the DB.
935 database_->DeleteSnippets(GetSnippetIDVector(to_delete));
936
937 if (content->snippets.empty() && content->dismissed.empty() &&
938 category != articles_category_ &&
939 !content->included_in_last_server_response) {
940 categories_to_erase.push_back(category);
941 }
942 }
943
944 for (Category category : categories_to_erase) {
945 UpdateCategoryStatus(category, CategoryStatus::NOT_PROVIDED);
946 category_contents_.erase(category);
947 }
948
949 StoreCategoriesToPrefs();
950 }
951
952 void RemoteSuggestionsProvider::ClearOrphanedImages() {
953 auto alive_snippets = base::MakeUnique<std::set<std::string>>();
954 for (const auto& entry : category_contents_) {
955 const CategoryContent& content = entry.second;
956 for (const auto& snippet_ptr : content.snippets) {
957 alive_snippets->insert(snippet_ptr->id());
958 }
959 for (const auto& snippet_ptr : content.dismissed) {
960 alive_snippets->insert(snippet_ptr->id());
961 }
962 }
963 database_->GarbageCollectImages(std::move(alive_snippets));
964 }
965
966 void RemoteSuggestionsProvider::NukeAllSnippets() {
967 std::vector<Category> categories_to_erase;
968
969 // Empty the ARTICLES category and remove all others, since they may or may
970 // not be personalized.
971 for (const auto& item : category_contents_) {
972 Category category = item.first;
973
974 ClearCachedSuggestions(category);
975 ClearDismissedSuggestionsForDebugging(category);
976
977 UpdateCategoryStatus(category, CategoryStatus::NOT_PROVIDED);
978
979 // Remove the category entirely; it may or may not reappear.
980 if (category != articles_category_) {
981 categories_to_erase.push_back(category);
982 }
983 }
984
985 for (Category category : categories_to_erase) {
986 category_contents_.erase(category);
987 }
988
989 StoreCategoriesToPrefs();
990 }
991
992 void RemoteSuggestionsProvider::FetchSuggestionImage(
993 const ContentSuggestion::ID& suggestion_id,
994 const ImageFetchedCallback& callback) {
995 if (!base::ContainsKey(category_contents_, suggestion_id.category())) {
996 base::ThreadTaskRunnerHandle::Get()->PostTask(
997 FROM_HERE, base::Bind(callback, gfx::Image()));
998 return;
999 }
1000 GURL image_url = FindSnippetImageUrl(suggestion_id);
1001 if (image_url.is_empty()) {
1002 // As we don't know the corresponding snippet anymore, we don't expect to
1003 // find it in the database (and also can't fetch it remotely). Cut the
1004 // lookup short and return directly.
1005 base::ThreadTaskRunnerHandle::Get()->PostTask(
1006 FROM_HERE, base::Bind(callback, gfx::Image()));
1007 return;
1008 }
1009 image_fetcher_.FetchSuggestionImage(suggestion_id, image_url, callback);
1010 }
1011
1012 void RemoteSuggestionsProvider::EnterStateReady() {
1013 if (nuke_when_initialized_) {
1014 NukeAllSnippets();
1015 nuke_when_initialized_ = false;
1016 }
1017
1018 auto article_category_it = category_contents_.find(articles_category_);
1019 DCHECK(article_category_it != category_contents_.end());
1020 if (article_category_it->second.snippets.empty() || fetch_when_ready_) {
1021 // TODO(jkrcal): Fetching snippets automatically upon creation of this
1022 // lazily created service can cause troubles, e.g. in unit tests where
1023 // network I/O is not allowed.
1024 // Either add a DCHECK here that we actually are allowed to do network I/O
1025 // or change the logic so that some explicit call is always needed for the
1026 // network request.
1027 FetchSnippets(/*interactive_request=*/false);
1028 fetch_when_ready_ = false;
1029 }
1030
1031 for (const auto& item : category_contents_) {
1032 Category category = item.first;
1033 const CategoryContent& content = item.second;
1034 // FetchSnippets has set the status to |AVAILABLE_LOADING| if relevant,
1035 // otherwise we transition to |AVAILABLE| here.
1036 if (content.status != CategoryStatus::AVAILABLE_LOADING) {
1037 UpdateCategoryStatus(category, CategoryStatus::AVAILABLE);
1038 }
1039 }
1040 }
1041
1042 void RemoteSuggestionsProvider::EnterStateDisabled() {
1043 NukeAllSnippets();
1044 }
1045
1046 void RemoteSuggestionsProvider::EnterStateError() {
1047 status_service_.reset();
1048 }
1049
1050 void RemoteSuggestionsProvider::FinishInitialization() {
1051 if (nuke_when_initialized_) {
1052 // We nuke here in addition to EnterStateReady, so that it happens even if
1053 // we enter the DISABLED state below.
1054 NukeAllSnippets();
1055 nuke_when_initialized_ = false;
1056 }
1057
1058 // Note: Initializing the status service will run the callback right away with
1059 // the current state.
1060 status_service_->Init(base::Bind(&RemoteSuggestionsProvider::OnStatusChanged,
1061 base::Unretained(this)));
1062
1063 // Always notify here even if we got nothing from the database, because we
1064 // don't know how long the fetch will take or if it will even complete.
1065 for (const auto& item : category_contents_) {
1066 Category category = item.first;
1067 const CategoryContent& content = item.second;
1068 // Note: We might be in a non-available status here, e.g. DISABLED due to
1069 // enterprise policy.
1070 if (IsCategoryStatusAvailable(content.status)) {
1071 NotifyNewSuggestions(category, content);
1072 }
1073 }
1074 }
1075
1076 void RemoteSuggestionsProvider::OnStatusChanged(
1077 RemoteSuggestionsStatus old_status,
1078 RemoteSuggestionsStatus new_status) {
1079 switch (new_status) {
1080 case RemoteSuggestionsStatus::ENABLED_AND_SIGNED_IN:
1081 if (old_status == RemoteSuggestionsStatus::ENABLED_AND_SIGNED_OUT) {
1082 DCHECK(state_ == State::READY);
1083 // Clear nonpersonalized suggestions.
1084 NukeAllSnippets();
1085 // Fetch personalized ones.
1086 FetchSnippets(/*interactive_request=*/true);
1087 } else {
1088 // Do not change the status. That will be done in EnterStateReady().
1089 EnterState(State::READY);
1090 }
1091 break;
1092
1093 case RemoteSuggestionsStatus::ENABLED_AND_SIGNED_OUT:
1094 if (old_status == RemoteSuggestionsStatus::ENABLED_AND_SIGNED_IN) {
1095 DCHECK(state_ == State::READY);
1096 // Clear personalized suggestions.
1097 NukeAllSnippets();
1098 // Fetch nonpersonalized ones.
1099 FetchSnippets(/*interactive_request=*/true);
1100 } else {
1101 // Do not change the status. That will be done in EnterStateReady().
1102 EnterState(State::READY);
1103 }
1104 break;
1105
1106 case RemoteSuggestionsStatus::EXPLICITLY_DISABLED:
1107 EnterState(State::DISABLED);
1108 UpdateAllCategoryStatus(CategoryStatus::CATEGORY_EXPLICITLY_DISABLED);
1109 break;
1110
1111 case RemoteSuggestionsStatus::SIGNED_OUT_AND_DISABLED:
1112 EnterState(State::DISABLED);
1113 UpdateAllCategoryStatus(CategoryStatus::SIGNED_OUT);
1114 break;
1115 }
1116 }
1117
1118 void RemoteSuggestionsProvider::EnterState(State state) {
1119 if (state == state_) {
1120 return;
1121 }
1122
1123 UMA_HISTOGRAM_ENUMERATION("NewTabPage.Snippets.EnteredState",
1124 static_cast<int>(state),
1125 static_cast<int>(State::COUNT));
1126
1127 switch (state) {
1128 case State::NOT_INITED:
1129 // Initial state, it should not be possible to get back there.
1130 NOTREACHED();
1131 break;
1132
1133 case State::READY:
1134 DCHECK(state_ == State::NOT_INITED || state_ == State::DISABLED);
1135
1136 DVLOG(1) << "Entering state: READY";
1137 state_ = State::READY;
1138 EnterStateReady();
1139 break;
1140
1141 case State::DISABLED:
1142 DCHECK(state_ == State::NOT_INITED || state_ == State::READY);
1143
1144 DVLOG(1) << "Entering state: DISABLED";
1145 state_ = State::DISABLED;
1146 EnterStateDisabled();
1147 break;
1148
1149 case State::ERROR_OCCURRED:
1150 DVLOG(1) << "Entering state: ERROR_OCCURRED";
1151 state_ = State::ERROR_OCCURRED;
1152 EnterStateError();
1153 break;
1154
1155 case State::COUNT:
1156 NOTREACHED();
1157 break;
1158 }
1159
1160 // Schedule or un-schedule background fetching after each state change.
1161 RescheduleFetching(false);
1162 }
1163
1164 void RemoteSuggestionsProvider::NotifyNewSuggestions(
1165 Category category,
1166 const CategoryContent& content) {
1167 DCHECK(IsCategoryStatusAvailable(content.status));
1168
1169 std::vector<ContentSuggestion> result =
1170 ConvertToContentSuggestions(category, content.snippets);
1171
1172 DVLOG(1) << "NotifyNewSuggestions(): " << result.size()
1173 << " items in category " << category;
1174 observer()->OnNewSuggestions(this, category, std::move(result));
1175 }
1176
1177 void RemoteSuggestionsProvider::UpdateCategoryStatus(Category category,
1178 CategoryStatus status) {
1179 auto content_it = category_contents_.find(category);
1180 DCHECK(content_it != category_contents_.end());
1181 CategoryContent& content = content_it->second;
1182
1183 if (status == content.status) {
1184 return;
1185 }
1186
1187 DVLOG(1) << "UpdateCategoryStatus(): " << category.id() << ": "
1188 << static_cast<int>(content.status) << " -> "
1189 << static_cast<int>(status);
1190 content.status = status;
1191 observer()->OnCategoryStatusChanged(this, category, content.status);
1192 }
1193
1194 void RemoteSuggestionsProvider::UpdateAllCategoryStatus(CategoryStatus status) {
1195 for (const auto& category : category_contents_) {
1196 UpdateCategoryStatus(category.first, status);
1197 }
1198 }
1199
1200 namespace {
1201
1202 template <typename T>
1203 typename T::const_iterator FindSnippetInContainer(
1204 const T& container,
1205 const std::string& id_within_category) {
1206 return std::find_if(
1207 container.begin(), container.end(),
1208 [&id_within_category](const std::unique_ptr<NTPSnippet>& snippet) {
1209 return snippet->id() == id_within_category;
1210 });
1211 }
1212
1213 } // namespace
1214
1215 const NTPSnippet* RemoteSuggestionsProvider::CategoryContent::FindSnippet(
1216 const std::string& id_within_category) const {
1217 // Search for the snippet in current and archived snippets.
1218 auto it = FindSnippetInContainer(snippets, id_within_category);
1219 if (it != snippets.end()) {
1220 return it->get();
1221 }
1222 auto archived_it = FindSnippetInContainer(archived, id_within_category);
1223 if (archived_it != archived.end()) {
1224 return archived_it->get();
1225 }
1226 auto dismissed_it = FindSnippetInContainer(dismissed, id_within_category);
1227 if (dismissed_it != dismissed.end()) {
1228 return dismissed_it->get();
1229 }
1230 return nullptr;
1231 }
1232
1233 RemoteSuggestionsProvider::CategoryContent*
1234 RemoteSuggestionsProvider::UpdateCategoryInfo(Category category,
1235 const CategoryInfo& info) {
1236 auto content_it = category_contents_.find(category);
1237 if (content_it == category_contents_.end()) {
1238 content_it = category_contents_
1239 .insert(std::make_pair(category, CategoryContent(info)))
1240 .first;
1241 } else {
1242 content_it->second.info = info;
1243 }
1244 return &content_it->second;
1245 }
1246
1247 void RemoteSuggestionsProvider::RestoreCategoriesFromPrefs() {
1248 // This must only be called at startup, before there are any categories.
1249 DCHECK(category_contents_.empty());
1250
1251 const base::ListValue* list =
1252 pref_service_->GetList(prefs::kRemoteSuggestionCategories);
1253 for (const std::unique_ptr<base::Value>& entry : *list) {
1254 const base::DictionaryValue* dict = nullptr;
1255 if (!entry->GetAsDictionary(&dict)) {
1256 DLOG(WARNING) << "Invalid category pref value: " << *entry;
1257 continue;
1258 }
1259 int id = 0;
1260 if (!dict->GetInteger(kCategoryContentId, &id)) {
1261 DLOG(WARNING) << "Invalid category pref value, missing '"
1262 << kCategoryContentId << "': " << *entry;
1263 continue;
1264 }
1265 base::string16 title;
1266 if (!dict->GetString(kCategoryContentTitle, &title)) {
1267 DLOG(WARNING) << "Invalid category pref value, missing '"
1268 << kCategoryContentTitle << "': " << *entry;
1269 continue;
1270 }
1271 bool included_in_last_server_response = false;
1272 if (!dict->GetBoolean(kCategoryContentProvidedByServer,
1273 &included_in_last_server_response)) {
1274 DLOG(WARNING) << "Invalid category pref value, missing '"
1275 << kCategoryContentProvidedByServer << "': " << *entry;
1276 continue;
1277 }
1278 bool allow_fetching_more_results = false;
1279 // This wasn't always around, so it's okay if it's missing.
1280 dict->GetBoolean(kCategoryContentAllowFetchingMore,
1281 &allow_fetching_more_results);
1282
1283 Category category = category_factory()->FromIDValue(id);
1284 // TODO(tschumann): The following has a bad smell that category
1285 // serialization / deserialization should not be done inside this
1286 // class. We should move that into a central place that also knows how to
1287 // parse data we received from remote backends.
1288 CategoryInfo info =
1289 category == articles_category_
1290 ? BuildArticleCategoryInfo(title)
1291 : BuildRemoteCategoryInfo(title, allow_fetching_more_results);
1292 CategoryContent* content = UpdateCategoryInfo(category, info);
1293 content->included_in_last_server_response =
1294 included_in_last_server_response;
1295 }
1296 }
1297
1298 void RemoteSuggestionsProvider::StoreCategoriesToPrefs() {
1299 // Collect all the CategoryContents.
1300 std::vector<std::pair<Category, const CategoryContent*>> to_store;
1301 for (const auto& entry : category_contents_) {
1302 to_store.emplace_back(entry.first, &entry.second);
1303 }
1304 // Sort them into the proper category order.
1305 std::sort(to_store.begin(), to_store.end(),
1306 [this](const std::pair<Category, const CategoryContent*>& left,
1307 const std::pair<Category, const CategoryContent*>& right) {
1308 return category_factory()->CompareCategories(left.first,
1309 right.first);
1310 });
1311 // Convert the relevant info into a base::ListValue for storage.
1312 base::ListValue list;
1313 for (const auto& entry : to_store) {
1314 const Category& category = entry.first;
1315 const CategoryContent& content = *entry.second;
1316 auto dict = base::MakeUnique<base::DictionaryValue>();
1317 dict->SetInteger(kCategoryContentId, category.id());
1318 // TODO(tschumann): Persist other properties of the CategoryInfo.
1319 dict->SetString(kCategoryContentTitle, content.info.title());
1320 dict->SetBoolean(kCategoryContentProvidedByServer,
1321 content.included_in_last_server_response);
1322 dict->SetBoolean(kCategoryContentAllowFetchingMore,
1323 content.info.has_more_action());
1324 list.Append(std::move(dict));
1325 }
1326 // Finally, store the result in the pref service.
1327 pref_service_->Set(prefs::kRemoteSuggestionCategories, list);
1328 }
1329
1330 RemoteSuggestionsProvider::CategoryContent::CategoryContent(
1331 const CategoryInfo& info)
1332 : info(info) {}
1333
1334 RemoteSuggestionsProvider::CategoryContent::CategoryContent(CategoryContent&&) =
1335 default;
1336
1337 RemoteSuggestionsProvider::CategoryContent::~CategoryContent() = default;
1338
1339 RemoteSuggestionsProvider::CategoryContent&
1340 RemoteSuggestionsProvider::CategoryContent::operator=(CategoryContent&&) =
1341 default;
1342
1343 } // namespace ntp_snippets 16 } // namespace ntp_snippets
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698