blob: 642f77078b7c5504f5c8ff4cc97c0a4ab9adeff0 [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),
treibfd0db3c2016-09-20 12:22:1526 ntp_snippets_service_(nullptr),
jkrcale13510e2016-09-08 17:56:2027 user_classifier_(pref_service) {
vitaliii45941152016-09-05 08:58:1328 // Can be null in tests.
29 if (history_service)
30 history_service_observer_.Add(history_service);
31}
pke6dbb90af2016-07-08 14:00:4632
33ContentSuggestionsService::~ContentSuggestionsService() {}
34
35void ContentSuggestionsService::Shutdown() {
pke5728f082016-08-03 17:27:3536 ntp_snippets_service_ = nullptr;
37 id_category_map_.clear();
38 suggestions_by_category_.clear();
39 providers_by_category_.clear();
40 categories_.clear();
41 providers_.clear();
pke6dbb90af2016-07-08 14:00:4642 state_ = State::DISABLED;
43 FOR_EACH_OBSERVER(Observer, observers_, ContentSuggestionsServiceShutdown());
44}
45
pke9c5095ac2016-08-01 13:53:1246CategoryStatus ContentSuggestionsService::GetCategoryStatus(
47 Category category) const {
pke6dbb90af2016-07-08 14:00:4648 if (state_ == State::DISABLED) {
pke9c5095ac2016-08-01 13:53:1249 return CategoryStatus::ALL_SUGGESTIONS_EXPLICITLY_DISABLED;
pke6dbb90af2016-07-08 14:00:4650 }
51
pke4d3a4d62016-08-02 09:06:2152 auto iterator = providers_by_category_.find(category);
53 if (iterator == providers_by_category_.end())
pke9c5095ac2016-08-01 13:53:1254 return CategoryStatus::NOT_PROVIDED;
pke6dbb90af2016-07-08 14:00:4655
56 return iterator->second->GetCategoryStatus(category);
57}
58
pkebd2f650a2016-08-09 14:53:4559base::Optional<CategoryInfo> ContentSuggestionsService::GetCategoryInfo(
60 Category category) const {
61 auto iterator = providers_by_category_.find(category);
62 if (iterator == providers_by_category_.end())
63 return base::Optional<CategoryInfo>();
64 return iterator->second->GetCategoryInfo(category);
65}
66
pke6dbb90af2016-07-08 14:00:4667const std::vector<ContentSuggestion>&
pke9c5095ac2016-08-01 13:53:1268ContentSuggestionsService::GetSuggestionsForCategory(Category category) const {
pke6dbb90af2016-07-08 14:00:4669 auto iterator = suggestions_by_category_.find(category);
70 if (iterator == suggestions_by_category_.end())
71 return no_suggestions_;
72 return iterator->second;
73}
74
75void ContentSuggestionsService::FetchSuggestionImage(
76 const std::string& suggestion_id,
77 const ImageFetchedCallback& callback) {
78 if (!id_category_map_.count(suggestion_id)) {
79 LOG(WARNING) << "Requested image for unknown suggestion " << suggestion_id;
pke1da90602016-08-05 14:20:2780 base::ThreadTaskRunnerHandle::Get()->PostTask(
pkef29505d2016-08-26 14:46:3481 FROM_HERE, base::Bind(callback, gfx::Image()));
pke6dbb90af2016-07-08 14:00:4682 return;
83 }
pke9c5095ac2016-08-01 13:53:1284 Category category = id_category_map_.at(suggestion_id);
pke4d3a4d62016-08-02 09:06:2185 if (!providers_by_category_.count(category)) {
pke6dbb90af2016-07-08 14:00:4686 LOG(WARNING) << "Requested image for suggestion " << suggestion_id
pke31b07942016-08-01 11:35:0287 << " for unavailable category " << category;
pke1da90602016-08-05 14:20:2788 base::ThreadTaskRunnerHandle::Get()->PostTask(
pkef29505d2016-08-26 14:46:3489 FROM_HERE, base::Bind(callback, gfx::Image()));
pke6dbb90af2016-07-08 14:00:4690 return;
91 }
pke4d3a4d62016-08-02 09:06:2192 providers_by_category_[category]->FetchSuggestionImage(suggestion_id,
93 callback);
pke6dbb90af2016-07-08 14:00:4694}
95
vitaliii685fdfaa2016-08-31 11:25:4696void ContentSuggestionsService::ClearHistory(
97 base::Time begin,
98 base::Time end,
99 const base::Callback<bool(const GURL& url)>& filter) {
100 for (const auto& provider : providers_) {
101 provider->ClearHistory(begin, end, filter);
102 }
103}
104
treib7d1d7a52016-08-24 14:04:55105void ContentSuggestionsService::ClearAllCachedSuggestions() {
pke6dbb90af2016-07-08 14:00:46106 suggestions_by_category_.clear();
107 id_category_map_.clear();
pke151b5502016-08-09 12:15:13108 for (const auto& category_provider_pair : providers_by_category_) {
treib7d1d7a52016-08-24 14:04:55109 category_provider_pair.second->ClearCachedSuggestions(
pke151b5502016-08-09 12:15:13110 category_provider_pair.first);
pke222d8a52016-08-10 12:37:52111 FOR_EACH_OBSERVER(Observer, observers_,
112 OnNewSuggestions(category_provider_pair.first));
pke6dbb90af2016-07-08 14:00:46113 }
pke6dbb90af2016-07-08 14:00:46114}
115
treib7d1d7a52016-08-24 14:04:55116void ContentSuggestionsService::ClearCachedSuggestions(
pke151b5502016-08-09 12:15:13117 Category category) {
118 for (const ContentSuggestion& suggestion :
119 suggestions_by_category_[category]) {
120 id_category_map_.erase(suggestion.id());
pke6dbb90af2016-07-08 14:00:46121 }
pke151b5502016-08-09 12:15:13122 suggestions_by_category_[category].clear();
123 auto iterator = providers_by_category_.find(category);
124 if (iterator != providers_by_category_.end())
treib7d1d7a52016-08-24 14:04:55125 iterator->second->ClearCachedSuggestions(category);
pke151b5502016-08-09 12:15:13126}
127
pkede0dd9f2016-08-23 09:18:11128void ContentSuggestionsService::GetDismissedSuggestionsForDebugging(
129 Category category,
130 const DismissedSuggestionsCallback& callback) {
pke151b5502016-08-09 12:15:13131 auto iterator = providers_by_category_.find(category);
pkede0dd9f2016-08-23 09:18:11132 if (iterator != providers_by_category_.end())
133 iterator->second->GetDismissedSuggestionsForDebugging(category, callback);
134 else
135 callback.Run(std::vector<ContentSuggestion>());
pke151b5502016-08-09 12:15:13136}
137
138void ContentSuggestionsService::ClearDismissedSuggestionsForDebugging(
139 Category category) {
140 auto iterator = providers_by_category_.find(category);
141 if (iterator != providers_by_category_.end())
142 iterator->second->ClearDismissedSuggestionsForDebugging(category);
pke6dbb90af2016-07-08 14:00:46143}
144
pke2646c95b2016-07-25 12:18:44145void ContentSuggestionsService::DismissSuggestion(
pke6dbb90af2016-07-08 14:00:46146 const std::string& suggestion_id) {
147 if (!id_category_map_.count(suggestion_id)) {
pke2646c95b2016-07-25 12:18:44148 LOG(WARNING) << "Dismissed unknown suggestion " << suggestion_id;
pke6dbb90af2016-07-08 14:00:46149 return;
150 }
pke9c5095ac2016-08-01 13:53:12151 Category category = id_category_map_.at(suggestion_id);
pke4d3a4d62016-08-02 09:06:21152 if (!providers_by_category_.count(category)) {
pke2646c95b2016-07-25 12:18:44153 LOG(WARNING) << "Dismissed suggestion " << suggestion_id
pke31b07942016-08-01 11:35:02154 << " for unavailable category " << category;
pke6dbb90af2016-07-08 14:00:46155 return;
156 }
pke4d3a4d62016-08-02 09:06:21157 providers_by_category_[category]->DismissSuggestion(suggestion_id);
pke6dbb90af2016-07-08 14:00:46158
159 // Remove the suggestion locally.
pke2a48f852016-08-18 13:33:52160 bool removed = RemoveSuggestionByID(category, suggestion_id);
161 DCHECK(removed) << "The dismissed suggestion " << suggestion_id
162 << " has already been removed. Providers must not call"
163 << " OnNewSuggestions in response to DismissSuggestion.";
pke6dbb90af2016-07-08 14:00:46164}
165
dgn212feea3b2016-09-16 15:08:20166void ContentSuggestionsService::DismissCategory(Category category) {
167 auto providers_it = providers_by_category_.find(category);
168 if (providers_it == providers_by_category_.end())
169 return;
170
171 providers_by_category_.erase(providers_it);
172 categories_.erase(
173 std::find(categories_.begin(), categories_.end(), category));
174}
175
pke6dbb90af2016-07-08 14:00:46176void ContentSuggestionsService::AddObserver(Observer* observer) {
177 observers_.AddObserver(observer);
178}
179
180void ContentSuggestionsService::RemoveObserver(Observer* observer) {
181 observers_.RemoveObserver(observer);
182}
183
184void ContentSuggestionsService::RegisterProvider(
pke5728f082016-08-03 17:27:35185 std::unique_ptr<ContentSuggestionsProvider> provider) {
186 DCHECK(state_ == State::ENABLED);
pke5728f082016-08-03 17:27:35187 providers_.push_back(std::move(provider));
pke6dbb90af2016-07-08 14:00:46188}
189
190////////////////////////////////////////////////////////////////////////////////
191// Private methods
192
193void ContentSuggestionsService::OnNewSuggestions(
pke4d3a4d62016-08-02 09:06:21194 ContentSuggestionsProvider* provider,
195 Category category,
pke6dbb90af2016-07-08 14:00:46196 std::vector<ContentSuggestion> new_suggestions) {
pke3b2e3632016-08-12 12:52:53197 if (RegisterCategoryIfRequired(provider, category))
pke4d3a4d62016-08-02 09:06:21198 NotifyCategoryStatusChanged(category);
pke3b2e3632016-08-12 12:52:53199
200 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category)))
201 return;
pke6dbb90af2016-07-08 14:00:46202
203 for (const ContentSuggestion& suggestion :
pke4d3a4d62016-08-02 09:06:21204 suggestions_by_category_[category]) {
pke6dbb90af2016-07-08 14:00:46205 id_category_map_.erase(suggestion.id());
206 }
207
pke3b2e3632016-08-12 12:52:53208 for (const ContentSuggestion& suggestion : new_suggestions)
pke4d3a4d62016-08-02 09:06:21209 id_category_map_.insert(std::make_pair(suggestion.id(), category));
pke6dbb90af2016-07-08 14:00:46210
pke4d3a4d62016-08-02 09:06:21211 suggestions_by_category_[category] = std::move(new_suggestions);
pke6dbb90af2016-07-08 14:00:46212
treib063e6a62016-08-25 11:34:29213 // The positioning of the bookmarks category depends on whether it's empty.
214 // TODO(treib): Remove this temporary hack, crbug.com/640568.
215 if (category.IsKnownCategory(KnownCategories::BOOKMARKS))
216 SortCategories();
217
pke222d8a52016-08-10 12:37:52218 FOR_EACH_OBSERVER(Observer, observers_, OnNewSuggestions(category));
pke6dbb90af2016-07-08 14:00:46219}
220
221void ContentSuggestionsService::OnCategoryStatusChanged(
pke4d3a4d62016-08-02 09:06:21222 ContentSuggestionsProvider* provider,
223 Category category,
pke9c5095ac2016-08-01 13:53:12224 CategoryStatus new_status) {
225 if (!IsCategoryStatusAvailable(new_status)) {
pke6dbb90af2016-07-08 14:00:46226 for (const ContentSuggestion& suggestion :
pke4d3a4d62016-08-02 09:06:21227 suggestions_by_category_[category]) {
pke6dbb90af2016-07-08 14:00:46228 id_category_map_.erase(suggestion.id());
229 }
pke4d3a4d62016-08-02 09:06:21230 suggestions_by_category_.erase(category);
pke6dbb90af2016-07-08 14:00:46231 }
pke4d3a4d62016-08-02 09:06:21232 if (new_status == CategoryStatus::NOT_PROVIDED) {
dgn212feea3b2016-09-16 15:08:20233 DCHECK(providers_by_category_.find(category) !=
234 providers_by_category_.end());
235 DCHECK_EQ(provider, providers_by_category_.find(category)->second);
236 DismissCategory(category);
pke4d3a4d62016-08-02 09:06:21237 } else {
238 RegisterCategoryIfRequired(provider, category);
239 DCHECK_EQ(new_status, provider->GetCategoryStatus(category));
240 }
241 NotifyCategoryStatusChanged(category);
pke6dbb90af2016-07-08 14:00:46242}
243
pke2a48f852016-08-18 13:33:52244void ContentSuggestionsService::OnSuggestionInvalidated(
245 ContentSuggestionsProvider* provider,
246 Category category,
247 const std::string& suggestion_id) {
248 RemoveSuggestionByID(category, suggestion_id);
249 FOR_EACH_OBSERVER(Observer, observers_,
250 OnSuggestionInvalidated(category, suggestion_id));
251}
252
vitaliii45941152016-09-05 08:58:13253// history::HistoryServiceObserver implementation.
254void ContentSuggestionsService::OnURLsDeleted(
255 history::HistoryService* history_service,
256 bool all_history,
257 bool expired,
258 const history::URLRows& deleted_rows,
259 const std::set<GURL>& favicon_urls) {
260 // We don't care about expired entries.
261 if (expired)
262 return;
263
264 // Redirect to ClearHistory().
265 if (all_history) {
266 base::Time begin = base::Time();
267 base::Time end = base::Time::Max();
268 base::Callback<bool(const GURL& url)> filter =
269 base::Bind([](const GURL& url) { return true; });
270 ClearHistory(begin, end, filter);
271 } else {
272 if (deleted_rows.empty())
273 return;
274
275 base::Time begin = deleted_rows[0].last_visit();
276 base::Time end = deleted_rows[0].last_visit();
277 std::set<GURL> deleted_urls;
278 for (const history::URLRow& row : deleted_rows) {
279 if (row.last_visit() < begin)
280 begin = row.last_visit();
281 if (row.last_visit() > end)
282 end = row.last_visit();
283 deleted_urls.insert(row.url());
284 }
285 base::Callback<bool(const GURL& url)> filter = base::Bind(
286 [](const std::set<GURL>& set, const GURL& url) {
287 return set.count(url) != 0;
288 },
289 deleted_urls);
290 ClearHistory(begin, end, filter);
291 }
292}
293
294void ContentSuggestionsService::HistoryServiceBeingDeleted(
295 history::HistoryService* history_service) {
296 history_service_observer_.RemoveAll();
297}
298
pke4d3a4d62016-08-02 09:06:21299bool ContentSuggestionsService::RegisterCategoryIfRequired(
300 ContentSuggestionsProvider* provider,
301 Category category) {
302 auto it = providers_by_category_.find(category);
303 if (it != providers_by_category_.end()) {
304 DCHECK_EQ(it->second, provider);
305 return false;
306 }
307
308 providers_by_category_[category] = provider;
309 categories_.push_back(category);
treib063e6a62016-08-25 11:34:29310 SortCategories();
pke4d3a4d62016-08-02 09:06:21311 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) {
312 suggestions_by_category_.insert(
313 std::make_pair(category, std::vector<ContentSuggestion>()));
314 }
315 return true;
pke6dbb90af2016-07-08 14:00:46316}
317
pke2a48f852016-08-18 13:33:52318bool ContentSuggestionsService::RemoveSuggestionByID(
319 Category category,
320 const std::string& suggestion_id) {
321 id_category_map_.erase(suggestion_id);
322 std::vector<ContentSuggestion>* suggestions =
323 &suggestions_by_category_[category];
324 auto position =
325 std::find_if(suggestions->begin(), suggestions->end(),
326 [&suggestion_id](const ContentSuggestion& suggestion) {
327 return suggestion_id == suggestion.id();
328 });
329 if (position == suggestions->end())
330 return false;
331 suggestions->erase(position);
treib063e6a62016-08-25 11:34:29332
333 // The positioning of the bookmarks category depends on whether it's empty.
334 // TODO(treib): Remove this temporary hack, crbug.com/640568.
335 if (category.IsKnownCategory(KnownCategories::BOOKMARKS))
336 SortCategories();
337
pke2a48f852016-08-18 13:33:52338 return true;
339}
340
pke9c5095ac2016-08-01 13:53:12341void ContentSuggestionsService::NotifyCategoryStatusChanged(Category category) {
pke6dbb90af2016-07-08 14:00:46342 FOR_EACH_OBSERVER(
343 Observer, observers_,
344 OnCategoryStatusChanged(category, GetCategoryStatus(category)));
345}
346
treib063e6a62016-08-25 11:34:29347void ContentSuggestionsService::SortCategories() {
348 auto it = suggestions_by_category_.find(
349 category_factory_.FromKnownCategory(KnownCategories::BOOKMARKS));
350 bool bookmarks_empty =
351 (it == suggestions_by_category_.end() || it->second.empty());
352 std::sort(
353 categories_.begin(), categories_.end(),
354 [this, bookmarks_empty](const Category& left, const Category& right) {
355 // If the bookmarks section is empty, put it at the end.
356 // TODO(treib): This is a temporary hack, see crbug.com/640568.
357 if (bookmarks_empty) {
358 if (left.IsKnownCategory(KnownCategories::BOOKMARKS))
359 return false;
360 if (right.IsKnownCategory(KnownCategories::BOOKMARKS))
361 return true;
362 }
363 return category_factory_.CompareCategories(left, right);
364 });
365}
366
pke6dbb90af2016-07-08 14:00:46367} // namespace ntp_snippets