blob: 58dd4333c6ad17dad2244882a60ef242d8394295 [file] [log] [blame]
pke6dbb90af2016-07-08 14:00:461// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "components/ntp_snippets/content_suggestions_service.h"
6
7#include <algorithm>
8#include <iterator>
vitaliii45941152016-09-05 08:58:139#include <set>
jkrcale13510e2016-09-08 17:56:2010#include <utility>
pke6dbb90af2016-07-08 14:00:4611
12#include "base/bind.h"
pke1da90602016-08-05 14:20:2713#include "base/location.h"
pke6dbb90af2016-07-08 14:00:4614#include "base/strings/string_number_conversions.h"
pke1da90602016-08-05 14:20:2715#include "base/threading/thread_task_runner_handle.h"
dgn52914722016-10-18 10:28:4216#include "base/values.h"
17#include "components/ntp_snippets/pref_names.h"
18#include "components/prefs/pref_registry_simple.h"
19#include "components/prefs/pref_service.h"
pke6dbb90af2016-07-08 14:00:4620#include "ui/gfx/image/image.h"
21
22namespace ntp_snippets {
23
vitaliii45941152016-09-05 08:58:1324ContentSuggestionsService::ContentSuggestionsService(
25 State state,
jkrcale13510e2016-09-08 17:56:2026 history::HistoryService* history_service,
27 PrefService* pref_service)
28 : state_(state),
29 history_service_observer_(this),
treibfd0db3c2016-09-20 12:22:1530 ntp_snippets_service_(nullptr),
dgn52914722016-10-18 10:28:4231 pref_service_(pref_service),
jkrcale13510e2016-09-08 17:56:2032 user_classifier_(pref_service) {
vitaliii45941152016-09-05 08:58:1333 // Can be null in tests.
34 if (history_service)
35 history_service_observer_.Add(history_service);
dgn52914722016-10-18 10:28:4236
37 RestoreDismissedCategoriesFromPrefs();
vitaliii45941152016-09-05 08:58:1338}
pke6dbb90af2016-07-08 14:00:4639
treib62e819e2016-09-27 11:47:3440ContentSuggestionsService::~ContentSuggestionsService() = default;
pke6dbb90af2016-07-08 14:00:4641
42void ContentSuggestionsService::Shutdown() {
pke5728f082016-08-03 17:27:3543 ntp_snippets_service_ = nullptr;
pke5728f082016-08-03 17:27:3544 suggestions_by_category_.clear();
45 providers_by_category_.clear();
46 categories_.clear();
47 providers_.clear();
pke6dbb90af2016-07-08 14:00:4648 state_ = State::DISABLED;
49 FOR_EACH_OBSERVER(Observer, observers_, ContentSuggestionsServiceShutdown());
50}
51
dgn52914722016-10-18 10:28:4252// static
53void ContentSuggestionsService::RegisterProfilePrefs(
54 PrefRegistrySimple* registry) {
55 registry->RegisterListPref(prefs::kDismissedCategories);
56}
57
pke9c5095ac2016-08-01 13:53:1258CategoryStatus ContentSuggestionsService::GetCategoryStatus(
59 Category category) const {
pke6dbb90af2016-07-08 14:00:4660 if (state_ == State::DISABLED) {
pke9c5095ac2016-08-01 13:53:1261 return CategoryStatus::ALL_SUGGESTIONS_EXPLICITLY_DISABLED;
pke6dbb90af2016-07-08 14:00:4662 }
63
pke4d3a4d62016-08-02 09:06:2164 auto iterator = providers_by_category_.find(category);
65 if (iterator == providers_by_category_.end())
pke9c5095ac2016-08-01 13:53:1266 return CategoryStatus::NOT_PROVIDED;
pke6dbb90af2016-07-08 14:00:4667
68 return iterator->second->GetCategoryStatus(category);
69}
70
pkebd2f650a2016-08-09 14:53:4571base::Optional<CategoryInfo> ContentSuggestionsService::GetCategoryInfo(
72 Category category) const {
73 auto iterator = providers_by_category_.find(category);
74 if (iterator == providers_by_category_.end())
75 return base::Optional<CategoryInfo>();
76 return iterator->second->GetCategoryInfo(category);
77}
78
pke6dbb90af2016-07-08 14:00:4679const std::vector<ContentSuggestion>&
pke9c5095ac2016-08-01 13:53:1280ContentSuggestionsService::GetSuggestionsForCategory(Category category) const {
pke6dbb90af2016-07-08 14:00:4681 auto iterator = suggestions_by_category_.find(category);
82 if (iterator == suggestions_by_category_.end())
83 return no_suggestions_;
84 return iterator->second;
85}
86
87void ContentSuggestionsService::FetchSuggestionImage(
treib4bbc54922016-09-28 17:26:4488 const ContentSuggestion::ID& suggestion_id,
pke6dbb90af2016-07-08 14:00:4689 const ImageFetchedCallback& callback) {
treib4bbc54922016-09-28 17:26:4490 if (!providers_by_category_.count(suggestion_id.category())) {
pke6dbb90af2016-07-08 14:00:4691 LOG(WARNING) << "Requested image for suggestion " << suggestion_id
treib4bbc54922016-09-28 17:26:4492 << " for unavailable category " << suggestion_id.category();
pke1da90602016-08-05 14:20:2793 base::ThreadTaskRunnerHandle::Get()->PostTask(
pkef29505d2016-08-26 14:46:3494 FROM_HERE, base::Bind(callback, gfx::Image()));
pke6dbb90af2016-07-08 14:00:4695 return;
96 }
treib4bbc54922016-09-28 17:26:4497 providers_by_category_[suggestion_id.category()]->FetchSuggestionImage(
98 suggestion_id, callback);
pke6dbb90af2016-07-08 14:00:4699}
100
vitaliii685fdfaa2016-08-31 11:25:46101void ContentSuggestionsService::ClearHistory(
102 base::Time begin,
103 base::Time end,
104 const base::Callback<bool(const GURL& url)>& filter) {
105 for (const auto& provider : providers_) {
106 provider->ClearHistory(begin, end, filter);
107 }
108}
109
treib7d1d7a52016-08-24 14:04:55110void ContentSuggestionsService::ClearAllCachedSuggestions() {
pke6dbb90af2016-07-08 14:00:46111 suggestions_by_category_.clear();
pke151b5502016-08-09 12:15:13112 for (const auto& category_provider_pair : providers_by_category_) {
treib7d1d7a52016-08-24 14:04:55113 category_provider_pair.second->ClearCachedSuggestions(
pke151b5502016-08-09 12:15:13114 category_provider_pair.first);
pke222d8a52016-08-10 12:37:52115 FOR_EACH_OBSERVER(Observer, observers_,
116 OnNewSuggestions(category_provider_pair.first));
pke6dbb90af2016-07-08 14:00:46117 }
pke6dbb90af2016-07-08 14:00:46118}
119
jkrcal928ed3f2016-09-23 18:47:06120void ContentSuggestionsService::ClearCachedSuggestions(Category category) {
pke151b5502016-08-09 12:15:13121 suggestions_by_category_[category].clear();
122 auto iterator = providers_by_category_.find(category);
123 if (iterator != providers_by_category_.end())
treib7d1d7a52016-08-24 14:04:55124 iterator->second->ClearCachedSuggestions(category);
pke151b5502016-08-09 12:15:13125}
126
pkede0dd9f2016-08-23 09:18:11127void ContentSuggestionsService::GetDismissedSuggestionsForDebugging(
128 Category category,
129 const DismissedSuggestionsCallback& callback) {
pke151b5502016-08-09 12:15:13130 auto iterator = providers_by_category_.find(category);
pkede0dd9f2016-08-23 09:18:11131 if (iterator != providers_by_category_.end())
132 iterator->second->GetDismissedSuggestionsForDebugging(category, callback);
133 else
134 callback.Run(std::vector<ContentSuggestion>());
pke151b5502016-08-09 12:15:13135}
136
137void ContentSuggestionsService::ClearDismissedSuggestionsForDebugging(
138 Category category) {
139 auto iterator = providers_by_category_.find(category);
140 if (iterator != providers_by_category_.end())
141 iterator->second->ClearDismissedSuggestionsForDebugging(category);
pke6dbb90af2016-07-08 14:00:46142}
143
pke2646c95b2016-07-25 12:18:44144void ContentSuggestionsService::DismissSuggestion(
treib4bbc54922016-09-28 17:26:44145 const ContentSuggestion::ID& suggestion_id) {
146 if (!providers_by_category_.count(suggestion_id.category())) {
pke2646c95b2016-07-25 12:18:44147 LOG(WARNING) << "Dismissed suggestion " << suggestion_id
treib4bbc54922016-09-28 17:26:44148 << " for unavailable category " << suggestion_id.category();
pke6dbb90af2016-07-08 14:00:46149 return;
150 }
treib4bbc54922016-09-28 17:26:44151 providers_by_category_[suggestion_id.category()]->DismissSuggestion(
152 suggestion_id);
pke6dbb90af2016-07-08 14:00:46153
154 // Remove the suggestion locally.
treib4bbc54922016-09-28 17:26:44155 bool removed = RemoveSuggestionByID(suggestion_id);
pke2a48f852016-08-18 13:33:52156 DCHECK(removed) << "The dismissed suggestion " << suggestion_id
157 << " has already been removed. Providers must not call"
158 << " OnNewSuggestions in response to DismissSuggestion.";
pke6dbb90af2016-07-08 14:00:46159}
160
dgn212feea3b2016-09-16 15:08:20161void ContentSuggestionsService::DismissCategory(Category category) {
162 auto providers_it = providers_by_category_.find(category);
163 if (providers_it == providers_by_category_.end())
164 return;
165
dgn52914722016-10-18 10:28:42166 ContentSuggestionsProvider* provider = providers_it->second;
167 UnregisterCategory(category, provider);
168
169 dismissed_providers_by_category_[category] = provider;
170 StoreDismissedCategoriesToPrefs();
dgn212feea3b2016-09-16 15:08:20171}
172
mvanouwerkerk52783d92016-10-12 11:03:40173void ContentSuggestionsService::RestoreDismissedCategories() {
174 // Make a copy as the original will be modified during iteration.
175 auto dismissed_providers_by_category_copy = dismissed_providers_by_category_;
176 for (const auto& category_provider_pair :
177 dismissed_providers_by_category_copy) {
dgn52914722016-10-18 10:28:42178 RestoreDismissedCategory(category_provider_pair.first);
mvanouwerkerk52783d92016-10-12 11:03:40179 }
dgn52914722016-10-18 10:28:42180 StoreDismissedCategoriesToPrefs();
mvanouwerkerk52783d92016-10-12 11:03:40181 DCHECK(dismissed_providers_by_category_.empty());
182}
183
pke6dbb90af2016-07-08 14:00:46184void ContentSuggestionsService::AddObserver(Observer* observer) {
185 observers_.AddObserver(observer);
186}
187
188void ContentSuggestionsService::RemoveObserver(Observer* observer) {
189 observers_.RemoveObserver(observer);
190}
191
192void ContentSuggestionsService::RegisterProvider(
pke5728f082016-08-03 17:27:35193 std::unique_ptr<ContentSuggestionsProvider> provider) {
194 DCHECK(state_ == State::ENABLED);
pke5728f082016-08-03 17:27:35195 providers_.push_back(std::move(provider));
pke6dbb90af2016-07-08 14:00:46196}
197
198////////////////////////////////////////////////////////////////////////////////
199// Private methods
200
201void ContentSuggestionsService::OnNewSuggestions(
pke4d3a4d62016-08-02 09:06:21202 ContentSuggestionsProvider* provider,
203 Category category,
treib62e819e2016-09-27 11:47:34204 std::vector<ContentSuggestion> suggestions) {
dgn52914722016-10-18 10:28:42205 if (TryRegisterProviderForCategory(provider, category)) {
pke4d3a4d62016-08-02 09:06:21206 NotifyCategoryStatusChanged(category);
dgn52914722016-10-18 10:28:42207 } else if (IsCategoryDismissed(category)) {
208 // The category has been registered as a dismissed one. We need to
209 // check if the dismissal can be cleared now that we received new data.
210 if (suggestions.empty())
211 return;
212
213 RestoreDismissedCategory(category);
214 StoreDismissedCategoriesToPrefs();
215
216 NotifyCategoryStatusChanged(category);
217 }
pke3b2e3632016-08-12 12:52:53218
treib39fffa12016-10-14 14:59:10219 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) {
220 // A provider shouldn't send us suggestions while it's not available.
221 DCHECK(suggestions.empty());
pke3b2e3632016-08-12 12:52:53222 return;
treib39fffa12016-10-14 14:59:10223 }
pke6dbb90af2016-07-08 14:00:46224
treib62e819e2016-09-27 11:47:34225 suggestions_by_category_[category] = std::move(suggestions);
pke6dbb90af2016-07-08 14:00:46226
treib063e6a62016-08-25 11:34:29227 // The positioning of the bookmarks category depends on whether it's empty.
228 // TODO(treib): Remove this temporary hack, crbug.com/640568.
229 if (category.IsKnownCategory(KnownCategories::BOOKMARKS))
230 SortCategories();
231
pke222d8a52016-08-10 12:37:52232 FOR_EACH_OBSERVER(Observer, observers_, OnNewSuggestions(category));
pke6dbb90af2016-07-08 14:00:46233}
234
235void ContentSuggestionsService::OnCategoryStatusChanged(
pke4d3a4d62016-08-02 09:06:21236 ContentSuggestionsProvider* provider,
237 Category category,
pke9c5095ac2016-08-01 13:53:12238 CategoryStatus new_status) {
pke4d3a4d62016-08-02 09:06:21239 if (new_status == CategoryStatus::NOT_PROVIDED) {
dgn52914722016-10-18 10:28:42240 UnregisterCategory(category, provider);
pke4d3a4d62016-08-02 09:06:21241 } else {
dgn52914722016-10-18 10:28:42242 if (!IsCategoryStatusAvailable(new_status))
243 suggestions_by_category_.erase(category);
244 TryRegisterProviderForCategory(provider, category);
pke4d3a4d62016-08-02 09:06:21245 DCHECK_EQ(new_status, provider->GetCategoryStatus(category));
246 }
dgn52914722016-10-18 10:28:42247
248 if (!IsCategoryDismissed(category))
249 NotifyCategoryStatusChanged(category);
pke6dbb90af2016-07-08 14:00:46250}
251
pke2a48f852016-08-18 13:33:52252void ContentSuggestionsService::OnSuggestionInvalidated(
253 ContentSuggestionsProvider* provider,
treib4bbc54922016-09-28 17:26:44254 const ContentSuggestion::ID& suggestion_id) {
255 RemoveSuggestionByID(suggestion_id);
pke2a48f852016-08-18 13:33:52256 FOR_EACH_OBSERVER(Observer, observers_,
treib4bbc54922016-09-28 17:26:44257 OnSuggestionInvalidated(suggestion_id));
pke2a48f852016-08-18 13:33:52258}
259
vitaliii45941152016-09-05 08:58:13260// history::HistoryServiceObserver implementation.
261void ContentSuggestionsService::OnURLsDeleted(
262 history::HistoryService* history_service,
263 bool all_history,
264 bool expired,
265 const history::URLRows& deleted_rows,
266 const std::set<GURL>& favicon_urls) {
267 // We don't care about expired entries.
268 if (expired)
269 return;
270
271 // Redirect to ClearHistory().
272 if (all_history) {
273 base::Time begin = base::Time();
274 base::Time end = base::Time::Max();
275 base::Callback<bool(const GURL& url)> filter =
276 base::Bind([](const GURL& url) { return true; });
277 ClearHistory(begin, end, filter);
278 } else {
279 if (deleted_rows.empty())
280 return;
281
282 base::Time begin = deleted_rows[0].last_visit();
283 base::Time end = deleted_rows[0].last_visit();
284 std::set<GURL> deleted_urls;
285 for (const history::URLRow& row : deleted_rows) {
286 if (row.last_visit() < begin)
287 begin = row.last_visit();
288 if (row.last_visit() > end)
289 end = row.last_visit();
290 deleted_urls.insert(row.url());
291 }
292 base::Callback<bool(const GURL& url)> filter = base::Bind(
293 [](const std::set<GURL>& set, const GURL& url) {
294 return set.count(url) != 0;
295 },
296 deleted_urls);
297 ClearHistory(begin, end, filter);
298 }
299}
300
301void ContentSuggestionsService::HistoryServiceBeingDeleted(
302 history::HistoryService* history_service) {
303 history_service_observer_.RemoveAll();
304}
305
dgn52914722016-10-18 10:28:42306bool ContentSuggestionsService::TryRegisterProviderForCategory(
pke4d3a4d62016-08-02 09:06:21307 ContentSuggestionsProvider* provider,
308 Category category) {
309 auto it = providers_by_category_.find(category);
310 if (it != providers_by_category_.end()) {
311 DCHECK_EQ(it->second, provider);
312 return false;
313 }
314
mvanouwerkerk52783d92016-10-12 11:03:40315 auto dismissed_it = dismissed_providers_by_category_.find(category);
316 if (dismissed_it != dismissed_providers_by_category_.end()) {
dgn52914722016-10-18 10:28:42317 // The initialisation of dismissed categories registers them with |nullptr|
318 // for providers, we need to check for that to see if the provider is
319 // already registered or not.
320 if (!dismissed_it->second) {
321 dismissed_it->second = provider;
322 } else {
323 DCHECK_EQ(dismissed_it->second, provider);
324 }
325 return false;
mvanouwerkerk52783d92016-10-12 11:03:40326 }
327
dgn52914722016-10-18 10:28:42328 RegisterCategory(category, provider);
329 return true;
330}
331
332void ContentSuggestionsService::RegisterCategory(
333 Category category,
334 ContentSuggestionsProvider* provider) {
335 DCHECK(!base::ContainsKey(providers_by_category_, category));
336 DCHECK(!IsCategoryDismissed(category));
337
pke4d3a4d62016-08-02 09:06:21338 providers_by_category_[category] = provider;
339 categories_.push_back(category);
treib063e6a62016-08-25 11:34:29340 SortCategories();
pke4d3a4d62016-08-02 09:06:21341 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) {
342 suggestions_by_category_.insert(
343 std::make_pair(category, std::vector<ContentSuggestion>()));
344 }
dgn52914722016-10-18 10:28:42345}
346
347void ContentSuggestionsService::UnregisterCategory(
348 Category category,
349 ContentSuggestionsProvider* provider) {
350 auto providers_it = providers_by_category_.find(category);
351 if (providers_it == providers_by_category_.end()) {
352 DCHECK(IsCategoryDismissed(category));
353 return;
354 }
355
356 DCHECK_EQ(provider, providers_it->second);
357 providers_by_category_.erase(providers_it);
358 categories_.erase(
359 std::find(categories_.begin(), categories_.end(), category));
360 suggestions_by_category_.erase(category);
pke6dbb90af2016-07-08 14:00:46361}
362
pke2a48f852016-08-18 13:33:52363bool ContentSuggestionsService::RemoveSuggestionByID(
treib4bbc54922016-09-28 17:26:44364 const ContentSuggestion::ID& suggestion_id) {
pke2a48f852016-08-18 13:33:52365 std::vector<ContentSuggestion>* suggestions =
treib4bbc54922016-09-28 17:26:44366 &suggestions_by_category_[suggestion_id.category()];
pke2a48f852016-08-18 13:33:52367 auto position =
368 std::find_if(suggestions->begin(), suggestions->end(),
369 [&suggestion_id](const ContentSuggestion& suggestion) {
370 return suggestion_id == suggestion.id();
371 });
372 if (position == suggestions->end())
373 return false;
374 suggestions->erase(position);
treib063e6a62016-08-25 11:34:29375
376 // The positioning of the bookmarks category depends on whether it's empty.
377 // TODO(treib): Remove this temporary hack, crbug.com/640568.
treib4bbc54922016-09-28 17:26:44378 if (suggestion_id.category().IsKnownCategory(KnownCategories::BOOKMARKS))
treib063e6a62016-08-25 11:34:29379 SortCategories();
380
pke2a48f852016-08-18 13:33:52381 return true;
382}
383
pke9c5095ac2016-08-01 13:53:12384void ContentSuggestionsService::NotifyCategoryStatusChanged(Category category) {
pke6dbb90af2016-07-08 14:00:46385 FOR_EACH_OBSERVER(
386 Observer, observers_,
387 OnCategoryStatusChanged(category, GetCategoryStatus(category)));
388}
389
treib063e6a62016-08-25 11:34:29390void ContentSuggestionsService::SortCategories() {
391 auto it = suggestions_by_category_.find(
392 category_factory_.FromKnownCategory(KnownCategories::BOOKMARKS));
393 bool bookmarks_empty =
394 (it == suggestions_by_category_.end() || it->second.empty());
395 std::sort(
396 categories_.begin(), categories_.end(),
397 [this, bookmarks_empty](const Category& left, const Category& right) {
398 // If the bookmarks section is empty, put it at the end.
399 // TODO(treib): This is a temporary hack, see crbug.com/640568.
400 if (bookmarks_empty) {
401 if (left.IsKnownCategory(KnownCategories::BOOKMARKS))
402 return false;
403 if (right.IsKnownCategory(KnownCategories::BOOKMARKS))
404 return true;
405 }
406 return category_factory_.CompareCategories(left, right);
407 });
408}
409
dgn52914722016-10-18 10:28:42410bool ContentSuggestionsService::IsCategoryDismissed(Category category) const {
411 return base::ContainsKey(dismissed_providers_by_category_, category);
412}
413
414void ContentSuggestionsService::RestoreDismissedCategory(Category category) {
415 auto dismissed_it = dismissed_providers_by_category_.find(category);
416 DCHECK(base::ContainsKey(dismissed_providers_by_category_, category));
417
418 // Keep the reference to the provider and remove it from the dismissed ones,
419 // because the category registration enforces that it's not dismissed.
420 ContentSuggestionsProvider* provider = dismissed_it->second;
421 dismissed_providers_by_category_.erase(dismissed_it);
422
423 if (provider)
424 RegisterCategory(category, provider);
425}
426
427void ContentSuggestionsService::RestoreDismissedCategoriesFromPrefs() {
428 // This must only be called at startup.
429 DCHECK(dismissed_providers_by_category_.empty());
430 DCHECK(providers_by_category_.empty());
431
432 const base::ListValue* list =
433 pref_service_->GetList(prefs::kDismissedCategories);
434 for (const std::unique_ptr<base::Value>& entry : *list) {
435 int id = 0;
436 if (!entry->GetAsInteger(&id)) {
437 DLOG(WARNING) << "Invalid category pref value: " << *entry;
438 continue;
439 }
440
441 // When the provider is registered, it will be stored in this map.
442 dismissed_providers_by_category_[category_factory()->FromIDValue(id)] =
443 nullptr;
444 }
445}
446
447void ContentSuggestionsService::StoreDismissedCategoriesToPrefs() {
448 base::ListValue list;
449 for (const auto& category_provider_pair : dismissed_providers_by_category_) {
450 list.AppendInteger(category_provider_pair.first.id());
451 }
452
453 pref_service_->Set(prefs::kDismissedCategories, list);
454}
455
pke6dbb90af2016-07-08 14:00:46456} // namespace ntp_snippets