blob: 565f4201abb2712dec310187962bd61bc050c29c [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"
jkrcal27b02c12017-03-21 11:18:2614#include "base/memory/ptr_util.h"
jkrcal08d79b52017-04-13 14:02:1115#include "base/metrics/histogram_macros.h"
pke6dbb90af2016-07-08 14:00:4616#include "base/strings/string_number_conversions.h"
pke1da90602016-08-05 14:20:2717#include "base/threading/thread_task_runner_handle.h"
jkrcal27b02c12017-03-21 11:18:2618#include "base/time/default_clock.h"
dgn52914722016-10-18 10:28:4219#include "base/values.h"
jkrcal7b7e71f12017-04-06 13:12:4020#include "components/favicon/core/large_icon_service.h"
21#include "components/favicon_base/fallback_icon_style.h"
22#include "components/favicon_base/favicon_types.h"
dgnf6500c12017-05-09 17:05:3123#include "components/ntp_snippets/content_suggestions_metrics.h"
dgn52914722016-10-18 10:28:4224#include "components/ntp_snippets/pref_names.h"
jkrcal8083bc52017-04-24 16:34:0225#include "components/ntp_snippets/remote/remote_suggestions_provider.h"
dgn52914722016-10-18 10:28:4226#include "components/prefs/pref_registry_simple.h"
27#include "components/prefs/pref_service.h"
pke6dbb90af2016-07-08 14:00:4628#include "ui/gfx/image/image.h"
29
30namespace ntp_snippets {
31
jkrcal08d79b52017-04-13 14:02:1132namespace {
33
34// Enumeration listing all possible outcomes for fetch attempts of favicons for
35// content suggestions. Used for UMA histograms, so do not change existing
36// values. Insert new values at the end, and update the histogram definition.
37// GENERATED_JAVA_ENUM_PACKAGE: org.chromium.chrome.browser.ntp.snippets
38enum class FaviconFetchResult {
39 SUCCESS_CACHED = 0,
40 SUCCESS_FETCHED = 1,
41 FAILURE = 2,
42 COUNT = 3
43};
44
45void RecordFaviconFetchResult(FaviconFetchResult result) {
46 UMA_HISTOGRAM_ENUMERATION(
47 "NewTabPage.ContentSuggestions.ArticleFaviconFetchResult", result,
48 FaviconFetchResult::COUNT);
49}
50
51} // namespace
52
vitaliii45941152016-09-05 08:58:1353ContentSuggestionsService::ContentSuggestionsService(
54 State state,
dgnf5708892016-11-22 10:36:2255 SigninManagerBase* signin_manager,
jkrcale13510e2016-09-08 17:56:2056 history::HistoryService* history_service,
jkrcal7b7e71f12017-04-06 13:12:4057 favicon::LargeIconService* large_icon_service,
vitaliii7456f5a2016-12-19 11:13:2558 PrefService* pref_service,
jkrcalf9966462017-03-29 16:25:2159 std::unique_ptr<CategoryRanker> category_ranker,
60 std::unique_ptr<UserClassifier> user_classifier,
61 std::unique_ptr<RemoteSuggestionsScheduler> remote_suggestions_scheduler)
jkrcale13510e2016-09-08 17:56:2062 : state_(state),
dgnf5708892016-11-22 10:36:2263 signin_observer_(this),
jkrcale13510e2016-09-08 17:56:2064 history_service_observer_(this),
jkrcal093410c2016-12-21 16:13:5565 remote_suggestions_provider_(nullptr),
jkrcal7b7e71f12017-04-06 13:12:4066 large_icon_service_(large_icon_service),
dgn52914722016-10-18 10:28:4267 pref_service_(pref_service),
jkrcalf9966462017-03-29 16:25:2168 remote_suggestions_scheduler_(std::move(remote_suggestions_scheduler)),
69 user_classifier_(std::move(user_classifier)),
vitaliii7456f5a2016-12-19 11:13:2570 category_ranker_(std::move(category_ranker)) {
vitaliii45941152016-09-05 08:58:1371 // Can be null in tests.
dgnf5708892016-11-22 10:36:2272 if (signin_manager) {
73 signin_observer_.Add(signin_manager);
74 }
75
vitaliii4408d6c2016-11-21 14:16:2476 if (history_service) {
vitaliii45941152016-09-05 08:58:1377 history_service_observer_.Add(history_service);
vitaliii4408d6c2016-11-21 14:16:2478 }
dgn52914722016-10-18 10:28:4279
80 RestoreDismissedCategoriesFromPrefs();
vitaliii45941152016-09-05 08:58:1381}
pke6dbb90af2016-07-08 14:00:4682
treib62e819e2016-09-27 11:47:3483ContentSuggestionsService::~ContentSuggestionsService() = default;
pke6dbb90af2016-07-08 14:00:4684
85void ContentSuggestionsService::Shutdown() {
jkrcal093410c2016-12-21 16:13:5586 remote_suggestions_provider_ = nullptr;
87 remote_suggestions_scheduler_ = nullptr;
pke5728f082016-08-03 17:27:3588 suggestions_by_category_.clear();
89 providers_by_category_.clear();
90 categories_.clear();
91 providers_.clear();
pke6dbb90af2016-07-08 14:00:4692 state_ = State::DISABLED;
vitaliii4408d6c2016-11-21 14:16:2493 for (Observer& observer : observers_) {
ericwilligers42b92c12016-10-24 20:21:1394 observer.ContentSuggestionsServiceShutdown();
vitaliii4408d6c2016-11-21 14:16:2495 }
pke6dbb90af2016-07-08 14:00:4696}
97
dgn52914722016-10-18 10:28:4298// static
99void ContentSuggestionsService::RegisterProfilePrefs(
100 PrefRegistrySimple* registry) {
101 registry->RegisterListPref(prefs::kDismissedCategories);
102}
103
vitaliii8b5ab282016-12-20 11:06:22104std::vector<Category> ContentSuggestionsService::GetCategories() const {
105 std::vector<Category> sorted_categories = categories_;
106 std::sort(sorted_categories.begin(), sorted_categories.end(),
107 [this](const Category& left, const Category& right) {
108 return category_ranker_->Compare(left, right);
109 });
110 return sorted_categories;
111}
112
pke9c5095ac2016-08-01 13:53:12113CategoryStatus ContentSuggestionsService::GetCategoryStatus(
114 Category category) const {
pke6dbb90af2016-07-08 14:00:46115 if (state_ == State::DISABLED) {
pke9c5095ac2016-08-01 13:53:12116 return CategoryStatus::ALL_SUGGESTIONS_EXPLICITLY_DISABLED;
pke6dbb90af2016-07-08 14:00:46117 }
118
pke4d3a4d62016-08-02 09:06:21119 auto iterator = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24120 if (iterator == providers_by_category_.end()) {
pke9c5095ac2016-08-01 13:53:12121 return CategoryStatus::NOT_PROVIDED;
vitaliii4408d6c2016-11-21 14:16:24122 }
pke6dbb90af2016-07-08 14:00:46123
124 return iterator->second->GetCategoryStatus(category);
125}
126
pkebd2f650a2016-08-09 14:53:45127base::Optional<CategoryInfo> ContentSuggestionsService::GetCategoryInfo(
128 Category category) const {
129 auto iterator = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24130 if (iterator == providers_by_category_.end()) {
pkebd2f650a2016-08-09 14:53:45131 return base::Optional<CategoryInfo>();
vitaliii4408d6c2016-11-21 14:16:24132 }
pkebd2f650a2016-08-09 14:53:45133 return iterator->second->GetCategoryInfo(category);
134}
135
pke6dbb90af2016-07-08 14:00:46136const std::vector<ContentSuggestion>&
pke9c5095ac2016-08-01 13:53:12137ContentSuggestionsService::GetSuggestionsForCategory(Category category) const {
pke6dbb90af2016-07-08 14:00:46138 auto iterator = suggestions_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24139 if (iterator == suggestions_by_category_.end()) {
pke6dbb90af2016-07-08 14:00:46140 return no_suggestions_;
vitaliii4408d6c2016-11-21 14:16:24141 }
pke6dbb90af2016-07-08 14:00:46142 return iterator->second;
143}
144
145void ContentSuggestionsService::FetchSuggestionImage(
treib4bbc54922016-09-28 17:26:44146 const ContentSuggestion::ID& suggestion_id,
pke6dbb90af2016-07-08 14:00:46147 const ImageFetchedCallback& callback) {
treib4bbc54922016-09-28 17:26:44148 if (!providers_by_category_.count(suggestion_id.category())) {
pke6dbb90af2016-07-08 14:00:46149 LOG(WARNING) << "Requested image for suggestion " << suggestion_id
treib4bbc54922016-09-28 17:26:44150 << " for unavailable category " << suggestion_id.category();
pke1da90602016-08-05 14:20:27151 base::ThreadTaskRunnerHandle::Get()->PostTask(
pkef29505d2016-08-26 14:46:34152 FROM_HERE, base::Bind(callback, gfx::Image()));
pke6dbb90af2016-07-08 14:00:46153 return;
154 }
treib4bbc54922016-09-28 17:26:44155 providers_by_category_[suggestion_id.category()]->FetchSuggestionImage(
156 suggestion_id, callback);
pke6dbb90af2016-07-08 14:00:46157}
158
jkrcalcd011682017-04-13 05:41:16159// TODO(jkrcal): Split the favicon fetching into a separate class.
jkrcal10004602017-03-29 07:44:28160void ContentSuggestionsService::FetchSuggestionFavicon(
161 const ContentSuggestion::ID& suggestion_id,
162 int minimum_size_in_pixel,
163 int desired_size_in_pixel,
164 const ImageFetchedCallback& callback) {
jkrcal8083bc52017-04-24 16:34:02165 const GURL& domain_with_favicon = GetFaviconDomain(suggestion_id);
166 if (!domain_with_favicon.is_valid() || !large_icon_service_) {
jkrcal7b7e71f12017-04-06 13:12:40167 base::ThreadTaskRunnerHandle::Get()->PostTask(
168 FROM_HERE, base::Bind(callback, gfx::Image()));
jkrcal08d79b52017-04-13 14:02:11169 RecordFaviconFetchResult(FaviconFetchResult::FAILURE);
jkrcal7b7e71f12017-04-06 13:12:40170 return;
171 }
172
jkrcal7b7e71f12017-04-06 13:12:40173 // TODO(jkrcal): Create a general wrapper function in LargeIconService that
174 // does handle the get-from-cache-and-fallback-to-google-server functionality
175 // in one shot (for all clients that do not need to react in between).
176 large_icon_service_->GetLargeIconImageOrFallbackStyle(
jkrcalcd011682017-04-13 05:41:16177 domain_with_favicon, minimum_size_in_pixel, desired_size_in_pixel,
jkrcal7b7e71f12017-04-06 13:12:40178 base::Bind(&ContentSuggestionsService::OnGetFaviconFromCacheFinished,
jkrcalcd011682017-04-13 05:41:16179 base::Unretained(this), domain_with_favicon,
180 minimum_size_in_pixel, desired_size_in_pixel, callback,
jkrcal7b7e71f12017-04-06 13:12:40181 /*continue_to_google_server=*/true),
182 &favicons_task_tracker_);
183}
184
jkrcal8083bc52017-04-24 16:34:02185GURL ContentSuggestionsService::GetFaviconDomain(
186 const ContentSuggestion::ID& suggestion_id) {
187 const std::vector<ContentSuggestion>& suggestions =
188 suggestions_by_category_[suggestion_id.category()];
189 auto position =
190 std::find_if(suggestions.begin(), suggestions.end(),
191 [&suggestion_id](const ContentSuggestion& suggestion) {
192 return suggestion_id == suggestion.id();
193 });
194 if (position != suggestions.end()) {
195 return position->url_with_favicon();
196 }
197
198 // Look up the URL in the archive of |remote_suggestions_provider_|.
199 // TODO(jkrcal): Fix how Fetch more works or find other ways to remove this
200 // hack. crbug.com/714031
201 if (providers_by_category_[suggestion_id.category()] ==
202 remote_suggestions_provider_) {
203 return remote_suggestions_provider_->GetUrlWithFavicon(suggestion_id);
204 }
205 return GURL();
206}
207
jkrcal7b7e71f12017-04-06 13:12:40208void ContentSuggestionsService::OnGetFaviconFromCacheFinished(
209 const GURL& publisher_url,
210 int minimum_size_in_pixel,
211 int desired_size_in_pixel,
212 const ImageFetchedCallback& callback,
213 bool continue_to_google_server,
214 const favicon_base::LargeIconImageResult& result) {
215 if (!result.image.IsEmpty()) {
216 callback.Run(result.image);
jkrcal08d79b52017-04-13 14:02:11217 // The icon is from cache if we haven't gone to Google server yet. The icon
218 // is freshly fetched, otherwise.
219 RecordFaviconFetchResult(continue_to_google_server
220 ? FaviconFetchResult::SUCCESS_CACHED
221 : FaviconFetchResult::SUCCESS_FETCHED);
jkrcal7b7e71f12017-04-06 13:12:40222 return;
223 }
224
225 if (!continue_to_google_server ||
226 (result.fallback_icon_style &&
227 !result.fallback_icon_style->is_default_background_color)) {
228 // We cannot download from the server if there is some small icon in the
jkrcal08d79b52017-04-13 14:02:11229 // cache (resulting in non-default background color) or if we already did
230 // so.
jkrcal7b7e71f12017-04-06 13:12:40231 callback.Run(gfx::Image());
jkrcal08d79b52017-04-13 14:02:11232 RecordFaviconFetchResult(FaviconFetchResult::FAILURE);
jkrcal7b7e71f12017-04-06 13:12:40233 return;
234 }
235
236 // Try to fetch the favicon from a Google favicon server.
237 large_icon_service_
238 ->GetLargeIconOrFallbackStyleFromGoogleServerSkippingLocalCache(
jkrcalff2ef682017-05-09 07:22:05239 publisher_url, minimum_size_in_pixel, desired_size_in_pixel,
jkrcal7b7e71f12017-04-06 13:12:40240 base::Bind(
241 &ContentSuggestionsService::OnGetFaviconFromGoogleServerFinished,
242 base::Unretained(this), publisher_url, minimum_size_in_pixel,
243 desired_size_in_pixel, callback));
244}
245
246void ContentSuggestionsService::OnGetFaviconFromGoogleServerFinished(
247 const GURL& publisher_url,
248 int minimum_size_in_pixel,
249 int desired_size_in_pixel,
250 const ImageFetchedCallback& callback,
251 bool success) {
252 if (!success) {
253 callback.Run(gfx::Image());
jkrcal08d79b52017-04-13 14:02:11254 RecordFaviconFetchResult(FaviconFetchResult::FAILURE);
jkrcal7b7e71f12017-04-06 13:12:40255 return;
256 }
257
258 // Get the freshly downloaded icon from the cache.
259 large_icon_service_->GetLargeIconImageOrFallbackStyle(
260 publisher_url, minimum_size_in_pixel, desired_size_in_pixel,
261 base::Bind(&ContentSuggestionsService::OnGetFaviconFromCacheFinished,
262 base::Unretained(this), publisher_url, minimum_size_in_pixel,
263 desired_size_in_pixel, callback,
264 /*continue_to_google_server=*/false),
265 &favicons_task_tracker_);
jkrcal10004602017-03-29 07:44:28266}
267
vitaliii685fdfaa2016-08-31 11:25:46268void ContentSuggestionsService::ClearHistory(
269 base::Time begin,
270 base::Time end,
271 const base::Callback<bool(const GURL& url)>& filter) {
272 for (const auto& provider : providers_) {
273 provider->ClearHistory(begin, end, filter);
274 }
vitaliii6343b2c2017-01-04 07:57:11275 category_ranker_->ClearHistory(begin, end);
tschumann5829c3412017-01-09 21:45:43276 // This potentially removed personalized data which we shouldn't display
277 // anymore.
278 for (Observer& observer : observers_) {
279 observer.OnFullRefreshRequired();
280 }
vitaliii685fdfaa2016-08-31 11:25:46281}
282
treib7d1d7a52016-08-24 14:04:55283void ContentSuggestionsService::ClearAllCachedSuggestions() {
pke6dbb90af2016-07-08 14:00:46284 suggestions_by_category_.clear();
pke151b5502016-08-09 12:15:13285 for (const auto& category_provider_pair : providers_by_category_) {
treib7d1d7a52016-08-24 14:04:55286 category_provider_pair.second->ClearCachedSuggestions(
pke151b5502016-08-09 12:15:13287 category_provider_pair.first);
vitaliii4408d6c2016-11-21 14:16:24288 for (Observer& observer : observers_) {
ericwilligers42b92c12016-10-24 20:21:13289 observer.OnNewSuggestions(category_provider_pair.first);
vitaliii4408d6c2016-11-21 14:16:24290 }
pke6dbb90af2016-07-08 14:00:46291 }
pke6dbb90af2016-07-08 14:00:46292}
293
jkrcal928ed3f2016-09-23 18:47:06294void ContentSuggestionsService::ClearCachedSuggestions(Category category) {
pke151b5502016-08-09 12:15:13295 suggestions_by_category_[category].clear();
296 auto iterator = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24297 if (iterator != providers_by_category_.end()) {
treib7d1d7a52016-08-24 14:04:55298 iterator->second->ClearCachedSuggestions(category);
vitaliii4408d6c2016-11-21 14:16:24299 }
pke151b5502016-08-09 12:15:13300}
301
pkede0dd9f2016-08-23 09:18:11302void ContentSuggestionsService::GetDismissedSuggestionsForDebugging(
303 Category category,
304 const DismissedSuggestionsCallback& callback) {
pke151b5502016-08-09 12:15:13305 auto iterator = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24306 if (iterator != providers_by_category_.end()) {
pkede0dd9f2016-08-23 09:18:11307 iterator->second->GetDismissedSuggestionsForDebugging(category, callback);
vitaliii4408d6c2016-11-21 14:16:24308 } else {
pkede0dd9f2016-08-23 09:18:11309 callback.Run(std::vector<ContentSuggestion>());
vitaliii4408d6c2016-11-21 14:16:24310 }
pke151b5502016-08-09 12:15:13311}
312
313void ContentSuggestionsService::ClearDismissedSuggestionsForDebugging(
314 Category category) {
315 auto iterator = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24316 if (iterator != providers_by_category_.end()) {
pke151b5502016-08-09 12:15:13317 iterator->second->ClearDismissedSuggestionsForDebugging(category);
vitaliii4408d6c2016-11-21 14:16:24318 }
pke6dbb90af2016-07-08 14:00:46319}
320
pke2646c95b2016-07-25 12:18:44321void ContentSuggestionsService::DismissSuggestion(
treib4bbc54922016-09-28 17:26:44322 const ContentSuggestion::ID& suggestion_id) {
323 if (!providers_by_category_.count(suggestion_id.category())) {
pke2646c95b2016-07-25 12:18:44324 LOG(WARNING) << "Dismissed suggestion " << suggestion_id
treib4bbc54922016-09-28 17:26:44325 << " for unavailable category " << suggestion_id.category();
pke6dbb90af2016-07-08 14:00:46326 return;
327 }
dgnf6500c12017-05-09 17:05:31328
329 metrics::RecordContentSuggestionDismissed();
330
treib4bbc54922016-09-28 17:26:44331 providers_by_category_[suggestion_id.category()]->DismissSuggestion(
332 suggestion_id);
pke6dbb90af2016-07-08 14:00:46333
vitaliii2600e8e02016-12-09 17:23:36334 // Remove the suggestion locally if it is present. A suggestion may be missing
335 // localy e.g. if it was sent to UI through |Fetch| or it has been dismissed
336 // from a different NTP.
337 RemoveSuggestionByID(suggestion_id);
pke6dbb90af2016-07-08 14:00:46338}
339
dgn212feea3b2016-09-16 15:08:20340void ContentSuggestionsService::DismissCategory(Category category) {
341 auto providers_it = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24342 if (providers_it == providers_by_category_.end()) {
dgn212feea3b2016-09-16 15:08:20343 return;
vitaliii4408d6c2016-11-21 14:16:24344 }
dgn212feea3b2016-09-16 15:08:20345
dgnf6500c12017-05-09 17:05:31346 metrics::RecordCategoryDismissed();
347
dgn52914722016-10-18 10:28:42348 ContentSuggestionsProvider* provider = providers_it->second;
349 UnregisterCategory(category, provider);
350
351 dismissed_providers_by_category_[category] = provider;
352 StoreDismissedCategoriesToPrefs();
vitaliii3d9423e2017-01-03 17:08:13353
354 category_ranker_->OnCategoryDismissed(category);
dgn212feea3b2016-09-16 15:08:20355}
356
mvanouwerkerk52783d92016-10-12 11:03:40357void ContentSuggestionsService::RestoreDismissedCategories() {
358 // Make a copy as the original will be modified during iteration.
359 auto dismissed_providers_by_category_copy = dismissed_providers_by_category_;
360 for (const auto& category_provider_pair :
361 dismissed_providers_by_category_copy) {
dgn52914722016-10-18 10:28:42362 RestoreDismissedCategory(category_provider_pair.first);
mvanouwerkerk52783d92016-10-12 11:03:40363 }
dgn52914722016-10-18 10:28:42364 StoreDismissedCategoriesToPrefs();
mvanouwerkerk52783d92016-10-12 11:03:40365 DCHECK(dismissed_providers_by_category_.empty());
366}
367
pke6dbb90af2016-07-08 14:00:46368void ContentSuggestionsService::AddObserver(Observer* observer) {
369 observers_.AddObserver(observer);
370}
371
372void ContentSuggestionsService::RemoveObserver(Observer* observer) {
373 observers_.RemoveObserver(observer);
374}
375
376void ContentSuggestionsService::RegisterProvider(
pke5728f082016-08-03 17:27:35377 std::unique_ptr<ContentSuggestionsProvider> provider) {
378 DCHECK(state_ == State::ENABLED);
pke5728f082016-08-03 17:27:35379 providers_.push_back(std::move(provider));
pke6dbb90af2016-07-08 14:00:46380}
381
tschumann83578aa2016-11-03 13:18:32382void ContentSuggestionsService::Fetch(
383 const Category& category,
384 const std::set<std::string>& known_suggestion_ids,
fhorschigd24046c42016-11-09 11:26:25385 const FetchDoneCallback& callback) {
tschumann83578aa2016-11-03 13:18:32386 auto providers_it = providers_by_category_.find(category);
vitaliii4408d6c2016-11-21 14:16:24387 if (providers_it == providers_by_category_.end()) {
tschumann83578aa2016-11-03 13:18:32388 return;
vitaliii4408d6c2016-11-21 14:16:24389 }
tschumann83578aa2016-11-03 13:18:32390
dgnf6500c12017-05-09 17:05:31391 metrics::RecordFetchAction();
392
tschumann83578aa2016-11-03 13:18:32393 providers_it->second->Fetch(category, known_suggestion_ids, callback);
394}
395
jkrcal093410c2016-12-21 16:13:55396void ContentSuggestionsService::ReloadSuggestions() {
397 for (const auto& provider : providers_) {
398 provider->ReloadSuggestions();
399 }
400}
401
dgn65d6cd82017-04-11 00:36:36402void ContentSuggestionsService::SetRemoteSuggestionsEnabled(bool enabled) {
dgnb8a8a5c2017-03-31 12:35:36403 pref_service_->SetBoolean(prefs::kEnableSnippets, enabled);
404}
405
dgn65d6cd82017-04-11 00:36:36406bool ContentSuggestionsService::AreRemoteSuggestionsEnabled() const {
dgnb8a8a5c2017-03-31 12:35:36407 return pref_service_->GetBoolean(prefs::kEnableSnippets);
408}
409
dgn65d6cd82017-04-11 00:36:36410bool ContentSuggestionsService::AreRemoteSuggestionsManaged() const {
dgnb8a8a5c2017-03-31 12:35:36411 return pref_service_->IsManagedPreference(prefs::kEnableSnippets);
412}
413
dgn65d6cd82017-04-11 00:36:36414bool ContentSuggestionsService::AreRemoteSuggestionsManagedByCustodian() const {
dgnb8a8a5c2017-03-31 12:35:36415 return pref_service_->IsPreferenceManagedByCustodian(prefs::kEnableSnippets);
416}
417
pke6dbb90af2016-07-08 14:00:46418////////////////////////////////////////////////////////////////////////////////
419// Private methods
420
421void ContentSuggestionsService::OnNewSuggestions(
pke4d3a4d62016-08-02 09:06:21422 ContentSuggestionsProvider* provider,
423 Category category,
treib62e819e2016-09-27 11:47:34424 std::vector<ContentSuggestion> suggestions) {
treib534523b2016-10-20 16:19:44425 // Providers shouldn't call this when they're in a non-available state.
426 DCHECK(
427 IsCategoryStatusInitOrAvailable(provider->GetCategoryStatus(category)));
428
dgn52914722016-10-18 10:28:42429 if (TryRegisterProviderForCategory(provider, category)) {
pke4d3a4d62016-08-02 09:06:21430 NotifyCategoryStatusChanged(category);
dgn52914722016-10-18 10:28:42431 } else if (IsCategoryDismissed(category)) {
432 // The category has been registered as a dismissed one. We need to
433 // check if the dismissal can be cleared now that we received new data.
vitaliii4408d6c2016-11-21 14:16:24434 if (suggestions.empty()) {
dgn52914722016-10-18 10:28:42435 return;
vitaliii4408d6c2016-11-21 14:16:24436 }
dgn52914722016-10-18 10:28:42437
438 RestoreDismissedCategory(category);
439 StoreDismissedCategoriesToPrefs();
440
441 NotifyCategoryStatusChanged(category);
442 }
pke3b2e3632016-08-12 12:52:53443
treib39fffa12016-10-14 14:59:10444 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) {
445 // A provider shouldn't send us suggestions while it's not available.
446 DCHECK(suggestions.empty());
pke3b2e3632016-08-12 12:52:53447 return;
treib39fffa12016-10-14 14:59:10448 }
pke6dbb90af2016-07-08 14:00:46449
treib62e819e2016-09-27 11:47:34450 suggestions_by_category_[category] = std::move(suggestions);
pke6dbb90af2016-07-08 14:00:46451
vitaliii4408d6c2016-11-21 14:16:24452 for (Observer& observer : observers_) {
ericwilligers42b92c12016-10-24 20:21:13453 observer.OnNewSuggestions(category);
vitaliii4408d6c2016-11-21 14:16:24454 }
pke6dbb90af2016-07-08 14:00:46455}
456
457void ContentSuggestionsService::OnCategoryStatusChanged(
pke4d3a4d62016-08-02 09:06:21458 ContentSuggestionsProvider* provider,
459 Category category,
pke9c5095ac2016-08-01 13:53:12460 CategoryStatus new_status) {
pke4d3a4d62016-08-02 09:06:21461 if (new_status == CategoryStatus::NOT_PROVIDED) {
dgn52914722016-10-18 10:28:42462 UnregisterCategory(category, provider);
pke4d3a4d62016-08-02 09:06:21463 } else {
vitaliii4408d6c2016-11-21 14:16:24464 if (!IsCategoryStatusAvailable(new_status)) {
dgn52914722016-10-18 10:28:42465 suggestions_by_category_.erase(category);
vitaliii4408d6c2016-11-21 14:16:24466 }
dgn52914722016-10-18 10:28:42467 TryRegisterProviderForCategory(provider, category);
pke4d3a4d62016-08-02 09:06:21468 DCHECK_EQ(new_status, provider->GetCategoryStatus(category));
469 }
dgn52914722016-10-18 10:28:42470
vitaliii4408d6c2016-11-21 14:16:24471 if (!IsCategoryDismissed(category)) {
dgn52914722016-10-18 10:28:42472 NotifyCategoryStatusChanged(category);
vitaliii4408d6c2016-11-21 14:16:24473 }
pke6dbb90af2016-07-08 14:00:46474}
475
pke2a48f852016-08-18 13:33:52476void ContentSuggestionsService::OnSuggestionInvalidated(
477 ContentSuggestionsProvider* provider,
treib4bbc54922016-09-28 17:26:44478 const ContentSuggestion::ID& suggestion_id) {
479 RemoveSuggestionByID(suggestion_id);
vitaliii4408d6c2016-11-21 14:16:24480 for (Observer& observer : observers_) {
ericwilligers42b92c12016-10-24 20:21:13481 observer.OnSuggestionInvalidated(suggestion_id);
vitaliii4408d6c2016-11-21 14:16:24482 }
pke2a48f852016-08-18 13:33:52483}
484
dgnf5708892016-11-22 10:36:22485// SigninManagerBase::Observer implementation
486void ContentSuggestionsService::GoogleSigninSucceeded(
487 const std::string& account_id,
488 const std::string& username,
489 const std::string& password) {
490 OnSignInStateChanged();
491}
492
493void ContentSuggestionsService::GoogleSignedOut(const std::string& account_id,
494 const std::string& username) {
495 OnSignInStateChanged();
496}
497
vitaliii45941152016-09-05 08:58:13498// history::HistoryServiceObserver implementation.
499void ContentSuggestionsService::OnURLsDeleted(
500 history::HistoryService* history_service,
501 bool all_history,
502 bool expired,
503 const history::URLRows& deleted_rows,
504 const std::set<GURL>& favicon_urls) {
505 // We don't care about expired entries.
vitaliii4408d6c2016-11-21 14:16:24506 if (expired) {
vitaliii45941152016-09-05 08:58:13507 return;
vitaliii4408d6c2016-11-21 14:16:24508 }
vitaliii45941152016-09-05 08:58:13509
vitaliii45941152016-09-05 08:58:13510 if (all_history) {
vitaliii45941152016-09-05 08:58:13511 base::Callback<bool(const GURL& url)> filter =
512 base::Bind([](const GURL& url) { return true; });
tschumann5829c3412017-01-09 21:45:43513 ClearHistory(base::Time(), base::Time::Max(), filter);
vitaliii45941152016-09-05 08:58:13514 } else {
tschumann5829c3412017-01-09 21:45:43515 // If a user deletes a single URL, we don't consider this a clear user
516 // intend to clear our data.
517 // TODO(tschumann): Single URL deletions should be handled on a case-by-case
518 // basis. However this depends on the provider's details and thus cannot be
519 // done here. Introduce a OnURLsDeleted() method on the providers to move
520 // this decision further down.
521 if (deleted_rows.size() < 2) {
vitaliii45941152016-09-05 08:58:13522 return;
vitaliii4408d6c2016-11-21 14:16:24523 }
vitaliii45941152016-09-05 08:58:13524 std::set<GURL> deleted_urls;
525 for (const history::URLRow& row : deleted_rows) {
vitaliii45941152016-09-05 08:58:13526 deleted_urls.insert(row.url());
527 }
sfierae8969bc2017-03-26 18:38:41528 base::Callback<bool(const GURL& url)> filter =
529 base::Bind([](const std::set<GURL>& set,
530 const GURL& url) { return set.count(url) != 0; },
531 deleted_urls);
tschumann5829c3412017-01-09 21:45:43532 // We usually don't have any time-related information (the URLRow objects
533 // usually don't provide a |last_visit()| timestamp. Hence we simply clear
534 // the whole history for the selected URLs.
535 ClearHistory(base::Time(), base::Time::Max(), filter);
vitaliii45941152016-09-05 08:58:13536 }
537}
538
539void ContentSuggestionsService::HistoryServiceBeingDeleted(
540 history::HistoryService* history_service) {
541 history_service_observer_.RemoveAll();
542}
543
dgn52914722016-10-18 10:28:42544bool ContentSuggestionsService::TryRegisterProviderForCategory(
pke4d3a4d62016-08-02 09:06:21545 ContentSuggestionsProvider* provider,
546 Category category) {
547 auto it = providers_by_category_.find(category);
548 if (it != providers_by_category_.end()) {
549 DCHECK_EQ(it->second, provider);
550 return false;
551 }
552
mvanouwerkerk52783d92016-10-12 11:03:40553 auto dismissed_it = dismissed_providers_by_category_.find(category);
554 if (dismissed_it != dismissed_providers_by_category_.end()) {
dgn52914722016-10-18 10:28:42555 // The initialisation of dismissed categories registers them with |nullptr|
556 // for providers, we need to check for that to see if the provider is
557 // already registered or not.
558 if (!dismissed_it->second) {
559 dismissed_it->second = provider;
560 } else {
561 DCHECK_EQ(dismissed_it->second, provider);
562 }
563 return false;
mvanouwerkerk52783d92016-10-12 11:03:40564 }
565
dgn52914722016-10-18 10:28:42566 RegisterCategory(category, provider);
567 return true;
568}
569
570void ContentSuggestionsService::RegisterCategory(
571 Category category,
572 ContentSuggestionsProvider* provider) {
573 DCHECK(!base::ContainsKey(providers_by_category_, category));
574 DCHECK(!IsCategoryDismissed(category));
575
pke4d3a4d62016-08-02 09:06:21576 providers_by_category_[category] = provider;
577 categories_.push_back(category);
pke4d3a4d62016-08-02 09:06:21578 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) {
579 suggestions_by_category_.insert(
580 std::make_pair(category, std::vector<ContentSuggestion>()));
581 }
dgn52914722016-10-18 10:28:42582}
583
584void ContentSuggestionsService::UnregisterCategory(
585 Category category,
586 ContentSuggestionsProvider* provider) {
587 auto providers_it = providers_by_category_.find(category);
588 if (providers_it == providers_by_category_.end()) {
589 DCHECK(IsCategoryDismissed(category));
590 return;
591 }
592
593 DCHECK_EQ(provider, providers_it->second);
594 providers_by_category_.erase(providers_it);
595 categories_.erase(
596 std::find(categories_.begin(), categories_.end(), category));
597 suggestions_by_category_.erase(category);
pke6dbb90af2016-07-08 14:00:46598}
599
pke2a48f852016-08-18 13:33:52600bool ContentSuggestionsService::RemoveSuggestionByID(
treib4bbc54922016-09-28 17:26:44601 const ContentSuggestion::ID& suggestion_id) {
pke2a48f852016-08-18 13:33:52602 std::vector<ContentSuggestion>* suggestions =
treib4bbc54922016-09-28 17:26:44603 &suggestions_by_category_[suggestion_id.category()];
pke2a48f852016-08-18 13:33:52604 auto position =
605 std::find_if(suggestions->begin(), suggestions->end(),
606 [&suggestion_id](const ContentSuggestion& suggestion) {
607 return suggestion_id == suggestion.id();
608 });
vitaliii4408d6c2016-11-21 14:16:24609 if (position == suggestions->end()) {
pke2a48f852016-08-18 13:33:52610 return false;
vitaliii4408d6c2016-11-21 14:16:24611 }
pke2a48f852016-08-18 13:33:52612 suggestions->erase(position);
treib063e6a62016-08-25 11:34:29613
pke2a48f852016-08-18 13:33:52614 return true;
615}
616
pke9c5095ac2016-08-01 13:53:12617void ContentSuggestionsService::NotifyCategoryStatusChanged(Category category) {
vitaliii4408d6c2016-11-21 14:16:24618 for (Observer& observer : observers_) {
ericwilligers42b92c12016-10-24 20:21:13619 observer.OnCategoryStatusChanged(category, GetCategoryStatus(category));
vitaliii4408d6c2016-11-21 14:16:24620 }
pke6dbb90af2016-07-08 14:00:46621}
622
dgnf5708892016-11-22 10:36:22623void ContentSuggestionsService::OnSignInStateChanged() {
624 // First notify the providers, so they can make the required changes.
625 for (const auto& provider : providers_) {
626 provider->OnSignInStateChanged();
627 }
628
629 // Finally notify the observers so they refresh only after the backend is
630 // ready.
631 for (Observer& observer : observers_) {
632 observer.OnFullRefreshRequired();
633 }
634}
635
dgn52914722016-10-18 10:28:42636bool ContentSuggestionsService::IsCategoryDismissed(Category category) const {
637 return base::ContainsKey(dismissed_providers_by_category_, category);
638}
639
640void ContentSuggestionsService::RestoreDismissedCategory(Category category) {
641 auto dismissed_it = dismissed_providers_by_category_.find(category);
642 DCHECK(base::ContainsKey(dismissed_providers_by_category_, category));
643
644 // Keep the reference to the provider and remove it from the dismissed ones,
645 // because the category registration enforces that it's not dismissed.
646 ContentSuggestionsProvider* provider = dismissed_it->second;
647 dismissed_providers_by_category_.erase(dismissed_it);
648
vitaliii4408d6c2016-11-21 14:16:24649 if (provider) {
dgn52914722016-10-18 10:28:42650 RegisterCategory(category, provider);
vitaliii4408d6c2016-11-21 14:16:24651 }
dgn52914722016-10-18 10:28:42652}
653
654void ContentSuggestionsService::RestoreDismissedCategoriesFromPrefs() {
655 // This must only be called at startup.
656 DCHECK(dismissed_providers_by_category_.empty());
657 DCHECK(providers_by_category_.empty());
658
659 const base::ListValue* list =
660 pref_service_->GetList(prefs::kDismissedCategories);
jdoerriea5676c62017-04-11 18:09:14661 for (const base::Value& entry : *list) {
dgn52914722016-10-18 10:28:42662 int id = 0;
jdoerriea5676c62017-04-11 18:09:14663 if (!entry.GetAsInteger(&id)) {
664 DLOG(WARNING) << "Invalid category pref value: " << entry;
dgn52914722016-10-18 10:28:42665 continue;
666 }
667
668 // When the provider is registered, it will be stored in this map.
vitaliii7456f5a2016-12-19 11:13:25669 dismissed_providers_by_category_[Category::FromIDValue(id)] = nullptr;
dgn52914722016-10-18 10:28:42670 }
671}
672
673void ContentSuggestionsService::StoreDismissedCategoriesToPrefs() {
674 base::ListValue list;
675 for (const auto& category_provider_pair : dismissed_providers_by_category_) {
676 list.AppendInteger(category_provider_pair.first.id());
677 }
678
679 pref_service_->Set(prefs::kDismissedCategories, list);
680}
681
pke6dbb90af2016-07-08 14:00:46682} // namespace ntp_snippets