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

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

Issue 2406573002: 📰 Persist category dismissals (Closed)
Patch Set: rebase and rewrite 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 }
50 62
51 auto iterator = providers_by_category_.find(category); 63 auto iterator = providers_by_category_.find(category);
52 if (iterator == providers_by_category_.end()) 64 if (iterator == providers_by_category_.end())
53 return CategoryStatus::NOT_PROVIDED; 65 return CategoryStatus::NOT_PROVIDED;
54 66
(...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after
143 DCHECK(removed) << "The dismissed suggestion " << suggestion_id 155 DCHECK(removed) << "The dismissed suggestion " << suggestion_id
144 << " has already been removed. Providers must not call" 156 << " has already been removed. Providers must not call"
145 << " OnNewSuggestions in response to DismissSuggestion."; 157 << " OnNewSuggestions in response to DismissSuggestion.";
146 } 158 }
147 159
148 void ContentSuggestionsService::DismissCategory(Category category) { 160 void ContentSuggestionsService::DismissCategory(Category category) {
149 auto providers_it = providers_by_category_.find(category); 161 auto providers_it = providers_by_category_.find(category);
150 if (providers_it == providers_by_category_.end()) 162 if (providers_it == providers_by_category_.end())
151 return; 163 return;
152 164
153 suggestions_by_category_[category].clear(); 165 suggestions_by_category_.erase(category);
dgn 2016/10/13 14:46:51 I'm not sure why we just cleared the entry before,
Marc Treib 2016/10/13 15:59:00 No, I think I missed this in a previous CL - erase
dgn 2016/10/14 21:20:28 It's not done currently, but it would make sense t
154 dismissed_providers_by_category_[providers_it->first] = providers_it->second; 166 ContentSuggestionsProvider* provider = providers_it->second;
155 providers_by_category_.erase(providers_it); 167
156 categories_.erase( 168 UnregisterCategory(category, provider);
157 std::find(categories_.begin(), categories_.end(), category)); 169
170 dismissed_providers_by_category_[category] = provider;
171 StoreDismissedCategoriesToPrefs();
158 } 172 }
159 173
160 void ContentSuggestionsService::RestoreDismissedCategories() { 174 void ContentSuggestionsService::RestoreDismissedCategories() {
161 // Make a copy as the original will be modified during iteration. 175 // Make a copy as the original will be modified during iteration.
162 auto dismissed_providers_by_category_copy = dismissed_providers_by_category_; 176 auto dismissed_providers_by_category_copy = dismissed_providers_by_category_;
163 for (const auto& category_provider_pair : 177 for (const auto& category_provider_pair :
164 dismissed_providers_by_category_copy) { 178 dismissed_providers_by_category_copy) {
165 RegisterCategoryIfRequired(category_provider_pair.second, 179 if (category_provider_pair.second) {
166 category_provider_pair.first); 180 RestoreDismissedCategory(category_provider_pair.first);
181 } else {
182 // There is no provider for this category, we just remove the entry.
183 dismissed_providers_by_category_.erase(category_provider_pair.first);
184 }
167 } 185 }
186 StoreDismissedCategoriesToPrefs();
168 DCHECK(dismissed_providers_by_category_.empty()); 187 DCHECK(dismissed_providers_by_category_.empty());
169 } 188 }
170 189
171 void ContentSuggestionsService::AddObserver(Observer* observer) { 190 void ContentSuggestionsService::AddObserver(Observer* observer) {
172 observers_.AddObserver(observer); 191 observers_.AddObserver(observer);
173 } 192 }
174 193
175 void ContentSuggestionsService::RemoveObserver(Observer* observer) { 194 void ContentSuggestionsService::RemoveObserver(Observer* observer) {
176 observers_.RemoveObserver(observer); 195 observers_.RemoveObserver(observer);
177 } 196 }
178 197
179 void ContentSuggestionsService::RegisterProvider( 198 void ContentSuggestionsService::RegisterProvider(
180 std::unique_ptr<ContentSuggestionsProvider> provider) { 199 std::unique_ptr<ContentSuggestionsProvider> provider) {
181 DCHECK(state_ == State::ENABLED); 200 DCHECK(state_ == State::ENABLED);
182 providers_.push_back(std::move(provider)); 201 providers_.push_back(std::move(provider));
183 } 202 }
184 203
185 //////////////////////////////////////////////////////////////////////////////// 204 ////////////////////////////////////////////////////////////////////////////////
186 // Private methods 205 // Private methods
187 206
188 void ContentSuggestionsService::OnNewSuggestions( 207 void ContentSuggestionsService::OnNewSuggestions(
189 ContentSuggestionsProvider* provider, 208 ContentSuggestionsProvider* provider,
190 Category category, 209 Category category,
191 std::vector<ContentSuggestion> suggestions) { 210 std::vector<ContentSuggestion> suggestions) {
192 if (RegisterCategoryIfRequired(provider, category)) 211 if (RegisterCategoryIfRequired(provider, category))
193 NotifyCategoryStatusChanged(category); 212 NotifyCategoryStatusChanged(category);
194 213
214 if (IsCategoryDismissed(category)) {
Marc Treib 2016/10/13 15:59:00 else if? If the category was just successfully reg
dgn 2016/10/14 21:20:28 Done.
215 // The category has been registered as a dismissed one. We need to
216 // check if the dismissal can be cleared now that we received new data.
217 if (suggestions.empty())
218 return;
219
220 RestoreDismissedCategory(category);
221 StoreDismissedCategoriesToPrefs();
222
223 NotifyCategoryStatusChanged(category);
224 }
225
195 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) 226 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category)))
196 return; 227 return;
197 228
198 suggestions_by_category_[category] = std::move(suggestions); 229 suggestions_by_category_[category] = std::move(suggestions);
199 230
200 // The positioning of the bookmarks category depends on whether it's empty. 231 // The positioning of the bookmarks category depends on whether it's empty.
201 // TODO(treib): Remove this temporary hack, crbug.com/640568. 232 // TODO(treib): Remove this temporary hack, crbug.com/640568.
202 if (category.IsKnownCategory(KnownCategories::BOOKMARKS)) 233 if (category.IsKnownCategory(KnownCategories::BOOKMARKS))
203 SortCategories(); 234 SortCategories();
204 235
205 FOR_EACH_OBSERVER(Observer, observers_, OnNewSuggestions(category)); 236 FOR_EACH_OBSERVER(Observer, observers_, OnNewSuggestions(category));
206 } 237 }
207 238
208 void ContentSuggestionsService::OnCategoryStatusChanged( 239 void ContentSuggestionsService::OnCategoryStatusChanged(
209 ContentSuggestionsProvider* provider, 240 ContentSuggestionsProvider* provider,
210 Category category, 241 Category category,
211 CategoryStatus new_status) { 242 CategoryStatus new_status) {
212 if (!IsCategoryStatusAvailable(new_status)) { 243 if (!IsCategoryStatusAvailable(new_status)) {
213 suggestions_by_category_.erase(category); 244 suggestions_by_category_.erase(category);
214 } 245 }
215 if (new_status == CategoryStatus::NOT_PROVIDED) { 246 if (new_status == CategoryStatus::NOT_PROVIDED) {
216 DCHECK(providers_by_category_.find(category) != 247 UnregisterCategory(category, provider);
217 providers_by_category_.end());
218 DCHECK_EQ(provider, providers_by_category_.find(category)->second);
219 DismissCategory(category);
220 } else { 248 } else {
221 RegisterCategoryIfRequired(provider, category); 249 RegisterCategoryIfRequired(provider, category);
222 DCHECK_EQ(new_status, provider->GetCategoryStatus(category)); 250 DCHECK_EQ(new_status, provider->GetCategoryStatus(category));
223 } 251 }
224 NotifyCategoryStatusChanged(category); 252
253 if (!IsCategoryDismissed(category))
254 NotifyCategoryStatusChanged(category);
225 } 255 }
226 256
227 void ContentSuggestionsService::OnSuggestionInvalidated( 257 void ContentSuggestionsService::OnSuggestionInvalidated(
228 ContentSuggestionsProvider* provider, 258 ContentSuggestionsProvider* provider,
229 const ContentSuggestion::ID& suggestion_id) { 259 const ContentSuggestion::ID& suggestion_id) {
230 RemoveSuggestionByID(suggestion_id); 260 RemoveSuggestionByID(suggestion_id);
231 FOR_EACH_OBSERVER(Observer, observers_, 261 FOR_EACH_OBSERVER(Observer, observers_,
232 OnSuggestionInvalidated(suggestion_id)); 262 OnSuggestionInvalidated(suggestion_id));
233 } 263 }
234 264
(...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after
274 } 304 }
275 305
276 void ContentSuggestionsService::HistoryServiceBeingDeleted( 306 void ContentSuggestionsService::HistoryServiceBeingDeleted(
277 history::HistoryService* history_service) { 307 history::HistoryService* history_service) {
278 history_service_observer_.RemoveAll(); 308 history_service_observer_.RemoveAll();
279 } 309 }
280 310
281 bool ContentSuggestionsService::RegisterCategoryIfRequired( 311 bool ContentSuggestionsService::RegisterCategoryIfRequired(
282 ContentSuggestionsProvider* provider, 312 ContentSuggestionsProvider* provider,
283 Category category) { 313 Category category) {
314 // Search for the category in both the regular and dismissed lists.
284 auto it = providers_by_category_.find(category); 315 auto it = providers_by_category_.find(category);
Marc Treib 2016/10/13 15:59:00 Can we early-out here if |it != providers_by_categ
dgn 2016/10/14 21:20:28 Done, also rewrote the function since it removes b
285 if (it != providers_by_category_.end()) { 316 auto dismissed_it = dismissed_providers_by_category_.find(category);
286 DCHECK_EQ(it->second, provider); 317
287 return false; 318 // We will need to register the provider for the category if it's absent from
319 // both. Note that the initialisation of dismissed categories registers them
320 // with |nullptr| for providers, we need to check for that too.
321 bool is_dismissed = dismissed_it != dismissed_providers_by_category_.end();
322 bool is_new = it == providers_by_category_.end() &&
323 (!is_dismissed || !dismissed_it->second);
Marc Treib 2016/10/13 15:59:00 Maybe |category_is_dismissed| and |provider_is_new
dgn 2016/10/14 21:20:28 Done.
324
325 if (is_new) {
326 if (is_dismissed) {
327 dismissed_it->second = provider;
328 return false;
329 }
330 RegisterCategoryInternal(category, provider);
331 } else {
332 DCHECK_EQ(is_dismissed ? dismissed_it->second : it->second, provider);
288 } 333 }
289 334
290 auto dismissed_it = dismissed_providers_by_category_.find(category); 335 return is_new;
291 if (dismissed_it != dismissed_providers_by_category_.end()) { 336 }
292 DCHECK_EQ(dismissed_it->second, provider); 337
293 dismissed_providers_by_category_.erase(dismissed_it); 338 void ContentSuggestionsService::RegisterCategoryInternal(
294 } 339 Category category,
340 ContentSuggestionsProvider* provider) {
341 DCHECK(providers_by_category_.find(category) == providers_by_category_.end());
Marc Treib 2016/10/13 15:59:00 optional nit: base::ContainsKey
dgn 2016/10/14 21:20:28 Thanks! I was annoyed the STL doesn't provide anyt
342 DCHECK(!IsCategoryDismissed(category));
295 343
296 providers_by_category_[category] = provider; 344 providers_by_category_[category] = provider;
297 categories_.push_back(category); 345 categories_.push_back(category);
298 SortCategories(); 346 SortCategories();
299 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) { 347 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) {
300 suggestions_by_category_.insert( 348 suggestions_by_category_.insert(
301 std::make_pair(category, std::vector<ContentSuggestion>())); 349 std::make_pair(category, std::vector<ContentSuggestion>()));
302 } 350 }
303 return true; 351 }
352
353 void ContentSuggestionsService::UnregisterCategory(
354 Category category,
355 ContentSuggestionsProvider* provider) {
356 if (IsCategoryDismissed(category))
Marc Treib 2016/10/13 15:59:00 Why?
dgn 2016/10/14 21:20:28 Because when OnCategoryStatusChanged() is called w
357 return;
358
359 auto providers_it = providers_by_category_.find(category);
360 DCHECK(providers_it != providers_by_category_.end());
361 DCHECK_EQ(provider, providers_it->second);
362 providers_by_category_.erase(providers_it);
363 categories_.erase(
364 std::find(categories_.begin(), categories_.end(), category));
304 } 365 }
305 366
306 bool ContentSuggestionsService::RemoveSuggestionByID( 367 bool ContentSuggestionsService::RemoveSuggestionByID(
307 const ContentSuggestion::ID& suggestion_id) { 368 const ContentSuggestion::ID& suggestion_id) {
308 std::vector<ContentSuggestion>* suggestions = 369 std::vector<ContentSuggestion>* suggestions =
309 &suggestions_by_category_[suggestion_id.category()]; 370 &suggestions_by_category_[suggestion_id.category()];
310 auto position = 371 auto position =
311 std::find_if(suggestions->begin(), suggestions->end(), 372 std::find_if(suggestions->begin(), suggestions->end(),
312 [&suggestion_id](const ContentSuggestion& suggestion) { 373 [&suggestion_id](const ContentSuggestion& suggestion) {
313 return suggestion_id == suggestion.id(); 374 return suggestion_id == suggestion.id();
(...skipping 29 matching lines...) Expand all
343 if (bookmarks_empty) { 404 if (bookmarks_empty) {
344 if (left.IsKnownCategory(KnownCategories::BOOKMARKS)) 405 if (left.IsKnownCategory(KnownCategories::BOOKMARKS))
345 return false; 406 return false;
346 if (right.IsKnownCategory(KnownCategories::BOOKMARKS)) 407 if (right.IsKnownCategory(KnownCategories::BOOKMARKS))
347 return true; 408 return true;
348 } 409 }
349 return category_factory_.CompareCategories(left, right); 410 return category_factory_.CompareCategories(left, right);
350 }); 411 });
351 } 412 }
352 413
414 bool ContentSuggestionsService::IsCategoryDismissed(Category category) const {
415 return dismissed_providers_by_category_.find(category) !=
416 dismissed_providers_by_category_.end();
Marc Treib 2016/10/13 15:59:00 optional nit: base::ContainsKey
dgn 2016/10/14 21:20:28 Done.
417 }
418
419 void ContentSuggestionsService::RestoreDismissedCategory(Category category) {
420 auto dismissed_it = dismissed_providers_by_category_.find(category);
421 DCHECK(dismissed_it != dismissed_providers_by_category_.end());
422
423 // Keep the reference to the provider and remove it from the dismissed ones,
424 // because the category registration enforces that it's not dismissed.
425 ContentSuggestionsProvider* provider = dismissed_it->second;
Marc Treib 2016/10/13 15:59:01 Can provider be null here? If not, DCHECK that?
dgn 2016/10/14 21:20:28 Good catch, it's possible, yes.
426 dismissed_providers_by_category_.erase(dismissed_it);
427
428 RegisterCategoryInternal(category, provider);
429 }
430
431 void ContentSuggestionsService::RestoreDismissedCategoriesFromPrefs() {
432 // This must only be called at startup.
433 DCHECK(dismissed_providers_by_category_.empty());
434 DCHECK(providers_by_category_.empty());
435
436 const base::ListValue* list =
437 pref_service_->GetList(prefs::kDismissedCategories);
438 for (const std::unique_ptr<base::Value>& entry : *list) {
439 int id = 0;
440 if (!entry->GetAsInteger(&id)) {
441 DLOG(WARNING) << "Invalid category pref value: " << *entry;
442 continue;
443 }
444
445 // When the provider is registered, it will be moved to this map.
Marc Treib 2016/10/13 15:59:00 nit: s/moved to/stored in/ ? "moved to" sounds lik
dgn 2016/10/14 21:20:28 Done.
446 dismissed_providers_by_category_[category_factory()->FromIDValue(id)] =
447 nullptr;
448 }
449 }
450
451 void ContentSuggestionsService::StoreDismissedCategoriesToPrefs() {
452 base::ListValue list;
453 for (const auto& category_provider_pair : dismissed_providers_by_category_) {
454 list.AppendInteger(category_provider_pair.first.id());
455 }
456
457 pref_service_->Set(prefs::kDismissedCategories, list);
458 }
459
353 } // namespace ntp_snippets 460 } // namespace ntp_snippets
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698