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