Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(252)

Side by Side Diff: components/ntp_snippets/content_suggestions_service.cc

Issue 2406573002: 📰 Persist category dismissals (Closed)
Patch Set: fix test Created 4 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2016 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/content_suggestions_service.h" 5 #include "components/ntp_snippets/content_suggestions_service.h"
6 6
7 #include <algorithm> 7 #include <algorithm>
8 #include <iterator> 8 #include <iterator>
9 #include <set> 9 #include <set>
10 #include <utility> 10 #include <utility>
11 11
12 #include "base/bind.h" 12 #include "base/bind.h"
13 #include "base/location.h" 13 #include "base/location.h"
14 #include "base/strings/string_number_conversions.h" 14 #include "base/strings/string_number_conversions.h"
15 #include "base/threading/thread_task_runner_handle.h" 15 #include "base/threading/thread_task_runner_handle.h"
16 #include "base/values.h"
17 #include "components/ntp_snippets/pref_names.h"
18 #include "components/prefs/pref_registry_simple.h"
16 #include "ui/gfx/image/image.h" 19 #include "ui/gfx/image/image.h"
17 20
18 namespace ntp_snippets { 21 namespace ntp_snippets {
19 22
20 ContentSuggestionsService::ContentSuggestionsService( 23 ContentSuggestionsService::ContentSuggestionsService(
21 State state, 24 State state,
22 history::HistoryService* history_service, 25 history::HistoryService* history_service,
23 PrefService* pref_service) 26 PrefService* pref_service)
24 : state_(state), 27 : state_(state),
25 history_service_observer_(this), 28 history_service_observer_(this),
26 ntp_snippets_service_(nullptr), 29 ntp_snippets_service_(nullptr),
30 pref_service_(pref_service),
27 user_classifier_(pref_service) { 31 user_classifier_(pref_service) {
28 // Can be null in tests. 32 // Can be null in tests.
29 if (history_service) 33 if (history_service)
30 history_service_observer_.Add(history_service); 34 history_service_observer_.Add(history_service);
35
36 RestoreDismissedCategoriesFromPrefs();
31 } 37 }
32 38
33 ContentSuggestionsService::~ContentSuggestionsService() = default; 39 ContentSuggestionsService::~ContentSuggestionsService() = default;
34 40
35 void ContentSuggestionsService::Shutdown() { 41 void ContentSuggestionsService::Shutdown() {
36 ntp_snippets_service_ = nullptr; 42 ntp_snippets_service_ = nullptr;
37 suggestions_by_category_.clear(); 43 suggestions_by_category_.clear();
38 providers_by_category_.clear(); 44 providers_by_category_.clear();
39 categories_.clear(); 45 categories_.clear();
40 providers_.clear(); 46 providers_.clear();
41 state_ = State::DISABLED; 47 state_ = State::DISABLED;
42 FOR_EACH_OBSERVER(Observer, observers_, ContentSuggestionsServiceShutdown()); 48 FOR_EACH_OBSERVER(Observer, observers_, ContentSuggestionsServiceShutdown());
43 } 49 }
44 50
51 // static
52 void ContentSuggestionsService::RegisterProfilePrefs(
53 PrefRegistrySimple* registry) {
54 registry->RegisterListPref(prefs::kDismissedCategories);
55 }
56
45 CategoryStatus ContentSuggestionsService::GetCategoryStatus( 57 CategoryStatus ContentSuggestionsService::GetCategoryStatus(
46 Category category) const { 58 Category category) const {
47 if (state_ == State::DISABLED) { 59 if (state_ == State::DISABLED)
48 return CategoryStatus::ALL_SUGGESTIONS_EXPLICITLY_DISABLED; 60 return CategoryStatus::ALL_SUGGESTIONS_EXPLICITLY_DISABLED;
49 } 61
62 if (IsCategoryDismissed(category))
63 return CategoryStatus::NOT_PROVIDED;
50 64
51 auto iterator = providers_by_category_.find(category); 65 auto iterator = providers_by_category_.find(category);
52 if (iterator == providers_by_category_.end()) 66 if (iterator == providers_by_category_.end())
53 return CategoryStatus::NOT_PROVIDED; 67 return CategoryStatus::NOT_PROVIDED;
54 68
55 return iterator->second->GetCategoryStatus(category); 69 return iterator->second->GetCategoryStatus(category);
56 } 70 }
57 71
58 base::Optional<CategoryInfo> ContentSuggestionsService::GetCategoryInfo( 72 base::Optional<CategoryInfo> ContentSuggestionsService::GetCategoryInfo(
59 Category category) const { 73 Category category) const {
74 if (IsCategoryDismissed(category))
75 return base::Optional<CategoryInfo>();
76
60 auto iterator = providers_by_category_.find(category); 77 auto iterator = providers_by_category_.find(category);
61 if (iterator == providers_by_category_.end()) 78 if (iterator == providers_by_category_.end())
62 return base::Optional<CategoryInfo>(); 79 return base::Optional<CategoryInfo>();
63 return iterator->second->GetCategoryInfo(category); 80 return iterator->second->GetCategoryInfo(category);
64 } 81 }
65 82
66 const std::vector<ContentSuggestion>& 83 const std::vector<ContentSuggestion>&
67 ContentSuggestionsService::GetSuggestionsForCategory(Category category) const { 84 ContentSuggestionsService::GetSuggestionsForCategory(Category category) const {
85 if (IsCategoryDismissed(category))
86 return no_suggestions_;
87
68 auto iterator = suggestions_by_category_.find(category); 88 auto iterator = suggestions_by_category_.find(category);
69 if (iterator == suggestions_by_category_.end()) 89 if (iterator == suggestions_by_category_.end())
70 return no_suggestions_; 90 return no_suggestions_;
71 return iterator->second; 91 return iterator->second;
72 } 92 }
73 93
74 void ContentSuggestionsService::FetchSuggestionImage( 94 void ContentSuggestionsService::FetchSuggestionImage(
75 const ContentSuggestion::ID& suggestion_id, 95 const ContentSuggestion::ID& suggestion_id,
76 const ImageFetchedCallback& callback) { 96 const ImageFetchedCallback& callback) {
77 if (!providers_by_category_.count(suggestion_id.category())) { 97 if (!providers_by_category_.count(suggestion_id.category())) {
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after
143 DCHECK(removed) << "The dismissed suggestion " << suggestion_id 163 DCHECK(removed) << "The dismissed suggestion " << suggestion_id
144 << " has already been removed. Providers must not call" 164 << " has already been removed. Providers must not call"
145 << " OnNewSuggestions in response to DismissSuggestion."; 165 << " OnNewSuggestions in response to DismissSuggestion.";
146 } 166 }
147 167
148 void ContentSuggestionsService::DismissCategory(Category category) { 168 void ContentSuggestionsService::DismissCategory(Category category) {
149 auto providers_it = providers_by_category_.find(category); 169 auto providers_it = providers_by_category_.find(category);
150 if (providers_it == providers_by_category_.end()) 170 if (providers_it == providers_by_category_.end())
151 return; 171 return;
152 172
153 providers_by_category_.erase(providers_it); 173 dismissed_categories_.insert(category);
154 categories_.erase( 174 StoreDismissedCategoriesToPrefs();
155 std::find(categories_.begin(), categories_.end(), category));
156 } 175 }
157 176
158 void ContentSuggestionsService::AddObserver(Observer* observer) { 177 void ContentSuggestionsService::AddObserver(Observer* observer) {
159 observers_.AddObserver(observer); 178 observers_.AddObserver(observer);
160 } 179 }
161 180
162 void ContentSuggestionsService::RemoveObserver(Observer* observer) { 181 void ContentSuggestionsService::RemoveObserver(Observer* observer) {
163 observers_.RemoveObserver(observer); 182 observers_.RemoveObserver(observer);
164 } 183 }
165 184
166 void ContentSuggestionsService::RegisterProvider( 185 void ContentSuggestionsService::RegisterProvider(
167 std::unique_ptr<ContentSuggestionsProvider> provider) { 186 std::unique_ptr<ContentSuggestionsProvider> provider) {
168 DCHECK(state_ == State::ENABLED); 187 DCHECK(state_ == State::ENABLED);
169 providers_.push_back(std::move(provider)); 188 providers_.push_back(std::move(provider));
170 } 189 }
171 190
172 //////////////////////////////////////////////////////////////////////////////// 191 ////////////////////////////////////////////////////////////////////////////////
173 // Private methods 192 // Private methods
174 193
175 void ContentSuggestionsService::OnNewSuggestions( 194 void ContentSuggestionsService::OnNewSuggestions(
176 ContentSuggestionsProvider* provider, 195 ContentSuggestionsProvider* provider,
177 Category category, 196 Category category,
178 std::vector<ContentSuggestion> suggestions) { 197 std::vector<ContentSuggestion> suggestions) {
179 if (RegisterCategoryIfRequired(provider, category)) 198 if (!suggestions.empty()) {
180 NotifyCategoryStatusChanged(category); 199 dismissed_categories_.erase(category);
200 StoreDismissedCategoriesToPrefs();
201 }
181 202
182 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) 203 OnNewSuggestionsInternal(provider, category, std::move(suggestions));
183 return; 204 }
184 205
185 suggestions_by_category_[category] = std::move(suggestions); 206 void ContentSuggestionsService::OnNewSuggestionBatch(
207 ContentSuggestionsProvider* provider,
208 SuggestionBatch suggestion_batch) {
209 // Update the dismissed suggestions. Remote suggestions are received at once
210 // from the server. Because of that, we always have the full list of valid
211 // remote categories according to the server. We remove invalid categories or
212 // the ones for which we got new suggestions to show.
213 for (auto it = dismissed_categories_.begin();
214 it != dismissed_categories_.end();) {
215 // Do nothing to the local sections, we only clean up the remote ones here.
216 if (!it->IsRemote()) {
217 ++it;
218 continue;
219 }
186 220
187 // The positioning of the bookmarks category depends on whether it's empty. 221 auto entry = suggestion_batch.find(*it);
188 // TODO(treib): Remove this temporary hack, crbug.com/640568.
189 if (category.IsKnownCategory(KnownCategories::BOOKMARKS))
190 SortCategories();
191 222
192 FOR_EACH_OBSERVER(Observer, observers_, OnNewSuggestions(category)); 223 // If the category is not included in the new bundle, it's not valid anymore
224 // and we can just remove it.
225 // TODO(dgn) Also remove all the related local data?
226 if (entry == suggestion_batch.end()) {
227 DVLOG(1) << "Dismissed category " << it->id()
228 << " unknown on the server.";
229 it = dismissed_categories_.erase(it);
230 continue;
231 }
232
233 // The category has new suggestions to show, it's not dismissed anymore.
234 if (!entry->second.empty()) {
235 DVLOG(1) << "Dismissed category " << it->id()
236 << " has new suggestions to show.";
237 it = dismissed_categories_.erase(it);
238 continue;
239 }
240
241 ++it;
242 }
243
244 StoreDismissedCategoriesToPrefs();
245
246 for (auto it = suggestion_batch.begin(); it != suggestion_batch.end(); ++it) {
247 OnNewSuggestionsInternal(provider, it->first, std::move(it->second));
248 }
193 } 249 }
194 250
195 void ContentSuggestionsService::OnCategoryStatusChanged( 251 void ContentSuggestionsService::OnCategoryStatusChanged(
196 ContentSuggestionsProvider* provider, 252 ContentSuggestionsProvider* provider,
197 Category category, 253 Category category,
198 CategoryStatus new_status) { 254 CategoryStatus new_status) {
199 if (!IsCategoryStatusAvailable(new_status)) { 255 if (!IsCategoryStatusAvailable(new_status)) {
200 suggestions_by_category_.erase(category); 256 suggestions_by_category_.erase(category);
201 } 257 }
202 if (new_status == CategoryStatus::NOT_PROVIDED) { 258 if (new_status == CategoryStatus::NOT_PROVIDED) {
203 DCHECK(providers_by_category_.find(category) != 259 auto providers_it = providers_by_category_.find(category);
204 providers_by_category_.end()); 260 DCHECK(providers_it != providers_by_category_.end());
205 DCHECK_EQ(provider, providers_by_category_.find(category)->second); 261 DCHECK_EQ(provider, providers_it->second);
206 DismissCategory(category); 262 providers_by_category_.erase(providers_it);
263 categories_.erase(
264 std::find(categories_.begin(), categories_.end(), category));
207 } else { 265 } else {
208 RegisterCategoryIfRequired(provider, category); 266 RegisterCategoryIfRequired(provider, category);
209 DCHECK_EQ(new_status, provider->GetCategoryStatus(category)); 267 DCHECK_EQ(new_status, provider->GetCategoryStatus(category));
210 } 268 }
211 NotifyCategoryStatusChanged(category); 269 NotifyCategoryStatusChanged(category);
212 } 270 }
213 271
214 void ContentSuggestionsService::OnSuggestionInvalidated( 272 void ContentSuggestionsService::OnSuggestionInvalidated(
215 ContentSuggestionsProvider* provider, 273 ContentSuggestionsProvider* provider,
216 const ContentSuggestion::ID& suggestion_id) { 274 const ContentSuggestion::ID& suggestion_id) {
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after
277 providers_by_category_[category] = provider; 335 providers_by_category_[category] = provider;
278 categories_.push_back(category); 336 categories_.push_back(category);
279 SortCategories(); 337 SortCategories();
280 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) { 338 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) {
281 suggestions_by_category_.insert( 339 suggestions_by_category_.insert(
282 std::make_pair(category, std::vector<ContentSuggestion>())); 340 std::make_pair(category, std::vector<ContentSuggestion>()));
283 } 341 }
284 return true; 342 return true;
285 } 343 }
286 344
345 void ContentSuggestionsService::OnNewSuggestionsInternal(
346 ContentSuggestionsProvider* provider,
347 Category category,
348 std::vector<ContentSuggestion> suggestions) {
349 // Ignore notifications concerning dismissed categories.
350 if (IsCategoryDismissed(category))
351 return;
352
353 if (RegisterCategoryIfRequired(provider, category))
354 NotifyCategoryStatusChanged(category);
355
356 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category)))
357 return;
358
359 suggestions_by_category_[category] = std::move(suggestions);
360
361 // The positioning of the bookmarks category depends on whether it's empty.
362 // TODO(treib): Remove this temporary hack, crbug.com/640568.
363 if (category.IsKnownCategory(KnownCategories::BOOKMARKS))
364 SortCategories();
365
366 FOR_EACH_OBSERVER(Observer, observers_, OnNewSuggestions(category));
367 }
368
287 bool ContentSuggestionsService::RemoveSuggestionByID( 369 bool ContentSuggestionsService::RemoveSuggestionByID(
288 const ContentSuggestion::ID& suggestion_id) { 370 const ContentSuggestion::ID& suggestion_id) {
289 std::vector<ContentSuggestion>* suggestions = 371 std::vector<ContentSuggestion>* suggestions =
290 &suggestions_by_category_[suggestion_id.category()]; 372 &suggestions_by_category_[suggestion_id.category()];
291 auto position = 373 auto position =
292 std::find_if(suggestions->begin(), suggestions->end(), 374 std::find_if(suggestions->begin(), suggestions->end(),
293 [&suggestion_id](const ContentSuggestion& suggestion) { 375 [&suggestion_id](const ContentSuggestion& suggestion) {
294 return suggestion_id == suggestion.id(); 376 return suggestion_id == suggestion.id();
295 }); 377 });
296 if (position == suggestions->end()) 378 if (position == suggestions->end())
297 return false; 379 return false;
298 suggestions->erase(position); 380 suggestions->erase(position);
299 381
300 // The positioning of the bookmarks category depends on whether it's empty. 382 // The positioning of the bookmarks category depends on whether it's empty.
301 // TODO(treib): Remove this temporary hack, crbug.com/640568. 383 // TODO(treib): Remove this temporary hack, crbug.com/640568.
302 if (suggestion_id.category().IsKnownCategory(KnownCategories::BOOKMARKS)) 384 if (suggestion_id.category().IsKnownCategory(KnownCategories::BOOKMARKS))
303 SortCategories(); 385 SortCategories();
304 386
305 return true; 387 return true;
306 } 388 }
307 389
308 void ContentSuggestionsService::NotifyCategoryStatusChanged(Category category) { 390 void ContentSuggestionsService::NotifyCategoryStatusChanged(Category category) {
391 // Ignore notifications concerning dismissed categories.
392 if (IsCategoryDismissed(category))
Marc Treib 2016/10/11 07:52:55 I thought changing the status should make the dism
dgn 2016/10/11 19:14:01 It currently works just by the fact that when stat
393 return;
394
309 FOR_EACH_OBSERVER( 395 FOR_EACH_OBSERVER(
310 Observer, observers_, 396 Observer, observers_,
311 OnCategoryStatusChanged(category, GetCategoryStatus(category))); 397 OnCategoryStatusChanged(category, GetCategoryStatus(category)));
312 } 398 }
313 399
400 bool ContentSuggestionsService::IsCategoryDismissed(Category category) const {
401 return dismissed_categories_.find(category) != dismissed_categories_.end();
402 }
403
314 void ContentSuggestionsService::SortCategories() { 404 void ContentSuggestionsService::SortCategories() {
315 auto it = suggestions_by_category_.find( 405 auto it = suggestions_by_category_.find(
316 category_factory_.FromKnownCategory(KnownCategories::BOOKMARKS)); 406 category_factory_.FromKnownCategory(KnownCategories::BOOKMARKS));
317 bool bookmarks_empty = 407 bool bookmarks_empty =
318 (it == suggestions_by_category_.end() || it->second.empty()); 408 (it == suggestions_by_category_.end() || it->second.empty());
319 std::sort( 409 std::sort(
320 categories_.begin(), categories_.end(), 410 categories_.begin(), categories_.end(),
321 [this, bookmarks_empty](const Category& left, const Category& right) { 411 [this, bookmarks_empty](const Category& left, const Category& right) {
322 // If the bookmarks section is empty, put it at the end. 412 // If the bookmarks section is empty, put it at the end.
323 // TODO(treib): This is a temporary hack, see crbug.com/640568. 413 // TODO(treib): This is a temporary hack, see crbug.com/640568.
324 if (bookmarks_empty) { 414 if (bookmarks_empty) {
325 if (left.IsKnownCategory(KnownCategories::BOOKMARKS)) 415 if (left.IsKnownCategory(KnownCategories::BOOKMARKS))
326 return false; 416 return false;
327 if (right.IsKnownCategory(KnownCategories::BOOKMARKS)) 417 if (right.IsKnownCategory(KnownCategories::BOOKMARKS))
328 return true; 418 return true;
329 } 419 }
330 return category_factory_.CompareCategories(left, right); 420 return category_factory_.CompareCategories(left, right);
331 }); 421 });
332 } 422 }
333 423
424 void ContentSuggestionsService::RestoreDismissedCategoriesFromPrefs() {
425 // This must only be called at startup.
426 DCHECK(dismissed_categories_.empty());
427
428 const base::ListValue* list =
429 pref_service_->GetList(prefs::kDismissedCategories);
430 for (const std::unique_ptr<base::Value>& entry : *list) {
431 int id = 0;
432 if (!entry->GetAsInteger(&id)) {
433 DLOG(WARNING) << "Invalid category pref value: " << *entry;
434 continue;
435 }
436
437 dismissed_categories_.insert(category_factory()->FromIDValue(id));
Marc Treib 2016/10/11 07:52:55 This might create the category, and the category o
dgn 2016/10/11 19:14:01 To be addressed by registering the articles at the
438 }
439 }
440
441 void ContentSuggestionsService::StoreDismissedCategoriesToPrefs() {
442 base::ListValue list;
443 for (Category category : dismissed_categories_) {
444 list.AppendInteger(category.id());
445 }
446
447 pref_service_->Set(prefs::kDismissedCategories, list);
448 }
449
334 } // namespace ntp_snippets 450 } // namespace ntp_snippets
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698