Chromium Code Reviews| OLD | NEW |
|---|---|
| 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" | |
| 19 #include "components/prefs/pref_service.h" | |
| 16 #include "ui/gfx/image/image.h" | 20 #include "ui/gfx/image/image.h" |
| 17 | 21 |
| 18 namespace ntp_snippets { | 22 namespace ntp_snippets { |
| 19 | 23 |
| 20 ContentSuggestionsService::ContentSuggestionsService( | 24 ContentSuggestionsService::ContentSuggestionsService( |
| 21 State state, | 25 State state, |
| 22 history::HistoryService* history_service, | 26 history::HistoryService* history_service, |
| 23 PrefService* pref_service) | 27 PrefService* pref_service) |
| 24 : state_(state), | 28 : state_(state), |
| 25 history_service_observer_(this), | 29 history_service_observer_(this), |
| 26 ntp_snippets_service_(nullptr), | 30 ntp_snippets_service_(nullptr), |
| 31 pref_service_(pref_service), | |
| 27 user_classifier_(pref_service) { | 32 user_classifier_(pref_service) { |
| 28 // Can be null in tests. | 33 // Can be null in tests. |
| 29 if (history_service) | 34 if (history_service) |
| 30 history_service_observer_.Add(history_service); | 35 history_service_observer_.Add(history_service); |
| 36 | |
| 37 RestoreDismissedCategoriesFromPrefs(); | |
| 31 } | 38 } |
| 32 | 39 |
| 33 ContentSuggestionsService::~ContentSuggestionsService() = default; | 40 ContentSuggestionsService::~ContentSuggestionsService() = default; |
| 34 | 41 |
| 35 void ContentSuggestionsService::Shutdown() { | 42 void ContentSuggestionsService::Shutdown() { |
| 36 ntp_snippets_service_ = nullptr; | 43 ntp_snippets_service_ = nullptr; |
| 37 suggestions_by_category_.clear(); | 44 suggestions_by_category_.clear(); |
| 38 providers_by_category_.clear(); | 45 providers_by_category_.clear(); |
| 39 categories_.clear(); | 46 categories_.clear(); |
| 40 providers_.clear(); | 47 providers_.clear(); |
| 41 state_ = State::DISABLED; | 48 state_ = State::DISABLED; |
| 42 FOR_EACH_OBSERVER(Observer, observers_, ContentSuggestionsServiceShutdown()); | 49 FOR_EACH_OBSERVER(Observer, observers_, ContentSuggestionsServiceShutdown()); |
| 43 } | 50 } |
| 44 | 51 |
| 52 // static | |
| 53 void ContentSuggestionsService::RegisterProfilePrefs( | |
| 54 PrefRegistrySimple* registry) { | |
| 55 registry->RegisterListPref(prefs::kDismissedCategories); | |
| 56 } | |
| 57 | |
| 45 CategoryStatus ContentSuggestionsService::GetCategoryStatus( | 58 CategoryStatus ContentSuggestionsService::GetCategoryStatus( |
| 46 Category category) const { | 59 Category category) const { |
| 47 if (state_ == State::DISABLED) { | 60 if (state_ == State::DISABLED) { |
| 48 return CategoryStatus::ALL_SUGGESTIONS_EXPLICITLY_DISABLED; | 61 return CategoryStatus::ALL_SUGGESTIONS_EXPLICITLY_DISABLED; |
| 49 } | 62 } |
| 50 | 63 |
| 51 auto iterator = providers_by_category_.find(category); | 64 auto iterator = providers_by_category_.find(category); |
| 52 if (iterator == providers_by_category_.end()) | 65 if (iterator == providers_by_category_.end()) |
| 53 return CategoryStatus::NOT_PROVIDED; | 66 return CategoryStatus::NOT_PROVIDED; |
| 54 | 67 |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 143 DCHECK(removed) << "The dismissed suggestion " << suggestion_id | 156 DCHECK(removed) << "The dismissed suggestion " << suggestion_id |
| 144 << " has already been removed. Providers must not call" | 157 << " has already been removed. Providers must not call" |
| 145 << " OnNewSuggestions in response to DismissSuggestion."; | 158 << " OnNewSuggestions in response to DismissSuggestion."; |
| 146 } | 159 } |
| 147 | 160 |
| 148 void ContentSuggestionsService::DismissCategory(Category category) { | 161 void ContentSuggestionsService::DismissCategory(Category category) { |
| 149 auto providers_it = providers_by_category_.find(category); | 162 auto providers_it = providers_by_category_.find(category); |
| 150 if (providers_it == providers_by_category_.end()) | 163 if (providers_it == providers_by_category_.end()) |
| 151 return; | 164 return; |
| 152 | 165 |
| 153 suggestions_by_category_[category].clear(); | 166 ContentSuggestionsProvider* provider = providers_it->second; |
| 154 dismissed_providers_by_category_[providers_it->first] = providers_it->second; | 167 UnregisterCategory(category, provider); |
| 155 providers_by_category_.erase(providers_it); | 168 |
| 156 categories_.erase( | 169 dismissed_providers_by_category_[category] = provider; |
| 157 std::find(categories_.begin(), categories_.end(), category)); | 170 StoreDismissedCategoriesToPrefs(); |
| 158 } | 171 } |
| 159 | 172 |
| 160 void ContentSuggestionsService::RestoreDismissedCategories() { | 173 void ContentSuggestionsService::RestoreDismissedCategories() { |
| 161 // Make a copy as the original will be modified during iteration. | 174 // Make a copy as the original will be modified during iteration. |
| 162 auto dismissed_providers_by_category_copy = dismissed_providers_by_category_; | 175 auto dismissed_providers_by_category_copy = dismissed_providers_by_category_; |
| 163 for (const auto& category_provider_pair : | 176 for (const auto& category_provider_pair : |
| 164 dismissed_providers_by_category_copy) { | 177 dismissed_providers_by_category_copy) { |
| 165 RegisterCategoryIfRequired(category_provider_pair.second, | 178 if (category_provider_pair.second) { |
| 166 category_provider_pair.first); | 179 RestoreDismissedCategory(category_provider_pair.first); |
| 180 } else { | |
| 181 // There is no provider for this category, we just remove the entry. | |
| 182 dismissed_providers_by_category_.erase(category_provider_pair.first); | |
|
Marc Treib
2016/10/17 08:44:46
I think RestoreDismissedCategory now handles a nul
dgn
2016/10/17 16:41:50
Done.
| |
| 183 } | |
| 167 } | 184 } |
| 185 StoreDismissedCategoriesToPrefs(); | |
| 168 DCHECK(dismissed_providers_by_category_.empty()); | 186 DCHECK(dismissed_providers_by_category_.empty()); |
| 169 } | 187 } |
| 170 | 188 |
| 171 void ContentSuggestionsService::AddObserver(Observer* observer) { | 189 void ContentSuggestionsService::AddObserver(Observer* observer) { |
| 172 observers_.AddObserver(observer); | 190 observers_.AddObserver(observer); |
| 173 } | 191 } |
| 174 | 192 |
| 175 void ContentSuggestionsService::RemoveObserver(Observer* observer) { | 193 void ContentSuggestionsService::RemoveObserver(Observer* observer) { |
| 176 observers_.RemoveObserver(observer); | 194 observers_.RemoveObserver(observer); |
| 177 } | 195 } |
| 178 | 196 |
| 179 void ContentSuggestionsService::RegisterProvider( | 197 void ContentSuggestionsService::RegisterProvider( |
| 180 std::unique_ptr<ContentSuggestionsProvider> provider) { | 198 std::unique_ptr<ContentSuggestionsProvider> provider) { |
| 181 DCHECK(state_ == State::ENABLED); | 199 DCHECK(state_ == State::ENABLED); |
| 182 providers_.push_back(std::move(provider)); | 200 providers_.push_back(std::move(provider)); |
| 183 } | 201 } |
| 184 | 202 |
| 185 //////////////////////////////////////////////////////////////////////////////// | 203 //////////////////////////////////////////////////////////////////////////////// |
| 186 // Private methods | 204 // Private methods |
| 187 | 205 |
| 188 void ContentSuggestionsService::OnNewSuggestions( | 206 void ContentSuggestionsService::OnNewSuggestions( |
| 189 ContentSuggestionsProvider* provider, | 207 ContentSuggestionsProvider* provider, |
| 190 Category category, | 208 Category category, |
| 191 std::vector<ContentSuggestion> suggestions) { | 209 std::vector<ContentSuggestion> suggestions) { |
| 192 if (RegisterCategoryIfRequired(provider, category)) | 210 if (TryRegisterProviderForCategory(provider, category)) { |
| 193 NotifyCategoryStatusChanged(category); | 211 NotifyCategoryStatusChanged(category); |
| 212 } else if (IsCategoryDismissed(category)) { | |
| 213 // The category has been registered as a dismissed one. We need to | |
| 214 // check if the dismissal can be cleared now that we received new data. | |
| 215 if (suggestions.empty()) | |
| 216 return; | |
| 217 | |
| 218 RestoreDismissedCategory(category); | |
| 219 StoreDismissedCategoriesToPrefs(); | |
| 220 | |
| 221 NotifyCategoryStatusChanged(category); | |
| 222 } | |
| 194 | 223 |
| 195 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) | 224 if (!IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) |
| 196 return; | 225 return; |
| 197 | 226 |
| 198 suggestions_by_category_[category] = std::move(suggestions); | 227 suggestions_by_category_[category] = std::move(suggestions); |
| 199 | 228 |
| 200 // The positioning of the bookmarks category depends on whether it's empty. | 229 // The positioning of the bookmarks category depends on whether it's empty. |
| 201 // TODO(treib): Remove this temporary hack, crbug.com/640568. | 230 // TODO(treib): Remove this temporary hack, crbug.com/640568. |
| 202 if (category.IsKnownCategory(KnownCategories::BOOKMARKS)) | 231 if (category.IsKnownCategory(KnownCategories::BOOKMARKS)) |
| 203 SortCategories(); | 232 SortCategories(); |
| 204 | 233 |
| 205 FOR_EACH_OBSERVER(Observer, observers_, OnNewSuggestions(category)); | 234 FOR_EACH_OBSERVER(Observer, observers_, OnNewSuggestions(category)); |
| 206 } | 235 } |
| 207 | 236 |
| 208 void ContentSuggestionsService::OnCategoryStatusChanged( | 237 void ContentSuggestionsService::OnCategoryStatusChanged( |
| 209 ContentSuggestionsProvider* provider, | 238 ContentSuggestionsProvider* provider, |
| 210 Category category, | 239 Category category, |
| 211 CategoryStatus new_status) { | 240 CategoryStatus new_status) { |
| 212 if (!IsCategoryStatusAvailable(new_status)) { | 241 if (!IsCategoryStatusAvailable(new_status)) { |
| 213 suggestions_by_category_.erase(category); | 242 suggestions_by_category_.erase(category); |
|
Marc Treib
2016/10/17 08:44:46
I think this can be removed, since there's now ano
dgn
2016/10/17 16:41:50
Done.
| |
| 214 } | 243 } |
| 215 if (new_status == CategoryStatus::NOT_PROVIDED) { | 244 if (new_status == CategoryStatus::NOT_PROVIDED) { |
| 216 DCHECK(providers_by_category_.find(category) != | 245 UnregisterCategory(category, provider); |
| 217 providers_by_category_.end()); | |
| 218 DCHECK_EQ(provider, providers_by_category_.find(category)->second); | |
| 219 DismissCategory(category); | |
| 220 } else { | 246 } else { |
| 221 RegisterCategoryIfRequired(provider, category); | 247 if (!IsCategoryStatusAvailable(new_status)) |
| 248 suggestions_by_category_.erase(category); | |
| 249 TryRegisterProviderForCategory(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 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 271 deleted_urls); | 301 deleted_urls); |
| 272 ClearHistory(begin, end, filter); | 302 ClearHistory(begin, end, filter); |
| 273 } | 303 } |
| 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::TryRegisterProviderForCategory( |
| 282 ContentSuggestionsProvider* provider, | 312 ContentSuggestionsProvider* provider, |
| 283 Category category) { | 313 Category category) { |
| 284 auto it = providers_by_category_.find(category); | 314 auto it = providers_by_category_.find(category); |
| 285 if (it != providers_by_category_.end()) { | 315 if (it != providers_by_category_.end()) { |
| 286 DCHECK_EQ(it->second, provider); | 316 DCHECK_EQ(it->second, provider); |
| 287 return false; | 317 return false; |
| 288 } | 318 } |
| 289 | 319 |
| 290 auto dismissed_it = dismissed_providers_by_category_.find(category); | 320 auto dismissed_it = dismissed_providers_by_category_.find(category); |
| 291 if (dismissed_it != dismissed_providers_by_category_.end()) { | 321 if (dismissed_it != dismissed_providers_by_category_.end()) { |
| 292 DCHECK_EQ(dismissed_it->second, provider); | 322 // The initialisation of dismissed categories registers them with |nullptr| |
| 293 dismissed_providers_by_category_.erase(dismissed_it); | 323 // for providers, we need to check for that to see if the provider is |
| 324 // already registered or not. | |
| 325 if (!dismissed_it->second) { | |
| 326 dismissed_it->second = provider; | |
| 327 } else { | |
| 328 DCHECK_EQ(dismissed_it->second, provider); | |
| 329 } | |
| 330 return false; | |
| 294 } | 331 } |
| 295 | 332 |
| 333 RegisterCategory(category, provider); | |
| 334 return true; | |
| 335 } | |
| 336 | |
| 337 void ContentSuggestionsService::RegisterCategory( | |
| 338 Category category, | |
| 339 ContentSuggestionsProvider* provider) { | |
| 340 DCHECK(!base::ContainsKey(providers_by_category_, category)); | |
| 341 DCHECK(!IsCategoryDismissed(category)); | |
| 342 | |
| 296 providers_by_category_[category] = provider; | 343 providers_by_category_[category] = provider; |
| 297 categories_.push_back(category); | 344 categories_.push_back(category); |
| 298 SortCategories(); | 345 SortCategories(); |
| 299 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) { | 346 if (IsCategoryStatusAvailable(provider->GetCategoryStatus(category))) { |
| 300 suggestions_by_category_.insert( | 347 suggestions_by_category_.insert( |
| 301 std::make_pair(category, std::vector<ContentSuggestion>())); | 348 std::make_pair(category, std::vector<ContentSuggestion>())); |
| 302 } | 349 } |
| 303 return true; | 350 } |
| 351 | |
| 352 void ContentSuggestionsService::UnregisterCategory( | |
| 353 Category category, | |
| 354 ContentSuggestionsProvider* provider) { | |
| 355 auto providers_it = providers_by_category_.find(category); | |
| 356 if (providers_it == providers_by_category_.end()) { | |
| 357 DCHECK(IsCategoryDismissed(category)); | |
| 358 return; | |
| 359 } | |
| 360 | |
| 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)); | |
| 365 suggestions_by_category_.erase(category); | |
| 304 } | 366 } |
| 305 | 367 |
| 306 bool ContentSuggestionsService::RemoveSuggestionByID( | 368 bool ContentSuggestionsService::RemoveSuggestionByID( |
| 307 const ContentSuggestion::ID& suggestion_id) { | 369 const ContentSuggestion::ID& suggestion_id) { |
| 308 std::vector<ContentSuggestion>* suggestions = | 370 std::vector<ContentSuggestion>* suggestions = |
| 309 &suggestions_by_category_[suggestion_id.category()]; | 371 &suggestions_by_category_[suggestion_id.category()]; |
| 310 auto position = | 372 auto position = |
| 311 std::find_if(suggestions->begin(), suggestions->end(), | 373 std::find_if(suggestions->begin(), suggestions->end(), |
| 312 [&suggestion_id](const ContentSuggestion& suggestion) { | 374 [&suggestion_id](const ContentSuggestion& suggestion) { |
| 313 return suggestion_id == suggestion.id(); | 375 return suggestion_id == suggestion.id(); |
| (...skipping 29 matching lines...) Expand all Loading... | |
| 343 if (bookmarks_empty) { | 405 if (bookmarks_empty) { |
| 344 if (left.IsKnownCategory(KnownCategories::BOOKMARKS)) | 406 if (left.IsKnownCategory(KnownCategories::BOOKMARKS)) |
| 345 return false; | 407 return false; |
| 346 if (right.IsKnownCategory(KnownCategories::BOOKMARKS)) | 408 if (right.IsKnownCategory(KnownCategories::BOOKMARKS)) |
| 347 return true; | 409 return true; |
| 348 } | 410 } |
| 349 return category_factory_.CompareCategories(left, right); | 411 return category_factory_.CompareCategories(left, right); |
| 350 }); | 412 }); |
| 351 } | 413 } |
| 352 | 414 |
| 415 bool ContentSuggestionsService::IsCategoryDismissed(Category category) const { | |
| 416 return base::ContainsKey(dismissed_providers_by_category_, category); | |
| 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()); | |
|
Marc Treib
2016/10/17 08:44:46
DCHECK_NE? Or alternatively, DCHECK(base::Contains
dgn
2016/10/17 16:41:50
Done.
| |
| 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; | |
| 426 dismissed_providers_by_category_.erase(dismissed_it); | |
| 427 | |
| 428 if (provider) | |
| 429 RegisterCategory(category, provider); | |
| 430 } | |
| 431 | |
| 432 void ContentSuggestionsService::RestoreDismissedCategoriesFromPrefs() { | |
| 433 // This must only be called at startup. | |
| 434 DCHECK(dismissed_providers_by_category_.empty()); | |
| 435 DCHECK(providers_by_category_.empty()); | |
| 436 | |
| 437 const base::ListValue* list = | |
| 438 pref_service_->GetList(prefs::kDismissedCategories); | |
| 439 for (const std::unique_ptr<base::Value>& entry : *list) { | |
| 440 int id = 0; | |
| 441 if (!entry->GetAsInteger(&id)) { | |
| 442 DLOG(WARNING) << "Invalid category pref value: " << *entry; | |
| 443 continue; | |
| 444 } | |
| 445 | |
| 446 // When the provider is registered, it will be stored in this map. | |
| 447 dismissed_providers_by_category_[category_factory()->FromIDValue(id)] = | |
| 448 nullptr; | |
| 449 } | |
| 450 } | |
| 451 | |
| 452 void ContentSuggestionsService::StoreDismissedCategoriesToPrefs() { | |
| 453 base::ListValue list; | |
| 454 for (const auto& category_provider_pair : dismissed_providers_by_category_) { | |
| 455 list.AppendInteger(category_provider_pair.first.id()); | |
| 456 } | |
| 457 | |
| 458 pref_service_->Set(prefs::kDismissedCategories, list); | |
| 459 } | |
| 460 | |
| 353 } // namespace ntp_snippets | 461 } // namespace ntp_snippets |
| OLD | NEW |