blob: c74d03e3030e93e9c244db784381f3fad4d17c46 [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"
pke6dbb90af2016-07-08 14:00:4616#include "ui/gfx/image/image.h"
17
18namespace ntp_snippets {
19
vitaliii45941152016-09-05 08:58:1320ContentSuggestionsService::ContentSuggestionsService(
21 State state,
jkrcale13510e2016-09-08 17:56:2022 history::HistoryService* history_service,
23 PrefService* pref_service)
24 : state_(state),
25 history_service_observer_(this),
26 user_classifier_(pref_service) {
vitaliii45941152016-09-05 08:58:1327 // Can be null in tests.
28 if (history_service)
29 history_service_observer_.Add(history_service);
30}
pke6dbb90af2016-07-08 14:00:4631
32ContentSuggestionsService::~ContentSuggestionsService() {}
33
34void ContentSuggestionsService::Shutdown() {
pke5728f082016-08-03 17:27:3535 ntp_snippets_service_ = nullptr;
36 id_category_map_.clear();
37 suggestions_by_category_.clear();
38 providers_by_category_.clear();
39 categories_.clear();
40 providers_.clear();
pke6dbb90af2016-07-08 14:00:4641 state_ = State::DISABLED;
42 FOR_EACH_OBSERVER(Observer, observers_, ContentSuggestionsServiceShutdown());
43}
44
pke9c5095ac2016-08-01 13:53:1245CategoryStatus ContentSuggestionsService::GetCategoryStatus(
46 Category category) const {
pke6dbb90af2016-07-08 14:00:4647 if (state_ == State::DISABLED) {
pke9c5095ac2016-08-01 13:53:1248 return CategoryStatus::ALL_SUGGESTIONS_EXPLICITLY_DISABLED;
pke6dbb90af2016-07-08 14:00:4649 }
50
pke4d3a4d62016-08-02 09:06:2151 auto iterator = providers_by_category_.find(category);
52 if (iterator == providers_by_category_.end())
pke9c5095ac2016-08-01 13:53:1253 return CategoryStatus::NOT_PROVIDED;
pke6dbb90af2016-07-08 14:00:4654
55 return iterator->second->GetCategoryStatus(category);
56}
57
pkebd2f650a2016-08-09 14:53:4558base::Optional<CategoryInfo> ContentSuggestionsService::GetCategoryInfo(
59 Category category) const {
60 auto iterator = providers_by_category_.find(category);
61 if (iterator == providers_by_category_.end())
62 return base::Optional<CategoryInfo>();
63 return iterator->second->GetCategoryInfo(category);
64}
65
pke6dbb90af2016-07-08 14:00:4666const std::vector<ContentSuggestion>&
pke9c5095ac2016-08-01 13:53:1267ContentSuggestionsService::GetSuggestionsForCategory(Category category) const {
pke6dbb90af2016-07-08 14:00:4668 auto iterator = suggestions_by_category_.find(category);
69 if (iterator == suggestions_by_category_.end())
70 return no_suggestions_;
71 return iterator->second;
72}
73
74void ContentSuggestionsService::FetchSuggestionImage(
75 const std::string& suggestion_id,
76 const ImageFetchedCallback& callback) {
77 if (!id_category_map_.count(suggestion_id)) {
78 LOG(WARNING) << "Requested image for unknown suggestion " << suggestion_id;
pke1da90602016-08-05 14:20:2779 base::ThreadTaskRunnerHandle::Get()->PostTask(
pkef29505d2016-08-26 14:46:3480 FROM_HERE, base::Bind(callback, gfx::Image()));
pke6dbb90af2016-07-08 14:00:4681 return;
82 }
pke9c5095ac2016-08-01 13:53:1283 Category category = id_category_map_.at(suggestion_id);
pke4d3a4d62016-08-02 09:06:2184 if (!providers_by_category_.count(category)) {
pke6dbb90af2016-07-08 14:00:4685 LOG(WARNING) << "Requested image for suggestion " << suggestion_id
pke31b07942016-08-01 11:35:0286 << " for unavailable category " << category;
pke1da90602016-08-05 14:20:2787 base::ThreadTaskRunnerHandle::Get()->PostTask(
pkef29505d2016-08-26 14:46:3488 FROM_HERE, base::Bind(callback, gfx::Image()));
pke6dbb90af2016-07-08 14:00:4689 return;
90 }
pke4d3a4d62016-08-02 09:06:2191 providers_by_category_[category]->FetchSuggestionImage(suggestion_id,
92 callback);
pke6dbb90af2016-07-08 14:00:4693}
94
vitaliii685fdfaa2016-08-31 11:25:4695void ContentSuggestionsService::ClearHistory(
96 base::Time begin,
97 base::Time end,
98 const base::Callback<bool(const GURL& url)>& filter) {
99 for (const auto& provider : providers_) {
100 provider->ClearHistory(begin, end, filter);
101 }
102}
103
treib7d1d7a52016-08-24 14:04:55104void ContentSuggestionsService::ClearAllCachedSuggestions() {
pke6dbb90af2016-07-08 14:00:46105 suggestions_by_category_.clear();
106 id_category_map_.clear();
pke151b5502016-08-09 12:15:13107 for (const auto& category_provider_pair : providers_by_category_) {
treib7d1d7a52016-08-24 14:04:55108 category_provider_pair.second->ClearCachedSuggestions(
pke151b5502016-08-09 12:15:13109 category_provider_pair.first);
pke222d8a52016-08-10 12:37:52110 FOR_EACH_OBSERVER(Observer, observers_,
111 OnNewSuggestions(category_provider_pair.first));
pke6dbb90af2016-07-08 14:00:46112 }
pke6dbb90af2016-07-08 14:00:46113}
114
treib7d1d7a52016-08-24 14:04:55115void ContentSuggestionsService::ClearCachedSuggestions(
pke151b5502016-08-09 12:15:13116 Category category) {
117 for (const ContentSuggestion& suggestion :
118 suggestions_by_category_[category]) {
119 id_category_map_.erase(suggestion.id());
pke6dbb90af2016-07-08 14:00:46120 }
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(
pke6dbb90af2016-07-08 14:00:46145 const std::string& suggestion_id) {
146 if (!id_category_map_.count(suggestion_id)) {
pke2646c95b2016-07-25 12:18:44147 LOG(WARNING) << "Dismissed unknown suggestion " << suggestion_id;
pke6dbb90af2016-07-08 14:00:46148 return;
149 }
pke9c5095ac2016-08-01 13:53:12150 Category category = id_category_map_.at(suggestion_id);
pke4d3a4d62016-08-02 09:06:21151 if (!providers_by_category_.count(category)) {
pke2646c95b2016-07-25 12:18:44152 LOG(WARNING) << "Dismissed suggestion " << suggestion_id
pke31b07942016-08-01 11:35:02153 << " for unavailable category " << category;
pke6dbb90af2016-07-08 14:00:46154 return;
155 }
pke4d3a4d62016-08-02 09:06:21156 providers_by_category_[category]->DismissSuggestion(suggestion_id);
pke6dbb90af2016-07-08 14:00:46157
158 // Remove the suggestion locally.
pke2a48f852016-08-18 13:33:52159 bool removed = RemoveSuggestionByID(category, suggestion_id);
160 DCHECK(removed) << "The dismissed suggestion " << suggestion_id
161 << " has already been removed. Providers must not call"
162 << " OnNewSuggestions in response to DismissSuggestion.";
pke6dbb90af2016-07-08 14:00:46163}
164
dgn212feea3b2016-09-16 15:08:20165void ContentSuggestionsService::DismissCategory(Category category) {
166 auto providers_it = providers_by_category_.find(category);
167 if (providers_it == providers_by_category_.end())
168 return;
169
170 providers_by_category_.erase(providers_it);
171 categories_.erase(
172 std::find(categories_.begin(), categories_.end(), category));
173}
174
pke6dbb90af2016-07-08 14:00:46175void ContentSuggestionsService::AddObserver(Observer* observer) {
176 observers_.AddObserver(observer);
177}
178
179void ContentSuggestionsService::RemoveObserver(Observer* observer) {
180 observers_.RemoveObserver(observer);
181}
182
183void ContentSuggestionsService::RegisterProvider(
pke5728f082016-08-03 17:27:35184 std::unique_ptr<ContentSuggestionsProvider> provider) {
185 DCHECK(state_ == State::ENABLED);
pke5728f082016-08-03 17:27:35186 providers_.push_back(std::move(provider));
pke6dbb90af2016-07-08 14:00:46187}
188
189////////////////////////////////////////////////////////////////////////////////
190// Private methods
191
192void ContentSuggestionsService::OnNewSuggestions(
pke4d3a4d62016-08-02 09:06:21193 ContentSuggestionsProvider* provider,
194 Category category,
pke6dbb90af2016-07-08 14:00:46195 std::vector<ContentSuggestion> new_suggestions) {
pke3b2e3632016-08-12 12:52:53196 if (RegisterCategoryIfRequired(provider, category))
pke4d3a4d62016-08-02 09:06:21197 NotifyCategoryStatusChanged(category);
pke3b2e3632016-08-12 12:52:53198
199 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category)))
200 return;
pke6dbb90af2016-07-08 14:00:46201
202 for (const ContentSuggestion& suggestion :
pke4d3a4d62016-08-02 09:06:21203 suggestions_by_category_[category]) {
pke6dbb90af2016-07-08 14:00:46204 id_category_map_.erase(suggestion.id());
205 }
206
pke3b2e3632016-08-12 12:52:53207 for (const ContentSuggestion& suggestion : new_suggestions)
pke4d3a4d62016-08-02 09:06:21208 id_category_map_.insert(std::make_pair(suggestion.id(), category));
pke6dbb90af2016-07-08 14:00:46209
pke4d3a4d62016-08-02 09:06:21210 suggestions_by_category_[category] = std::move(new_suggestions);
pke6dbb90af2016-07-08 14:00:46211
treib063e6a62016-08-25 11:34:29212 // The positioning of the bookmarks category depends on whether it's empty.
213 // TODO(treib): Remove this temporary hack, crbug.com/640568.
214 if (category.IsKnownCategory(KnownCategories::BOOKMARKS))
215 SortCategories();
216
pke222d8a52016-08-10 12:37:52217 FOR_EACH_OBSERVER(Observer, observers_, OnNewSuggestions(category));
pke6dbb90af2016-07-08 14:00:46218}
219
220void ContentSuggestionsService::OnCategoryStatusChanged(
pke4d3a4d62016-08-02 09:06:21221 ContentSuggestionsProvider* provider,
222 Category category,
pke9c5095ac2016-08-01 13:53:12223 CategoryStatus new_status) {
224 if (!IsCategoryStatusAvailable(new_status)) {
pke6dbb90af2016-07-08 14:00:46225 for (const ContentSuggestion& suggestion :
pke4d3a4d62016-08-02 09:06:21226 suggestions_by_category_[category]) {
pke6dbb90af2016-07-08 14:00:46227 id_category_map_.erase(suggestion.id());
228 }
pke4d3a4d62016-08-02 09:06:21229 suggestions_by_category_.erase(category);
pke6dbb90af2016-07-08 14:00:46230 }
pke4d3a4d62016-08-02 09:06:21231 if (new_status == CategoryStatus::NOT_PROVIDED) {
dgn212feea3b2016-09-16 15:08:20232 DCHECK(providers_by_category_.find(category) !=
233 providers_by_category_.end());
234 DCHECK_EQ(provider, providers_by_category_.find(category)->second);
235 DismissCategory(category);
pke4d3a4d62016-08-02 09:06:21236 } else {
237 RegisterCategoryIfRequired(provider, category);
238 DCHECK_EQ(new_status, provider->GetCategoryStatus(category));
239 }
240 NotifyCategoryStatusChanged(category);
pke6dbb90af2016-07-08 14:00:46241}
242
pke2a48f852016-08-18 13:33:52243void ContentSuggestionsService::OnSuggestionInvalidated(
244 ContentSuggestionsProvider* provider,
245 Category category,
246 const std::string& suggestion_id) {
247 RemoveSuggestionByID(category, suggestion_id);
248 FOR_EACH_OBSERVER(Observer, observers_,
249 OnSuggestionInvalidated(category, suggestion_id));
250}
251
vitaliii45941152016-09-05 08:58:13252// history::HistoryServiceObserver implementation.
253void ContentSuggestionsService::OnURLsDeleted(
254 history::HistoryService* history_service,
255 bool all_history,
256 bool expired,
257 const history::URLRows& deleted_rows,
258 const std::set<GURL>& favicon_urls) {
259 // We don't care about expired entries.
260 if (expired)
261 return;
262
263 // Redirect to ClearHistory().
264 if (all_history) {
265 base::Time begin = base::Time();
266 base::Time end = base::Time::Max();
267 base::Callback<bool(const GURL& url)> filter =
268 base::Bind([](const GURL& url) { return true; });
269 ClearHistory(begin, end, filter);
270 } else {
271 if (deleted_rows.empty())
272 return;
273
274 base::Time begin = deleted_rows[0].last_visit();
275 base::Time end = deleted_rows[0].last_visit();
276 std::set<GURL> deleted_urls;
277 for (const history::URLRow& row : deleted_rows) {
278 if (row.last_visit() < begin)
279 begin = row.last_visit();
280 if (row.last_visit() > end)
281 end = row.last_visit();
282 deleted_urls.insert(row.url());
283 }
284 base::Callback<bool(const GURL& url)> filter = base::Bind(
285 [](const std::set<GURL>& set, const GURL& url) {
286 return set.count(url) != 0;
287 },
288 deleted_urls);
289 ClearHistory(begin, end, filter);
290 }
291}
292
293void ContentSuggestionsService::HistoryServiceBeingDeleted(
294 history::HistoryService* history_service) {
295 history_service_observer_.RemoveAll();
296}
297
pke4d3a4d62016-08-02 09:06:21298bool ContentSuggestionsService::RegisterCategoryIfRequired(
299 ContentSuggestionsProvider* provider,
300 Category category) {
301 auto it = providers_by_category_.find(category);
302 if (it != providers_by_category_.end()) {
303 DCHECK_EQ(it->second, provider);
304 return false;
305 }
306
307 providers_by_category_[category] = provider;
308 categories_.push_back(category);
treib063e6a62016-08-25 11:34:29309 SortCategories();
pke4d3a4d62016-08-02 09:06:21310 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) {
311 suggestions_by_category_.insert(
312 std::make_pair(category, std::vector<ContentSuggestion>()));
313 }
314 return true;
pke6dbb90af2016-07-08 14:00:46315}
316
pke2a48f852016-08-18 13:33:52317bool ContentSuggestionsService::RemoveSuggestionByID(
318 Category category,
319 const std::string& suggestion_id) {
320 id_category_map_.erase(suggestion_id);
321 std::vector<ContentSuggestion>* suggestions =
322 &suggestions_by_category_[category];
323 auto position =
324 std::find_if(suggestions->begin(), suggestions->end(),
325 [&suggestion_id](const ContentSuggestion& suggestion) {
326 return suggestion_id == suggestion.id();
327 });
328 if (position == suggestions->end())
329 return false;
330 suggestions->erase(position);
treib063e6a62016-08-25 11:34:29331
332 // The positioning of the bookmarks category depends on whether it's empty.
333 // TODO(treib): Remove this temporary hack, crbug.com/640568.
334 if (category.IsKnownCategory(KnownCategories::BOOKMARKS))
335 SortCategories();
336
pke2a48f852016-08-18 13:33:52337 return true;
338}
339
pke9c5095ac2016-08-01 13:53:12340void ContentSuggestionsService::NotifyCategoryStatusChanged(Category category) {
pke6dbb90af2016-07-08 14:00:46341 FOR_EACH_OBSERVER(
342 Observer, observers_,
343 OnCategoryStatusChanged(category, GetCategoryStatus(category)));
344}
345
treib063e6a62016-08-25 11:34:29346void ContentSuggestionsService::SortCategories() {
347 auto it = suggestions_by_category_.find(
348 category_factory_.FromKnownCategory(KnownCategories::BOOKMARKS));
349 bool bookmarks_empty =
350 (it == suggestions_by_category_.end() || it->second.empty());
351 std::sort(
352 categories_.begin(), categories_.end(),
353 [this, bookmarks_empty](const Category& left, const Category& right) {
354 // If the bookmarks section is empty, put it at the end.
355 // TODO(treib): This is a temporary hack, see crbug.com/640568.
356 if (bookmarks_empty) {
357 if (left.IsKnownCategory(KnownCategories::BOOKMARKS))
358 return false;
359 if (right.IsKnownCategory(KnownCategories::BOOKMARKS))
360 return true;
361 }
362 return category_factory_.CompareCategories(left, right);
363 });
364}
365
pke6dbb90af2016-07-08 14:00:46366} // namespace ntp_snippets